Implementing Orchard.JobsQueue

This commit is contained in:
Sebastien Ros
2014-01-14 11:39:01 -08:00
parent bbd07239e4
commit 70be174c72
91 changed files with 1132 additions and 1105 deletions

View File

@@ -230,10 +230,6 @@
<Project>{D9A7B330-CD22-4DA1-A95A-8DE1982AD8EB}</Project>
<Name>Orchard.Media</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Web\Modules\Orchard.Messaging\Orchard.Messaging.csproj">
<Project>{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}</Project>
<Name>Orchard.Messaging</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Web\Modules\Orchard.Modules\Orchard.Modules.csproj">
<Project>{17F86780-9A1F-4AA1-86F1-875EEC2730C7}</Project>
<Name>Orchard.Modules</Name>

View File

@@ -1,25 +1,33 @@
using System;
using System.Collections.Generic;
using Orchard.Email.Models;
using System.Collections.Generic;
using Orchard.Email.Services;
using Orchard.Events;
using Orchard.Localization;
using Orchard.Messaging.Services;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Email.Activities {
public class EmailActivity : Task {
private readonly IMessageQueueService _messageQueueManager;
public interface IJobsQueueService : IEventHandler {
void Enqueue(string message, object parameters, int priority);
}
public EmailActivity(IMessageQueueService messageQueueManager) {
_messageQueueManager = messageQueueManager;
public class EmailActivity : Task {
private readonly IMessageService _messageService;
private readonly IJobsQueueService _jobsQueueService;
public EmailActivity(
IMessageService messageService,
IJobsQueueService jobsQueueService
) {
_messageService = messageService;
_jobsQueueService = jobsQueueService;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext) {
return new[] { T("Queued") };
return new[] { T("Done") };
}
public override string Form {
@@ -41,24 +49,27 @@ namespace Orchard.Email.Activities {
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext) {
var priority = activityContext.GetState<int>("Priority");
var body = activityContext.GetState<string>("Body");
var subject = activityContext.GetState<string>("Subject");
var recipients = Split(activityContext.GetState<string>("RecipientAddress"));
var payload = new EmailMessage {
Subject = subject,
Body = body,
Recipients = recipients
var recipients = activityContext.GetState<string>("Recipients");
var parameters = new Dictionary<string, object> {
{"Subject", subject},
{"Body", body},
{"Recipients", recipients}
};
_messageQueueManager.Enqueue(SmtpMessageChannel.MessageType, payload, priority);
var queued = activityContext.GetState<bool>("Queued");
yield return T("Queued");
}
if (!queued) {
_messageService.Send(SmtpMessageChannel.MessageType, parameters);
}
else {
var priority = activityContext.GetState<int>("Priority");
_jobsQueueService.Enqueue("IMessageService.Send", new { type = SmtpMessageChannel.MessageType, parameters = parameters }, priority);
}
private static string[] Split(string value) {
return !String.IsNullOrWhiteSpace(value) ? value.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) : new string[0];
yield return T("Done");
}
}
}

View File

@@ -1,28 +1,35 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.DisplayManagement;
using Orchard.Environment.Features;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Email.Forms {
public class EmailForm : Component, IFormProvider {
private readonly IFeatureManager _featureManager;
protected dynamic New { get; set; }
public EmailForm(IShapeFactory shapeFactory) {
public EmailForm(IShapeFactory shapeFactory,
IFeatureManager featureManager) {
_featureManager = featureManager;
New = shapeFactory;
}
public void Describe(DescribeContext context) {
Func<IShapeFactory, dynamic> formFactory =
shape => {
var jobsQueueEnabled = _featureManager.GetEnabledFeatures().Any(x => x.Id == "Orchard.JobsQueue");
var form = New.Form(
Id: "EmailActivity",
_Type: New.FieldSet(
Title: T("Send to"),
_RecipientAddress: New.Textbox(
Id: "recipient-address",
Name: "RecipientAddress",
Title: T("Email Address"),
_Recipients: New.Textbox(
Id: "recipients",
Name: "Recipients",
Title: T("Email Addresses"),
Description: T("Specify a comma-separated list of recipient email addresses. To include a display name, use the following format: John Doe &lt;john.doe@outlook.com&gt;"),
Classes: new[] {"large", "text", "tokenized"}),
_Subject: New.Textbox(
@@ -34,17 +41,27 @@ namespace Orchard.Email.Forms {
Id: "Body", Name: "Body",
Title: T("Body"),
Description: T("The body of the email message."),
Classes: new[] {"tokenized"}),
_Priority: New.SelectList(
Classes: new[] {"tokenized"})
));
if (jobsQueueEnabled) {
form._Type._Queued(New.Checkbox(
Id: "Queued", Name: "Queued",
Title: T("Queued"),
Checked: false, Value: "true",
Description: T("Check send it as a queued job.")));
form._Type._Priority(New.SelectList(
Id: "priority",
Name: "Priority",
Title: T("Priority"),
Description: ("The priority of this message.")
)));
));
form._Type._Priority.Add(new SelectListItem { Value = "-50", Text = T("Low").Text });
form._Type._Priority.Add(new SelectListItem { Value = "0", Text = T("Normal").Text });
form._Type._Priority.Add(new SelectListItem { Value = "50", Text = T("High").Text });
form._Type._Priority.Add(new SelectListItem { Value = "-50", Text = T("Low").Text });
form._Type._Priority.Add(new SelectListItem { Value = "0", Text = T("Normal").Text });
form._Type._Priority.Add(new SelectListItem { Value = "50", Text = T("High").Text });
}
return form;
};
@@ -63,12 +80,12 @@ namespace Orchard.Email.Forms {
public void Validating(ValidatingContext context) {
if (context.FormName != "EmailActivity") return;
var recipientAddress = context.ValueProvider.GetValue("RecipientAddress").AttemptedValue;
var recipients = context.ValueProvider.GetValue("Recipients").AttemptedValue;
var subject = context.ValueProvider.GetValue("Subject").AttemptedValue;
var body = context.ValueProvider.GetValue("Body").AttemptedValue;
if (String.IsNullOrWhiteSpace(recipientAddress)) {
context.ModelState.AddModelError("RecipientAddress", T("You must specify at least one recipient.").Text);
if (String.IsNullOrWhiteSpace(recipients)) {
context.ModelState.AddModelError("Recipients", T("You must specify at least one recipient.").Text);
}
if (String.IsNullOrWhiteSpace(subject)) {

View File

@@ -2,6 +2,6 @@
public class EmailMessage {
public string Subject { get; set; }
public string Body { get; set; }
public string[] Recipients { get; set; }
public string Recipients { get; set; }
}
}

View File

@@ -10,4 +10,4 @@ Features:
Name: Email Messaging
FeatureDescription: Email Messaging services.
Category: Messaging
Dependencies: Orchard.Messaging, Orchard.Workflows
Dependencies: Orchard.Workflows

View File

@@ -106,10 +106,6 @@
<Project>{642a49d7-8752-4177-80d6-bfbbcfad3de0}</Project>
<Name>Orchard.Forms</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Messaging\Orchard.Messaging.csproj">
<Project>{085948ff-0e9b-4a9a-b564-f8b8b4bdddbc}</Project>
<Name>Orchard.Messaging</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Workflows\Orchard.Workflows.csproj">
<Project>{7059493c-8251-4764-9c1e-2368b8b485bc}</Project>
<Name>Orchard.Workflows</Name>

View File

@@ -12,10 +12,9 @@ namespace Orchard.Email.Services {
public MessageChannelSelectorResult GetChannel(string messageType, object payload) {
if (messageType == "Email") {
var workContext = _workContextAccessor.GetContext();
var channel = workContext.Resolve<ISmtpChannel>();
return new MessageChannelSelectorResult {
Priority = 50,
MessageChannel = channel
MessageChannel = () => workContext.Resolve<ISmtpChannel>()
};
}

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Logging;
@@ -35,17 +37,18 @@ namespace Orchard.Email.Services {
_smtpClientField.Value.Dispose();
}
public void Process(string payload) {
public void Process(IDictionary<string, object> parameters) {
if (!_smtpSettings.IsValid()) {
return;
}
var emailMessage = JsonConvert.DeserializeObject<EmailMessage>(payload);
if (emailMessage == null) {
return;
}
var emailMessage = new EmailMessage {
Body = parameters["Body"] as string,
Subject = parameters["Subject"] as string,
Recipients = parameters["Recipients"] as string
};
if (emailMessage.Recipients.Length == 0) {
Logger.Error("Email message doesn't have any recipient");
@@ -65,7 +68,7 @@ namespace Orchard.Email.Services {
};
try {
foreach (var recipient in emailMessage.Recipients) {
foreach (var recipient in emailMessage.Recipients.Split(new [] {',', ';'}, StringSplitOptions.RemoveEmptyEntries)) {
mailMessage.To.Add(new MailAddress(recipient));
}

View File

@@ -1,4 +1,4 @@
@* Override this template to alter the email messages sent by the Smtp channel *@
<p style="font-family: 'Segoe UI', Arial, helvetica, sans-serif; font-size: 10pt; ">
<div style="font-family: 'Segoe UI', Arial, helvetica, sans-serif; font-size: 10pt; ">
@Model.Content
</p>
</div>

View File

@@ -1,15 +1,17 @@
using Orchard.UI.Navigation;
using Orchard.Environment.Extensions;
using Orchard.UI.Navigation;
namespace Orchard.Messaging {
namespace Orchard.JobsQueue {
[OrchardFeature("Orchard.JobsQueue.UI")]
public class AdminMenu : Component, INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder
.AddImageSet("messaging")
.Add(T("Message Queue"), "15.0", item => {
item.Action("List", "Admin", new { area = "Orchard.Messaging" });
.AddImageSet("jobsqueue")
.Add(T("Jobs Queue"), "15.0", item => {
item.Action("List", "Admin", new { area = "Orchard.JobsQueue" });
item.LinkToFirstChild(false);
});
}

View File

@@ -0,0 +1,91 @@
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Environment.Extensions;
using Orchard.Localization;
using Orchard.JobsQueue.Models;
using Orchard.JobsQueue.Services;
using Orchard.Mvc;
using Orchard.UI.Admin;
using Orchard.UI.Navigation;
using Orchard.UI.Notify;
namespace Orchard.JobsQueue.Controllers {
[Admin]
[OrchardFeature("Orchard.JobsQueue.UI")]
public class AdminController : Controller {
private readonly IJobsQueueManager _jobsQueueManager;
private readonly IOrchardServices _services;
private readonly IJobsQueueProcessor _jobsQueueProcessor;
public AdminController(
IJobsQueueManager jobsQueueManager,
IShapeFactory shapeFactory,
IOrchardServices services,
IJobsQueueProcessor jobsQueueProcessor) {
_jobsQueueManager = jobsQueueManager;
_services = services;
_jobsQueueProcessor = jobsQueueProcessor;
New = shapeFactory;
T = NullLocalizer.Instance;
}
public dynamic New { get; set; }
public Localizer T { get; set; }
public ActionResult Details(int id, string returnUrl) {
var job = _jobsQueueManager.GetJob(id);
if (!Url.IsLocalUrl(returnUrl))
returnUrl = Url.Action("List");
var model = New.ViewModel().Job(job).ReturnUrl(returnUrl);
return View(model);
}
public ActionResult List(PagerParameters pagerParameters) {
var pager = new Pager(_services.WorkContext.CurrentSite, pagerParameters);
var jobsCount = _jobsQueueManager.GetJobsCount();
var jobs = _jobsQueueManager.GetJobs(pager.GetStartIndex(), pager.PageSize).ToList();
var model = _services.New.ViewModel()
.Pager(_services.New.Pager(pager).TotalItemCount(jobsCount))
.JobsQueueStatus(_services.WorkContext.CurrentSite.As<JobsQueueSettingsPart>().Status)
.Jobs(jobs)
;
return View(model);
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Filter")]
public ActionResult Filter() {
return RedirectToAction("List");
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Resume")]
public ActionResult Resume() {
_jobsQueueManager.Resume();
_services.Notifier.Information(T("The queue has been resumed."));
return RedirectToAction("List");
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Pause")]
public ActionResult Pause() {
_jobsQueueManager.Pause();
_services.Notifier.Information(T("The queue has been paused."));
return RedirectToAction("List");
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Process")]
public ActionResult Process() {
_jobsQueueProcessor.ProcessQueue();
_services.Notifier.Information(T("Processing has started."));
return RedirectToAction("List");
}
}
}

View File

@@ -0,0 +1,43 @@
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Localization;
using Orchard.JobsQueue.Models;
using Orchard.JobsQueue.ViewModels;
using Orchard.Messaging.Models;
namespace Orchard.JobsQueue.Drivers {
[UsedImplicitly]
public class JobsQueueSettingsPartDriver : ContentPartDriver<JobsQueueSettingsPart> {
private const string TemplateName = "Parts/JobsQueueSettings";
public IOrchardServices Services { get; set; }
public JobsQueueSettingsPartDriver(IOrchardServices services) {
Services = services;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override string Prefix { get { return "JobsQueueSettings"; } }
protected override DriverResult Editor(JobsQueueSettingsPart part, dynamic shapeHelper) {
var model = new JobsQueueSettingsPartViewModel {
JobsQueueSettings = part
};
return ContentShape("Parts_JobsQueueSettings_Edit", () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
protected override DriverResult Editor(JobsQueueSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
var model = new JobsQueueSettingsPartViewModel {
JobsQueueSettings = part
};
updater.TryUpdateModel(model, Prefix, null, null);
return ContentShape("Parts_JobsQueueSettings_Edit", () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Orchard.Messaging.Extensions {
namespace Orchard.JobsQueue.Extensions {
public static class StringExtensions {
public static string TrimSafe(this string value) {
return value != null ? value.Trim() : null;

View File

@@ -0,0 +1,12 @@
using JetBrains.Annotations;
using Orchard.ContentManagement.Handlers;
using Orchard.JobsQueue.Models;
namespace Orchard.JobsQueue.Handlers {
[UsedImplicitly]
public class JobsQueueSettingsPartHandler : ContentHandler {
public JobsQueueSettingsPartHandler() {
Filters.Add(new ActivatingFilter<JobsQueueSettingsPart>("Site"));
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using Orchard.Data.Migration;
namespace Orchard.JobsQueue {
public class Migrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("QueuedJobRecord", table => table
.Column<int>("Id", c => c.Identity().PrimaryKey())
.Column<string>("Message", c => c.WithLength(64))
.Column<string>("Parameters", c => c.Unlimited())
.Column<int>("Priority", c => c.WithDefault(0))
.Column<DateTime>("CreatedUtc")
);
return 1;
}
}
}

View File

@@ -1,11 +1,11 @@
using Orchard.ContentManagement;
namespace Orchard.Messaging.Models {
public class MessageSettingsPart : ContentPart {
public MessageQueueStatus Status {
get { return this.Retrieve(x => x.Status); }
set { this.Store(x => x.Status, value); }
}
}
}
using Orchard.ContentManagement;
namespace Orchard.JobsQueue.Models {
public class JobsQueueSettingsPart : ContentPart {
public JobsQueueStatus Status {
get { return this.Retrieve(x => x.Status); }
set { this.Store(x => x.Status, value); }
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Orchard.JobsQueue.Models {
public enum JobsQueueStatus {
Idle,
Paused
}
}

View File

@@ -0,0 +1,15 @@
using System;
using Orchard.Data.Conventions;
namespace Orchard.JobsQueue.Models {
public class QueuedJobRecord {
public virtual int Id { get; set; }
public virtual int Priority { get; set; }
public virtual string Message { get; set; }
[StringLengthMax]
public virtual string Parameters { get; set; }
public virtual DateTime CreatedUtc { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
Name: Jobs Queue
AntiForgery: enabled
Author: The Orchard Team
Website: http://orchardproject.net
Version: 1.7.2
OrchardVersion: 1.7.2
Description: This module provides a jobs queue to process jobs asynchronously.
Features:
Orchard.JobsQueue:
Description: Provides a jobs queue to process jobs asynchronously.
Category: Developer
Dependencies: Settings, Orchard.TaskLease
Name: Jobs Queue
Orchard.JobsQueue.UI:
Description: Provides a UI to manage queued jobs.
Category: Developer
Dependencies: Orchard.JobsQueue
Name: Jobs Queue UI

View File

@@ -1,185 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>
</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}</ProjectGuid>
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.Messaging</RootNamespace>
<AssemblyName>Orchard.Messaging</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileUpgradeFlags>
</FileUpgradeFlags>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>4.0</OldToolsVersion>
<TargetFrameworkProfile />
<UseIISExpress>false</UseIISExpress>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Web" />
<Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.XML" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="Services\DefaultMessageService.cs" />
<Compile Include="Services\IMessageChannelSelector.cs" />
<Compile Include="Services\IMessageQueueProcessor.cs" />
<Compile Include="Services\IMessageQueueService.cs" />
<Compile Include="Services\IMessageService.cs" />
<Compile Include="Services\NullMessageChannelSelector.cs" />
<Compile Include="ViewModels\MessagesFilter.cs" />
<Compile Include="Drivers\MessageSettingsPartDriver.cs" />
<Compile Include="Handlers\MessageSettingsPartHandler.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Services\MessageQueueProcessor.cs" />
<Compile Include="Services\MessageQueueBackgroundTask.cs" />
<Compile Include="Services\IMessageChannel.cs" />
<Compile Include="Models\MessageQueueStatus.cs" />
<Compile Include="Models\QueuedMessageRecord.cs" />
<Compile Include="Models\MessageSettingsPart.cs" />
<Compile Include="Models\QueuedMessageStatus.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\MessageQueueService.cs" />
<Compile Include="ViewModels\MessageSettingsPartViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Module.txt" />
<Content Include="Placement.info" />
</ItemGroup>
<ItemGroup>
<Content Include="Styles\admin-messaging.min.css">
<DependentUpon>admin-messaging.css</DependentUpon>
</Content>
<Content Include="Styles\Images\queue-controls.png" />
<Content Include="Styles\Images\message-status.png" />
<Content Include="Styles\admin-messaging.css" />
<Content Include="Styles\menu.messaging-admin.css" />
<Content Include="Styles\Images\menu.messaging.png" />
<Content Include="Styles\workflows-activity-sendmessage.css" />
<Content Include="Views\EditorTemplates\Parts\MessageSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
<Name>Orchard.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
<Project>{9916839c-39fc-4ceb-a5af-89ca7e87119f}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Forms\Orchard.Forms.csproj">
<Project>{642a49d7-8752-4177-80d6-bfbbcfad3de0}</Project>
<Name>Orchard.Forms</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.TaskLease\Orchard.TaskLease.csproj">
<Project>{3F72A4E9-7B72-4260-B010-C16EC54F9BAF}</Project>
<Name>Orchard.TaskLease</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Workflows\Orchard.Workflows.csproj">
<Project>{7059493c-8251-4764-9c1e-2368b8b485bc}</Project>
<Name>Orchard.Workflows</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\MessageQueueViewModel.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\List.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Details.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Styles\Web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\Web.config" />
</ItemGroup>
<ItemGroup />
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>29609</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>
</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}</ProjectGuid>
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.JobsQueue</RootNamespace>
<AssemblyName>Orchard.JobsQueue</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileUpgradeFlags>
</FileUpgradeFlags>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>4.0</OldToolsVersion>
<TargetFrameworkProfile />
<UseIISExpress>false</UseIISExpress>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Web" />
<Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.XML" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="Services\IJobsQueueProcessor.cs" />
<Compile Include="Services\IJobsQueueManager.cs" />
<Compile Include="Services\IJobsQueueService.cs" />
<Compile Include="Drivers\JobsQueueSettingsPartDriver.cs" />
<Compile Include="Handlers\JobsQueueSettingsPartHandler.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Services\JobsQueueProcessor.cs" />
<Compile Include="Services\JobsQueueBackgroundTask.cs" />
<Compile Include="Models\JobsQueueStatus.cs" />
<Compile Include="Models\QueuedJobRecord.cs" />
<Compile Include="Models\JobsQueueSettingsPart.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\JobsQueueManager.cs" />
<Compile Include="Services\JobsQueueService.cs" />
<Compile Include="ViewModels\QueuedJobsSettingsPartViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Module.txt" />
<Content Include="Placement.info" />
</ItemGroup>
<ItemGroup>
<Content Include="Styles\Images\menu.jobsqueue.png" />
<Content Include="Styles\Images\queue-controls.png" />
<Content Include="Styles\admin-jobsqueue.css" />
<Content Include="Styles\menu.jobsqueue-admin.css" />
<Content Include="Styles\workflows-activity-sendmessage.css" />
<Content Include="Views\EditorTemplates\Parts\JobsQueueSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
<Name>Orchard.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
<Project>{9916839c-39fc-4ceb-a5af-89ca7e87119f}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Forms\Orchard.Forms.csproj">
<Project>{642a49d7-8752-4177-80d6-bfbbcfad3de0}</Project>
<Name>Orchard.Forms</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.TaskLease\Orchard.TaskLease.csproj">
<Project>{3F72A4E9-7B72-4260-B010-C16EC54F9BAF}</Project>
<Name>Orchard.TaskLease</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Workflows\Orchard.Workflows.csproj">
<Project>{7059493c-8251-4764-9c1e-2368b8b485bc}</Project>
<Name>Orchard.Workflows</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\List.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Details.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Styles\Web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\Web.config" />
</ItemGroup>
<ItemGroup />
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>29609</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,3 +1,3 @@
<Placement>
<Place Parts_MessageSettings_Edit="Content:10"/>
<Placement>
<Place Parts_MessageSettings_Edit="Content:10"/>
</Placement>

View File

@@ -1,36 +1,36 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Orchard.Messaging")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Orchard")]
[assembly: AssemblyCopyright("Copyright © Outercurve Foundation 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c39c2970-b7a5-466b-8dcb-0fc571a7d8c7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.2")]
[assembly: AssemblyFileVersion("1.7.2")]
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Orchard.JobsQueue")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Orchard")]
[assembly: AssemblyCopyright("Copyright © Outercurve Foundation 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c39c2970-b7a5-466b-8dcb-0fc571a7d8c7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.2")]
[assembly: AssemblyFileVersion("1.7.2")]

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Orchard.JobsQueue.Models;
namespace Orchard.JobsQueue.Services {
public interface IJobsQueueManager : IDependency {
QueuedJobRecord GetJob(int id);
void Delete(QueuedJobRecord job);
IEnumerable<QueuedJobRecord> GetJobs(int startIndex, int count);
int GetJobsCount();
void Resume();
void Pause();
}
}

View File

@@ -0,0 +1,5 @@
namespace Orchard.JobsQueue.Services {
public interface IJobsQueueProcessor : ISingletonDependency {
void ProcessQueue();
}
}

View File

@@ -0,0 +1,8 @@
using Orchard.Events;
using Orchard.JobsQueue.Models;
namespace Orchard.JobsQueue.Services {
public interface IJobsQueueService : IEventHandler {
QueuedJobRecord Enqueue(string message, object parameters, int priority);
}
}

View File

@@ -0,0 +1,14 @@
using Orchard.Tasks;
namespace Orchard.JobsQueue.Services {
public class JobsQueueBackgroundTask : Component, IBackgroundTask {
private readonly IJobsQueueProcessor _jobsQueueProcessor;
public JobsQueueBackgroundTask(IJobsQueueProcessor jobsQueueProcessor) {
_jobsQueueProcessor = jobsQueueProcessor;
}
public void Sweep() {
_jobsQueueProcessor.ProcessQueue();
}
}
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.JobsQueue.Models;
using Orchard.Settings;
namespace Orchard.JobsQueue.Services {
public class JobsQueueManager : IJobsQueueManager {
private readonly IRepository<QueuedJobRecord> _jobRepository;
private readonly JobsQueueSettingsPart _jobsQueueSettingsPart;
public JobsQueueManager(
IRepository<QueuedJobRecord> jobRepository,
ISiteService siteService) {
_jobRepository = jobRepository;
_jobsQueueSettingsPart = siteService.GetSiteSettings().As<JobsQueueSettingsPart>();
}
public void Resume() {
_jobsQueueSettingsPart.Status = JobsQueueStatus.Idle;
}
public void Pause() {
_jobsQueueSettingsPart.Status = JobsQueueStatus.Paused;
}
public int GetJobsCount() {
return GetMessagesQuery().Count();
}
public IEnumerable<QueuedJobRecord> GetJobs(int startIndex, int pageSize) {
return GetMessagesQuery()
.Skip(startIndex)
.Take(pageSize)
.ToList();
}
public QueuedJobRecord GetJob(int id) {
return _jobRepository.Get(id);
}
private IQueryable<QueuedJobRecord> GetMessagesQuery() {
var query = _jobRepository
.Table
.OrderByDescending(x => x.Priority)
.ThenByDescending(x => x.CreatedUtc);
return query;
}
public void Delete(QueuedJobRecord job) {
_jobRepository.Delete(job);
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Newtonsoft.Json.Linq;
using Orchard.Environment;
using Orchard.Events;
using Orchard.Logging;
using Orchard.JobsQueue.Models;
using Orchard.Services;
using Orchard.TaskLease.Services;
namespace Orchard.JobsQueue.Services {
public class JobsQueueProcessor : IJobsQueueProcessor {
private readonly Work<IJobsQueueManager> _jobsQueueManager;
private readonly Work<IClock> _clock;
private readonly Work<ITaskLeaseService> _taskLeaseService;
private readonly IEventBus _eventBus;
private readonly ReaderWriterLockSlim _rwl = new ReaderWriterLockSlim();
public JobsQueueProcessor(
Work<IClock> clock,
Work<IJobsQueueManager> jobsQueueManager,
Work<ITaskLeaseService> taskLeaseService,
IEventBus eventBus) {
_clock = clock;
_jobsQueueManager = jobsQueueManager;
_taskLeaseService = taskLeaseService;
_eventBus = eventBus;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public void ProcessQueue() {
// prevent two threads on the same machine to process the message queue
if (_rwl.TryEnterWriteLock(0)) {
try {
_taskLeaseService.Value.Acquire("JobsQueueProcessor", _clock.Value.UtcNow.AddMinutes(5));
IEnumerable<QueuedJobRecord> messages;
while ((messages = _jobsQueueManager.Value.GetJobs(0, 10).ToArray()).Any()) {
foreach (var message in messages.AsParallel()) {
ProcessMessage(message);
}
}
}
finally {
_rwl.ExitWriteLock();
}
}
}
private void ProcessMessage(QueuedJobRecord job) {
Logger.Debug("Processing job {0}.", job.Id);
try {
var payload = JObject.Parse(job.Parameters);
var parameters = payload.ToDictionary();
_eventBus.Notify(job.Message, parameters);
Logger.Debug("Processed job Id {0}.", job.Id);
}
catch (Exception e) {
Logger.Error(e, "An unexpected error while processing job {0}. Error message: {1}.", job.Id, e);
}
finally {
_jobsQueueManager.Value.Delete(job);
}
}
}
public static class JObjectExtensions {
public static IDictionary<string, object> ToDictionary(this JObject jObject) {
return (IDictionary<string, object>)Convert(jObject);
}
private static object Convert(this JToken jToken) {
if (jToken == null) {
throw new ArgumentNullException();
}
switch (jToken.Type) {
case JTokenType.Array:
var array = jToken as JArray;
return array.Values().Select(Convert).ToArray();
case JTokenType.Object:
var obj = jToken as JObject;
return obj
.Properties()
.ToDictionary(property => property.Name, property => Convert(property.Value));
default:
return jToken.ToObject<object>();
}
}
}
}

View File

@@ -0,0 +1,31 @@
using Newtonsoft.Json;
using Orchard.Data;
using Orchard.JobsQueue.Models;
using Orchard.Services;
namespace Orchard.JobsQueue.Services {
public class JobsQueueService : IJobsQueueService {
private readonly IClock _clock;
private readonly IRepository<QueuedJobRecord> _messageRepository;
public JobsQueueService(
IClock clock,
IRepository<QueuedJobRecord> messageRepository) {
_clock = clock;
_messageRepository = messageRepository;
}
public QueuedJobRecord Enqueue(string message, object parameters, int priority) {
var queuedJob = new QueuedJobRecord {
Parameters = JsonConvert.SerializeObject(parameters),
Message = message,
CreatedUtc = _clock.UtcNow,
};
_messageRepository.Create(queuedJob);
return queuedJob;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,27 +1,4 @@
.message-status {
background: url('./images/message-status.png') no-repeat -17px -17px;
padding-left: 25px;
line-height: 16px;
vertical-align: middle;
}
.message-status.pending {
background-position: -17px -17px;
}
.message-status.sending {
background-position: -17px -67px;
}
.message-status.sent {
background-position: -17px -116px;
}
.message-status.faulted {
background-position: -17px -166px;
}
fieldset.summary ul li:after {
fieldset.summary ul li:after {
content: ".";
display: block;
height: 0;

View File

@@ -0,0 +1,7 @@
.navicon-jobs-queue {
background-image: url(./images/menu.jobsqueue.png) !important;
}
.navicon-jobs-queue:hover {
background-position: 0 -30px !important;
}

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
using Autofac;
using Moq;
using NUnit.Framework;
using Orchard.JobsQueue.Models;
using Orchard.JobsQueue.Services;
using Orchard.Messaging.Models;
using Orchard.Messaging.Services;
using Orchard.Tests;
@@ -10,22 +12,22 @@ using Orchard.Tests;
namespace Orchard.Messaging.Tests {
[TestFixture]
public class MessageQueueProcessorTests : DatabaseEnabledTestsBase {
private List<QueuedMessageRecord> _messages;
private List<QueuedJobRecord> _messages;
protected override IEnumerable<Type> DatabaseTypes {
get {
yield return typeof(QueuedMessageRecord);
yield return typeof(QueuedJobRecord);
}
}
public override void Register(ContainerBuilder builder) {
var messageManagerMock = new Mock<IMessageQueueService>();
var messageManagerMock = new Mock<IJobsQueueService>();
builder.RegisterInstance(messageManagerMock.Object);
builder.RegisterType<MessageQueueProcessor>().As<IMessageQueueProcessor>();
builder.RegisterType<JobsQueueProcessor>().As<IJobsQueueProcessor>();
builder.RegisterType<StubMessageChannel>().As<IMessageChannel>();
_messages = new List<QueuedMessageRecord> {
_messages = new List<QueuedJobRecord> {
CreateMessage("Message 1"),
CreateMessage("Message 2")
};
@@ -33,7 +35,7 @@ namespace Orchard.Messaging.Tests {
messageManagerMock
.Setup(x => x.Enqueue(It.IsAny<string>(), It.IsAny<string>(), 0))
.Callback(() => _clock.Advance(TimeSpan.FromSeconds(1)))
.Returns(new QueuedMessageRecord ());
.Returns(new QueuedJobRecord ());
//messageManagerMock.Setup(x => x.EnterProcessingStatus()).Callback(() => {
// queue.Record.Status = MessageQueueStatus.Processing;
// queue.Record.StartedUtc = _clock.UtcNow;
@@ -42,17 +44,14 @@ namespace Orchard.Messaging.Tests {
[Test]
public void ProcessingQueueWithEnoughTimeSendsAllMessages() {
var processor = _container.Resolve<IMessageQueueProcessor>();
var processor = _container.Resolve<IJobsQueueProcessor>();
processor.ProcessQueue();
foreach (var message in _messages) {
Assert.That(message.Status, Is.EqualTo(QueuedMessageStatus.Sent));
}
}
private QueuedMessageRecord CreateMessage(string subject) {
return new QueuedMessageRecord {Id = 1, Type = "Email", Payload = "some payload data"};
private QueuedJobRecord CreateMessage(string subject) {
return new QueuedJobRecord {Id = 1, Message = "Email", Parameters = "some payload data"};
}
}
}

View File

@@ -2,6 +2,8 @@
using System.Collections.Generic;
using Autofac;
using NUnit.Framework;
using Orchard.JobsQueue.Models;
using Orchard.JobsQueue.Services;
using Orchard.Messaging.Models;
using Orchard.Messaging.Services;
using Orchard.Tests;
@@ -11,12 +13,12 @@ namespace Orchard.Messaging.Tests {
public class MessageQueueTests : DatabaseEnabledTestsBase {
protected override IEnumerable<Type> DatabaseTypes {
get {
yield return typeof(QueuedMessageRecord);
yield return typeof(QueuedJobRecord);
}
}
public override void Register(ContainerBuilder builder) {
builder.RegisterType<MessageQueueService>().As<IMessageQueueService>();
builder.RegisterType<JobsQueueService>().As<IJobsQueueService>();
}
}

View File

@@ -42,6 +42,10 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\..\lib\moq\Moq.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>..\..\..\..\..\lib\nunit\nunit.framework.dll</HintPath>
</Reference>
@@ -81,9 +85,9 @@
<Project>{6F759635-13D7-4E94-BCC9-80445D63F117}</Project>
<Name>Orchard.Tokens</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Messaging.csproj">
<ProjectReference Include="..\Orchard.JobsQueue.csproj">
<Project>{085948ff-0e9b-4a9a-b564-f8b8b4bdddbc}</Project>
<Name>Orchard.Messaging</Name>
<Name>Orchard.JobsQueue</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Orchard.Messaging.Services;
using Orchard.Tests.Stubs;
@@ -16,7 +17,7 @@ namespace Orchard.Messaging.Tests {
public void Dispose() {
}
public void Process(string payload) {
public void Process(IDictionary<string ,object> parameters) {
_clock.Advance(_simulatedProcessingTime);
}
}

View File

@@ -0,0 +1,7 @@
using Orchard.JobsQueue.Models;
namespace Orchard.JobsQueue.ViewModels {
public class JobsQueueSettingsPartViewModel {
public JobsQueueSettingsPart JobsQueueSettings { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
@using Orchard.JobsQueue.Models
@{
Style.Include("admin-jobsqueue.css");
Layout.Title = T("Job Details");
var message = (QueuedJobRecord)Model.Job;
var returnUrl = (string)Model.ReturnUrl;
}
<fieldset class="summary">
<ul>
<li>
<label>@T("Priority")</label>
<span>@message.Priority</span>
</li>
<li>
<label>@T("Created")</label>
<span>@message.CreatedUtc</span>
</li>
<li>
<label>@T("Payload")</label>
<span>@message.Parameters</span>
</li>
</ul>
</fieldset>
<a href="@returnUrl" class="button">@T("Back")</a>

View File

@@ -0,0 +1,65 @@
@using Orchard.Localization
@using Orchard.JobsQueue.Models
@using Orchard.Utility.Extensions
@{
IEnumerable<QueuedJobRecord> jobs = Model.Jobs;
JobsQueueStatus status = Model.JobsQueueStatus;
Layout.Title = T("Jobs Queue");
Style.Include("admin-jobsqueue.css");
}
@using (Html.BeginFormAntiForgeryPost()) {
<div class="manage">
<span class="queue-status @String.Format("{0}", status.ToString().HtmlClassify())">@T("Status: {0}", status)</span>
@if (status == JobsQueueStatus.Paused) {
<button type="submit" name="submit.Resume" value="resume" class="button">@T("Resume")</button>
}
else {
<button type="submit" name="submit.Pause" value="pause" class="button grey">@T("Pause")</button>
<button type="submit" name="submit.Process" value="process">@T("Process Now")</button>
}
</div>
if (!jobs.Any()) {
<div class="job info">@T("The queue is empty.")</div>
}
else {
<table class="items">
<thead>
<tr>
<th>@T("Priority")</th>
<th>@T("Type")</th>
<th>@T("Created")</th>
<th>@T("Actions")</th>
</tr>
</thead>
<tbody>
@foreach (var job in jobs) {
LocalizedString priorityName;
switch (job.Priority) {
case -50:
priorityName = T("Low");
break;
case 0:
priorityName = T("Normal");
break;
case 50:
priorityName = T("High");
break;
default:
priorityName = T("None");
break;
}
<tr>
<td>@priorityName</td>
<td>@job.Message</td>
<td>@Display.DateTimeRelative(dateTimeUtc: job.CreatedUtc)</td>
<td>
<a href="@Url.Action("Details", "Admin", new { job.Id, returnUrl = Request.Url.PathAndQuery })">@T("Details")</a>
</td>
</tr>
}
</tbody>
</table>
}
@Display(Model.Pager)
}

View File

@@ -1,9 +1,8 @@
@model MessageSettingsPartViewModel
@using Orchard.Messaging.ViewModels;
@*
<fieldset>
<legend>@T("Messaging")</legend>
<div>
</div>
</fieldset>
@model Orchard.JobsQueue.ViewModels.JobsQueueSettingsPartViewModel
@*
<fieldset>
<legend>@T("Messaging")</legend>
<div>
</div>
</fieldset>
*@

View File

@@ -1,37 +1,37 @@
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host"/>
<remove name="pages"/>
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false"/>
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false"/>
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc"/>
<add namespace="System.Web.Mvc.Ajax"/>
<add namespace="System.Web.Mvc.Html"/>
<add namespace="System.Web.Routing"/>
<add namespace="System.Web.WebPages"/>
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.5">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblies>
</compilation>
</system.web>
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host"/>
<remove name="pages"/>
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false"/>
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false"/>
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc"/>
<add namespace="System.Web.Mvc.Ajax"/>
<add namespace="System.Web.Mvc.Html"/>
<add namespace="System.Web.Routing"/>
<add namespace="System.Web.WebPages"/>
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.5">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblies>
</compilation>
</system.web>
</configuration>

View File

@@ -1,90 +0,0 @@
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Messaging.Models;
using Orchard.Messaging.Services;
using Orchard.Messaging.ViewModels;
using Orchard.Mvc;
using Orchard.UI.Admin;
using Orchard.UI.Navigation;
using Orchard.UI.Notify;
namespace Orchard.Messaging.Controllers {
[Admin]
public class AdminController : Controller {
private readonly IMessageQueueService _messageQueueManager;
private readonly IOrchardServices _services;
private readonly IMessageQueueProcessor _messageQueueProcessor;
public AdminController(
IMessageQueueService messageQueueManager,
IShapeFactory shapeFactory,
IOrchardServices services,
IMessageQueueProcessor messageQueueProcessor) {
_messageQueueManager = messageQueueManager;
_services = services;
_messageQueueProcessor = messageQueueProcessor;
New = shapeFactory;
T = NullLocalizer.Instance;
}
public dynamic New { get; set; }
public Localizer T { get; set; }
public ActionResult Details(int id, string returnUrl) {
var message = _messageQueueManager.GetMessage(id);
if (!Url.IsLocalUrl(returnUrl))
returnUrl = Url.Action("List");
var model = New.ViewModel().Message(message).ReturnUrl(returnUrl);
return View(model);
}
public ActionResult List(MessagesFilter filter, PagerParameters pagerParameters) {
var pager = new Pager(_services.WorkContext.CurrentSite, pagerParameters);
var messageCount = _messageQueueManager.GetMessagesCount(filter.Status);
var messages = _messageQueueManager.GetMessages(filter.Status, pager.GetStartIndex(), pager.PageSize).ToList();
var model = _services.New.ViewModel()
.Pager(_services.New.Pager(pager).TotalItemCount(messageCount))
.MessageQueueStatus(_services.WorkContext.CurrentSite.As<MessageSettingsPart>().Status)
.Messages(messages)
.Filter(filter);
return View(model);
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Filter")]
public ActionResult Filter(QueuedMessageStatus? status) {
return RedirectToAction("List", new { status });
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Resume")]
public ActionResult Resume(QueuedMessageStatus? status) {
_messageQueueManager.Resume();
_services.Notifier.Information(T("The queue has been resumed."));
return RedirectToAction("List", new { status });
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Pause")]
public ActionResult Pause(QueuedMessageStatus? status) {
_messageQueueManager.Pause();
_services.Notifier.Information(T("The queue has been paused."));
return RedirectToAction("List", new { status });
}
[HttpPost, ActionName("List")]
[FormValueRequired("submit.Process")]
public ActionResult Process(QueuedMessageStatus? status) {
_messageQueueProcessor.ProcessQueue();
_services.Notifier.Information(T("Processing has started."));
return RedirectToAction("List", new { status });
}
}
}

View File

@@ -1,42 +0,0 @@
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Localization;
using Orchard.Messaging.Models;
using Orchard.Messaging.ViewModels;
namespace Orchard.Messaging.Drivers {
[UsedImplicitly]
public class MessageSettingsPartDriver : ContentPartDriver<MessageSettingsPart> {
private const string TemplateName = "Parts/MessageSettings";
public IOrchardServices Services { get; set; }
public MessageSettingsPartDriver(IOrchardServices services) {
Services = services;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override string Prefix { get { return "MessageSettings"; } }
protected override DriverResult Editor(MessageSettingsPart part, dynamic shapeHelper) {
var model = new MessageSettingsPartViewModel {
MessageSettings = part
};
return ContentShape("Parts_MessageSettings_Edit", () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
protected override DriverResult Editor(MessageSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
var model = new MessageSettingsPartViewModel {
MessageSettings = part
};
updater.TryUpdateModel(model, Prefix, null, null);
return ContentShape("Parts_MessageSettings_Edit", () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
}
}
}

View File

@@ -1,12 +0,0 @@
using JetBrains.Annotations;
using Orchard.ContentManagement.Handlers;
using Orchard.Messaging.Models;
namespace Orchard.Messaging.Handlers {
[UsedImplicitly]
public class MessageSettingsPartHandler : ContentHandler {
public MessageSettingsPartHandler() {
Filters.Add(new ActivatingFilter<MessageSettingsPart>("Site"));
}
}
}

View File

@@ -1,23 +0,0 @@
using System;
using Orchard.Data.Migration;
namespace Orchard.Messaging {
public class Migrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("QueuedMessageRecord", table => table
.Column<int>("Id", c => c.Identity().PrimaryKey())
.Column<string>("Type", c => c.WithLength(64))
.Column<int>("Priority", c => c.WithDefault(0))
.Column<string>("Payload", c => c.Unlimited())
.Column<string>("Status", c => c.WithLength(64))
.Column<string>("Result", c => c.Unlimited())
.Column<DateTime>("CreatedUtc")
.Column<DateTime>("StartedUtc")
.Column<DateTime>("CompletedUtc")
);
return 1;
}
}
}

View File

@@ -1,6 +0,0 @@
namespace Orchard.Messaging.Models {
public enum MessageQueueStatus {
Idle,
Paused
}
}

View File

@@ -1,21 +0,0 @@
using System;
using Orchard.Data.Conventions;
namespace Orchard.Messaging.Models {
public class QueuedMessageRecord {
public virtual int Id { get; set; }
public virtual int Priority { get; set; }
public virtual string Type { get; set; }
[StringLengthMax]
public virtual string Payload { get; set; }
public virtual QueuedMessageStatus Status { get; set; }
public virtual DateTime CreatedUtc { get; set; }
public virtual DateTime? StartedUtc { get; set; }
public virtual DateTime? CompletedUtc { get; set; }
[StringLengthMax]
public virtual string Result { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace Orchard.Messaging.Models {
public enum QueuedMessageStatus {
Pending,
Sending,
Sent,
Faulted
}
}

View File

@@ -1,12 +0,0 @@
Name: Messaging
AntiForgery: enabled
Author: The Orchard Team
Website: http://orchardproject.net
Version: 1.7.2
OrchardVersion: 1.7.2
Description: This module provides the messaging infrastructure that modules can use to send messages.
Category: Messaging
Features:
Orchard.Messaging:
Description: Provides the messaging infrastructure that modules can use to send messages.
Dependencies: Settings, Orchard.TaskLease

View File

@@ -1,28 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Orchard.Logging;
namespace Orchard.Messaging.Services {
public class DefaultMessageService : Component, IMessageService {
private readonly IEnumerable<IMessageChannelSelector> _messageChannelSelectors;
public DefaultMessageService(IEnumerable<IMessageChannelSelector> messageChannelSelectors) {
_messageChannelSelectors = messageChannelSelectors;
}
public void Send(string type, string payload) {
var messageChannelResult = _messageChannelSelectors
.Select(x => x.GetChannel(type, payload))
.Where(x => x != null)
.OrderByDescending(x => x.Priority)
.FirstOrDefault();
if (messageChannelResult == null || messageChannelResult.MessageChannel == null) {
Logger.Information("No channels where found to process a message of type {0}", type);
return;
}
messageChannelResult.MessageChannel.Process(payload);
}
}
}

View File

@@ -1,5 +0,0 @@
namespace Orchard.Messaging.Services {
public interface IMessageQueueProcessor : ISingletonDependency {
void ProcessQueue();
}
}

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using Orchard.Messaging.Models;
namespace Orchard.Messaging.Services {
public interface IMessageQueueService : IDependency {
QueuedMessageRecord Enqueue(string type, object payload, int priority);
QueuedMessageRecord GetMessage(int id);
IEnumerable<QueuedMessageRecord> GetMessages(QueuedMessageStatus? status, int startIndex, int count);
int GetMessagesCount(QueuedMessageStatus? status = null);
void Resume();
void Pause();
}
}

View File

@@ -1,5 +0,0 @@
namespace Orchard.Messaging.Services {
public interface IMessageService : IDependency {
void Send(string type, string payload);
}
}

View File

@@ -1,14 +0,0 @@
using Orchard.Tasks;
namespace Orchard.Messaging.Services {
public class MessageQueueBackgroundTask : Component, IBackgroundTask {
private readonly IMessageQueueProcessor _messageQueueProcessor;
public MessageQueueBackgroundTask(IMessageQueueProcessor messageQueueProcessor) {
_messageQueueProcessor = messageQueueProcessor;
}
public void Sweep() {
_messageQueueProcessor.ProcessQueue();
}
}
}

View File

@@ -1,73 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Orchard.Environment;
using Orchard.Logging;
using Orchard.Messaging.Models;
using Orchard.Services;
using Orchard.TaskLease.Services;
namespace Orchard.Messaging.Services {
public class MessageQueueProcessor : IMessageQueueProcessor {
private readonly Work<IMessageQueueService> _messageQueueService;
private readonly Work<IMessageService> _messageService;
private readonly Work<IClock> _clock;
private readonly Work<ITaskLeaseService> _taskLeaseService;
private readonly ReaderWriterLockSlim _rwl = new ReaderWriterLockSlim();
public MessageQueueProcessor(
Work<IMessageQueueService> messageQueueService,
Work<IMessageService> messageService,
Work<IClock> clock,
Work<ITaskLeaseService> taskLeaseService) {
_messageQueueService = messageQueueService;
_messageService = messageService;
_clock = clock;
_taskLeaseService = taskLeaseService;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public void ProcessQueue() {
// prevent two threads on the same machine to process the message queue
if (_rwl.TryEnterWriteLock(0)) {
try {
_taskLeaseService.Value.Acquire("MessageQueueProcessor", _clock.Value.UtcNow.AddMinutes(5));
IEnumerable<QueuedMessageRecord> messages;
while ((messages = _messageQueueService.Value.GetMessages(QueuedMessageStatus.Pending, 0, 10).ToArray()).Any()) {
foreach (var message in messages.AsParallel()) {
ProcessMessage(message);
}
}
}
finally {
_rwl.ExitWriteLock();
}
}
}
private void ProcessMessage(QueuedMessageRecord message) {
message.StartedUtc = _clock.Value.UtcNow;
message.Status = QueuedMessageStatus.Sending;
Logger.Debug("Sending message ID {0}.", message.Id);
try {
_messageService.Value.Send(message.Type, message.Payload);
message.Status = QueuedMessageStatus.Sent;
Logger.Debug("Sent message ID {0}.", message.Id);
}
catch (Exception e) {
message.Status = QueuedMessageStatus.Faulted;
message.Result = e.ToString();
Logger.Error(e, "An unexpected error while sending message {0}. Error message: {1}.", message.Id, e);
}
finally {
message.CompletedUtc = _clock.Value.UtcNow;
}
}
}
}

View File

@@ -1,80 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Messaging.Models;
using Orchard.Services;
using Orchard.Settings;
namespace Orchard.Messaging.Services {
public class MessageQueueService : IMessageQueueService {
private readonly IClock _clock;
private readonly IRepository<QueuedMessageRecord> _messageRepository;
private readonly MessageSettingsPart _messageSettingsPart;
public MessageQueueService(
IClock clock,
IRepository<QueuedMessageRecord> messageRepository,
ISiteService siteService) {
_clock = clock;
_messageRepository = messageRepository;
_messageSettingsPart = siteService.GetSiteSettings().As<MessageSettingsPart>();
}
public QueuedMessageRecord Enqueue(string channelName, object payload, int priority) {
var queuedMessage = new QueuedMessageRecord {
Payload = ToJson(payload),
Type = channelName,
CreatedUtc = _clock.UtcNow,
Status = QueuedMessageStatus.Pending
};
_messageRepository.Create(queuedMessage);
return queuedMessage;
}
public void Resume() {
_messageSettingsPart.Status = MessageQueueStatus.Idle;
}
public void Pause() {
_messageSettingsPart.Status = MessageQueueStatus.Paused;
}
public int GetMessagesCount(QueuedMessageStatus? status = null) {
return GetMessagesQuery(status).Count();
}
public IEnumerable<QueuedMessageRecord> GetMessages(QueuedMessageStatus? status, int startIndex, int pageSize) {
return GetMessagesQuery(status)
.Skip(startIndex)
.Take(pageSize)
.ToList();
}
public QueuedMessageRecord GetMessage(int id) {
return _messageRepository.Get(id);
}
private IQueryable<QueuedMessageRecord> GetMessagesQuery(QueuedMessageStatus? status) {
var query = _messageRepository.Table;
if (status != null) {
query = query.Where(x => x.Status == status.Value);
}
query = query
.OrderByDescending(x => x.Priority)
.ThenByDescending(x => x.CreatedUtc);
return query;
}
private static string ToJson(object value) {
return value != null ? JsonConvert.SerializeObject(value) : null;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1 +0,0 @@
.message-status{background:url('./images/message-status.png') no-repeat -17px -17px;padding-left:25px;line-height:16px;vertical-align:middle}.message-status.pending{background-position:-17px -17px}.message-status.sending{background-position:-17px -67px}.message-status.sent{background-position:-17px -116px}.message-status.faulted{background-position:-17px -166px}fieldset.summary ul li:after{content:".";display:block;height:0;clear:both;visibility:hidden}fieldset.summary ul li label,fieldset.summary ul li span{float:left;display:block}fieldset.summary ul li label{width:200px;font-weight:bold}fieldset.summary ul li label:after{content:":"}span.queue-status{margin-right:1.5em}table.queues{clear:both}table.queues td{vertical-align:middle}ul.horizontal li{float:left}.clear{content:".";display:block;height:0;clear:both;visibility:hidden}

View File

@@ -1,7 +0,0 @@
.navicon-message-queue {
background-image: url(./images/menu.messaging.png) !important;
}
.navicon-message-queue:hover {
background-position: 0 -30px !important;
}

View File

@@ -1,7 +0,0 @@
using Orchard.Messaging.Models;
namespace Orchard.Messaging.ViewModels {
public class MessageSettingsPartViewModel {
public MessageSettingsPart MessageSettings { get; set; }
}
}

View File

@@ -1,7 +0,0 @@
using Orchard.Messaging.Models;
namespace Orchard.Messaging.ViewModels {
public class MessagesFilter {
public QueuedMessageStatus? Status { get; set; }
}
}

View File

@@ -1,39 +0,0 @@
@using Orchard.Messaging.Models
@using Orchard.Utility.Extensions
@{
Style.Include("admin-messaging.css", "admin-messaging.min.css");
Layout.Title = T("Message Details");
var message = (QueuedMessageRecord)Model.Message;
var returnUrl = (string)Model.ReturnUrl;
}
<fieldset class="summary">
<ul>
<li>
<label>@T("Status")</label>
<span class="message-status @message.Status.ToString().HtmlClassify()">@message.Status</span>
</li>
@if (message.Status == QueuedMessageStatus.Faulted) {
<li>
<label>@T("Error")</label>
<span>@message.Result</span>
</li>
}
<li>
<label>@T("Priority")</label>
<span>@message.Priority</span>
</li>
<li>
<label>@T("Created")</label>
<span>@message.CreatedUtc</span>
</li>
<li>
<label>@T("Completed")</label>
<span>@message.CompletedUtc</span>
</li>
<li>
<label>@T("Payload")</label>
<span>@message.Payload</span>
</li>
</ul>
</fieldset>
<a href="@returnUrl" class="button">@T("Back")</a>

View File

@@ -1,87 +0,0 @@
@using Orchard.Localization
@using Orchard.Messaging.Models
@using Orchard.Messaging.ViewModels
@using Orchard.Utility.Extensions
@{
IEnumerable<QueuedMessageRecord> messages = Model.Messages;
MessagesFilter filter = Model.Filter;
MessageQueueStatus status = Model.MessageQueueStatus;
Layout.Title = T("Message Queue");
Style.Include("admin-messaging.css", "admin-messaging.min.css");
}
@using (Html.BeginFormAntiForgeryPost()) {
<div class="manage">
<span class="queue-status @String.Format("{0}", status.ToString().HtmlClassify())">@T("Status: {0}", status)</span>
@if (status == MessageQueueStatus.Paused) {
<button type="submit" name="submit.Resume" value="resume" class="button">@T("Resume")</button>
}
else {
<button type="submit" name="submit.Pause" value="pause" class="button grey">@T("Pause")</button>
<button type="submit" name="submit.Process" value="process">@T("Process Now")</button>
}
</div>
<fieldset class="bulk-actions">
<label for="filterResults" class="bulk-filter">@T("Filter")</label>
<select id="filterResults" name="status">
@Html.SelectOption(filter.Status, default(QueuedMessageStatus?), T("All").ToString())
@Html.SelectOption(filter.Status, QueuedMessageStatus.Pending, T("Pending").ToString())
@Html.SelectOption(filter.Status, QueuedMessageStatus.Sending, T("Sending").ToString())
@Html.SelectOption(filter.Status, QueuedMessageStatus.Sent, T("Sent").ToString())
@Html.SelectOption(filter.Status, QueuedMessageStatus.Faulted, T("Faulted").ToString())
</select>
<button type="submit" name="submit.Filter" value="yes please">@T("Apply")</button>
</fieldset>
if (!messages.Any()) {
if (filter.Status == null) {
<div class="message info">@T("There no messages in this queue yet.")</div>
}
else {
<div class="message info">@T("There no messages in the '{0}' status.", filter.Status)</div>
}
}
else {
<table class="items">
<thead>
<tr>
<th>@T("Status")</th>
<th>@T("Priority")</th>
<th>@T("Type")</th>
<th>@T("Created")</th>
<th>@T("Completed")</th>
<th>@T("Actions")</th>
</tr>
</thead>
<tbody>
@foreach (var message in messages) {
LocalizedString priorityName;
switch (message.Priority) {
case -50:
priorityName = T("Low");
break;
case 0:
priorityName = T("Normal");
break;
case 50:
priorityName = T("High");
break;
default:
priorityName = T("None");
break;
}
<tr>
<td><span class="message-status @message.Status.ToString().HtmlClassify()">@message.Status</span></td>
<td>@priorityName</td>
<td>@message.Type</td>
<td>@Display.DateTimeRelative(dateTimeUtc: message.CreatedUtc)</td>
<td>@Display.DateTimeRelative(dateTimeUtc: message.CompletedUtc)</td>
<td>
<a href="@Url.Action("Details", "Admin", new { message.Id, returnUrl = Request.Url.PathAndQuery })">@T("Details")</a>
</td>
</tr>
}
</tbody>
</table>
}
@Display(Model.Pager)
}

View File

@@ -1,18 +0,0 @@
@model Orchard.Messaging.ViewModels.MessageQueueViewModel
<fieldset>
@Html.HiddenFor(m => m.Id)
<div>
@Html.LabelFor(m => m.Name)
@Html.TextBoxFor(m => m.Name, new { @class = "text medium" })
</div>
<div>
@Html.LabelFor(m => m.TimeSlice)
@Html.TextBoxFor(m => m.TimeSlice, new { @class = "text medium" })
<span class="hint">@T("The time this queue gets to process per cycle.")</span>
</div>
<div>
@Html.LabelFor(m => m.UpdateFrequency)
@Html.TextBoxFor(m => m.UpdateFrequency, new { @class = "text medium" })
<span class="hint">@T("The interval this queue is to be processed.")</span>
</div>
</fieldset>

View File

@@ -75,7 +75,7 @@ namespace Orchard.Setup.Services {
// Core
"Common", "Containers", "Contents", "Dashboard", "Feeds", "Navigation", "Reports", "Scheduling", "Settings", "Shapes", "Title",
// Modules
"Orchard.Pages", "Orchard.ContentPicker", "Orchard.Themes", "Orchard.TaskLease", "Orchard.Messaging", "Orchard.Users", "Orchard.Roles", "Orchard.Modules",
"Orchard.Pages", "Orchard.ContentPicker", "Orchard.Themes", "Orchard.Users", "Orchard.Roles", "Orchard.Modules",
"PackagingServices", "Orchard.Packaging", "Gallery", "Orchard.Recipes"
};

View File

@@ -9,4 +9,4 @@ Features:
Orchard.Users:
Description: Standard users.
Category: Core
Dependencies: Settings, Orchard.Messaging
Dependencies: Settings

View File

@@ -123,10 +123,6 @@
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Messaging\Orchard.Messaging.csproj">
<Project>{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}</Project>
<Name>Orchard.Messaging</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Views\Items\Content-User.Edit.cshtml" />

View File

@@ -4,7 +4,6 @@ using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Logging;
@@ -109,14 +108,15 @@ namespace Orchard.Users.Services {
var recipient = GetUser(userName);
if (recipient != null) {
var template = _shapeFactory.Create("Template_User_Moderated", Arguments.From(createUserParams));
template.Metadata.Wrappers.Add("Template_User_Wrapper");
var payload = new {
Subject = T("New account").Text,
Body = _shapeDisplay.Display(template),
Recipients = new [] { recipient.Email }
template.Metadata.Wrappers.Add("Template_User_Wrapper");
var parameters = new Dictionary<string, object> {
{"Subject", T("New account").Text},
{"Body", _shapeDisplay.Display(template)},
{"Recipients", new [] { recipient.Email }}
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
_messageService.Send("Email", parameters);
}
}
}

View File

@@ -1,5 +1,4 @@
using System.Linq;
using System.Collections.Generic;
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Messaging.Services;
@@ -10,9 +9,9 @@ using Orchard.Users.Models;
namespace Orchard.Users.Services {
public class MissingSettingsBanner : INotificationProvider {
private readonly IOrchardServices _orchardServices;
private readonly IMessageManager _messageManager;
private readonly IMessageChannelManager _messageManager;
public MissingSettingsBanner(IOrchardServices orchardServices, IMessageManager messageManager) {
public MissingSettingsBanner(IOrchardServices orchardServices, IMessageChannelManager messageManager) {
_orchardServices = orchardServices;
_messageManager = messageManager;
T = NullLocalizer.Instance;
@@ -28,7 +27,11 @@ namespace Orchard.Users.Services {
( registrationSettings.UsersMustValidateEmail ||
registrationSettings.NotifyModeration ||
registrationSettings.EnableLostPassword ) &&
!_messageManager.GetAvailableChannelServices().Contains("email") ) {
null == _messageManager.GetMessageChannel("Email", new Dictionary<string, object> {
{"Body", ""},
{"Subject", "Subject"},
{"Recipients", "john.doe@outlook.com"}
}) ) {
yield return new NotifyEntry { Message = T("Some Orchard.User settings require an Email channel to be enabled."), Type = NotifyType.Warning };
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Logging;
@@ -20,7 +20,7 @@ namespace Orchard.Users.Services {
[UsedImplicitly]
public class UserService : IUserService {
private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email
private static readonly TimeSpan DelayToResetPassword = new TimeSpan(1, 0, 0, 0); // 24 hours to validate email
private static readonly TimeSpan DelayToResetPassword = new TimeSpan(1, 0, 0, 0); // 24 hours to reset password
private readonly IContentManager _contentManager;
private readonly IMembershipService _membershipService;
@@ -140,15 +140,15 @@ namespace Orchard.Users.Services {
ContactEmail = site.As<RegistrationSettingsPart>().ValidateEmailContactEMail,
ChallengeUrl = url
}));
template.Metadata.Wrappers.Add("Template_User_Wrapper");
template.Metadata.Wrappers.Add("Template_User_Wrapper");
var parameters = new Dictionary<string, object> {
{"Subject", T("Verification E-Mail").Text},
{"Body", _shapeDisplay.Display(template)},
{"Recipients", user.Email}
};
var payload = new {
Subject = T("Verification E-Mail").Text,
Body = _shapeDisplay.Display(template),
Recipients = new[] { user.Email }
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
_messageService.Send("Email", parameters);
}
}
@@ -164,15 +164,15 @@ namespace Orchard.Users.Services {
User = user,
LostPasswordUrl = url
}));
template.Metadata.Wrappers.Add("Template_User_Wrapper");
template.Metadata.Wrappers.Add("Template_User_Wrapper");
var payload = new {
Subject = T("Lost password").Text,
Body = _shapeDisplay.Display(template),
Recipients = new[] { user.Email }
};
var parameters = new Dictionary<string, object> {
{"Subject", T("Lost password").Text},
{"Body", _shapeDisplay.Display(template)},
{"Recipients", user.Email }
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
_messageService.Send("Email", parameters);
return true;
}

View File

@@ -13,7 +13,13 @@ namespace Orchard.Workflows.Models {
return default(T);
}
return State.Value[key];
dynamic value = State.Value[key];
if (value == null) {
return default(T);
}
return value;
}
}
}

View File

@@ -68,24 +68,22 @@ namespace Upgrade.Controllers {
};
if (!newState.Body.StartsWith("<p ")) {
newState.Body =
"<p style=\"font-family:Arial, Helvetica; font-size:10pt;\">"
+ newState.Body
+ System.Environment.NewLine
+ "</p>";
newState.Body =
newState.Body
+ System.Environment.NewLine;
}
if (state.Recipient == "owner") {
newState.Recipients = new [] {"{User.Current.Email}"};
newState.Recipients = "{User.Current.Email}";
}
else if (state.Recipient == "author") {
newState.Recipients = new[] { "{Content.Author.Email}" };
newState.Recipients = "{Content.Author.Email}";
}
else if (state.Recipient == "admin") {
newState.Recipients = new[] { "{Site.SuperUser.Email}" };
newState.Recipients = "{Site.SuperUser.Email}";
}
else if (state.Recipient == "other") {
newState.Recipients = state.RecipientOther.Split(',');
newState.Recipients = state.RecipientOther;
}
record.State = JsonConvert.SerializeObject(newState);

View File

@@ -157,8 +157,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Lists", "Orchard.We
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Pages", "Orchard.Web\Modules\Orchard.Pages\Orchard.Pages.csproj", "{3420C92A-747F-4990-BA08-F2C9531E44AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Messaging", "Orchard.Web\Modules\Orchard.Messaging\Orchard.Messaging.csproj", "{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Localization", "Orchard.Web\Modules\Orchard.Localization\Orchard.Localization.csproj", "{FBC8B571-ED50-49D8-8D9D-64AB7454A0D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Scripting.Dlr", "Orchard.Web\Modules\Orchard.Scripting.Dlr\Orchard.Scripting.Dlr.csproj", "{2AD6973D-C7BB-416E-89FE-EEE34664E05F}"
@@ -235,7 +233,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Templates", "Orchar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Templates.Tests", "Orchard.Web\Modules\Orchard.Templates\Tests\Orchard.Templates.Tests\Orchard.Templates.Tests.csproj", "{8961613C-0ADD-4DF3-945B-CAE47987CAFD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Messaging.Tests", "Orchard.Web\Modules\Orchard.Messaging\Tests\Orchard.Messaging.Tests.csproj", "{2B4E039D-EDEB-43DD-8F5A-C640035906A9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.JobsQueue", "Orchard.Web\Modules\Orchard.JobsQueue\Orchard.JobsQueue.csproj", "{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -623,16 +621,6 @@ Global
{3420C92A-747F-4990-BA08-F2C9531E44AD}.FxCop|Any CPU.Build.0 = Release|Any CPU
{3420C92A-747F-4990-BA08-F2C9531E44AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3420C92A-747F-4990-BA08-F2C9531E44AD}.Release|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Coverage|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.FxCop|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Release|Any CPU.Build.0 = Release|Any CPU
{FBC8B571-ED50-49D8-8D9D-64AB7454A0D6}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{FBC8B571-ED50-49D8-8D9D-64AB7454A0D6}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{FBC8B571-ED50-49D8-8D9D-64AB7454A0D6}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
@@ -985,16 +973,16 @@ Global
{8961613C-0ADD-4DF3-945B-CAE47987CAFD}.FxCop|Any CPU.Build.0 = Release|Any CPU
{8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8961613C-0ADD-4DF3-945B-CAE47987CAFD}.Release|Any CPU.Build.0 = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Coverage|Any CPU.Build.0 = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.FxCop|Any CPU.Build.0 = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B4E039D-EDEB-43DD-8F5A-C640035906A9}.Release|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Coverage|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.FxCop|Any CPU.Build.0 = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1024,7 +1012,6 @@ Global
{C889167C-E52C-4A65-A419-224B3D1B957D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{137906EA-15FE-4AD8-A6A0-27528F0477D6} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{3420C92A-747F-4990-BA08-F2C9531E44AD} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{FBC8B571-ED50-49D8-8D9D-64AB7454A0D6} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{2AD6973D-C7BB-416E-89FE-EEE34664E05F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{99002B65-86F7-415E-BF4A-381AA8AB9CCC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
@@ -1056,6 +1043,7 @@ Global
{CBC7993C-57D8-4A6C-992C-19E849DFE71D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{36B82383-D69E-4897-A24A-648BABDF80EC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{10AB3CE2-A720-467F-9EC8-EBB4BAC9A1C9} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
@@ -1063,7 +1051,6 @@ Global
{E07AFA7E-7B36-44C3-A537-AFCCAA93EA7A} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{2969635F-D9C3-4D01-890D-437B46659690} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{8961613C-0ADD-4DF3-945B-CAE47987CAFD} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{2B4E039D-EDEB-43DD-8F5A-C640035906A9} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{5E5E7A21-C7B2-44D8-8593-2F9541AE041D} = {383DBA32-4A3E-48D1-AAC3-75377A694452}
{33B1BC8D-E292-4972-A363-22056B207156} = {383DBA32-4A3E-48D1-AAC3-75377A694452}
{0DFA2E10-96C8-4E05-BC10-B710B97ECCDE} = {383DBA32-4A3E-48D1-AAC3-75377A694452}

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
using Orchard.Caching;
using Orchard.DisplayManagement.Implementation;
using Orchard.Environment;
@@ -153,19 +154,21 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy {
// If the View is null, it means that the shape is being executed from a non-view origin / where no ViewContext was established by the view engine, but manually.
// Manually creating a ViewContext works when working with Shape methods, but not when the shape is implemented as a Razor view template.
// Horrible, but it will have to do for now.
result = RenderRazorViewToString(harvestShapeInfo.TemplateVirtualPath, displayContext.Value);
result = RenderRazorViewToString(harvestShapeInfo.TemplateVirtualPath, displayContext);
}
Logger.Information("Done rendering template file '{0}'", harvestShapeInfo.TemplateVirtualPath);
return result;
}
private IHtmlString RenderRazorViewToString(string path, object model) {
private IHtmlString RenderRazorViewToString(string path, DisplayContext context) {
using (var sw = new StringWriter()) {
var controllerContext = CreateControllerContext();
var viewResult = _viewEngine.Value.FindPartialView(controllerContext, path, false);
var viewContext = new ViewContext(controllerContext, viewResult.View, new ViewDataDictionary(model), new TempDataDictionary(), sw);
viewResult.View.Render(viewContext, sw);
context.ViewContext.ViewData = new ViewDataDictionary(context.Value);
context.ViewContext.TempData = new TempDataDictionary();
viewResult.View.Render(context.ViewContext, sw);
viewResult.ViewEngine.ReleaseView(controllerContext, viewResult.View);
return new HtmlString(sw.GetStringBuilder().ToString());
}
@@ -173,13 +176,15 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy {
private ControllerContext CreateControllerContext() {
var controller = new StubController();
var httpContext = _workContextAccessor.GetContext().HttpContext;
var routeData = httpContext.Request.RequestContext.RouteData;
var httpContext = _workContextAccessor.GetContext().Resolve<HttpContextBase>();
var requestContext = _workContextAccessor.GetContext().Resolve<RequestContext>();
var routeData = requestContext.RouteData;
if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller"))
routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", ""));
controller.ControllerContext = new ControllerContext(httpContext, routeData, controller);
controller.ControllerContext.RequestContext = requestContext;
return controller.ControllerContext;
}

View File

@@ -1,8 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Orchard.DisplayManagement.Implementation;
using Orchard.DisplayManagement.Shapes;
namespace Orchard.DisplayManagement {
@@ -11,43 +7,4 @@ namespace Orchard.DisplayManagement {
string Display(object shape);
IEnumerable<string> Display(IEnumerable<object> shapes);
}
public class ShapeDisplay : IShapeDisplay {
private readonly IDisplayHelperFactory _displayHelperFactory;
private readonly IWorkContextAccessor _workContextAccessor;
private readonly HttpContextBase _httpContextBase;
public ShapeDisplay(
IDisplayHelperFactory displayHelperFactory,
IWorkContextAccessor workContextAccessor,
HttpContextBase httpContextBase) {
_displayHelperFactory = displayHelperFactory;
_workContextAccessor = workContextAccessor;
_httpContextBase = httpContextBase;
}
public string Display(Shape shape) {
return Display((object) shape);
}
public string Display(object shape) {
var viewContext = new ViewContext { HttpContext = _httpContextBase };
viewContext.RouteData.DataTokens["IWorkContextAccessor"] = _workContextAccessor;
var display = _displayHelperFactory.CreateHelper(viewContext, new ViewDataContainer());
return ((DisplayHelper)display).ShapeExecute(shape).ToString();
}
public IEnumerable<string> Display(IEnumerable<object> shapes) {
return shapes.Select(Display).ToArray();
}
private class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
public ViewDataContainer() {
ViewData = new ViewDataDictionary();
}
}
}
}

View File

@@ -10,12 +10,14 @@ using Orchard.DisplayManagement.Descriptors;
using Orchard.DisplayManagement.Shapes;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Mvc;
namespace Orchard.DisplayManagement.Implementation {
public class DefaultDisplayManager : IDisplayManager {
private readonly Lazy<IShapeTableLocator> _shapeTableLocator;
private readonly IWorkContextAccessor _workContextAccessor;
private readonly IEnumerable<IShapeDisplayEvents> _shapeDisplayEvents;
private readonly IHttpContextAccessor _httpContextAccessor;
// this need to be Shape instead of IShape - cast to interface throws error on clr types like HtmlString
private static readonly CallSite<Func<CallSite, object, Shape>> _convertAsShapeCallsite = CallSite<Func<CallSite, object, Shape>>.Create(
@@ -28,10 +30,12 @@ namespace Orchard.DisplayManagement.Implementation {
public DefaultDisplayManager(
IWorkContextAccessor workContextAccessor,
IEnumerable<IShapeDisplayEvents> shapeDisplayEvents,
IHttpContextAccessor httpContextAccessor,
Lazy<IShapeTableLocator> shapeTableLocator) {
_shapeTableLocator = shapeTableLocator;
_workContextAccessor = workContextAccessor;
_shapeDisplayEvents = shapeDisplayEvents;
_httpContextAccessor = httpContextAccessor;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
}
@@ -52,8 +56,10 @@ namespace Orchard.DisplayManagement.Implementation {
if (shapeMetadata == null || string.IsNullOrEmpty(shapeMetadata.Type))
return CoerceHtmlString(context.Value);
var workContext = _workContextAccessor.GetContext(context.ViewContext);
var shapeTable = _shapeTableLocator.Value.Lookup(workContext.CurrentTheme.Id);
var workContext = _workContextAccessor.GetContext();
var shapeTable = _httpContextAccessor.Current() != null
? _shapeTableLocator.Value.Lookup(workContext.CurrentTheme.Id)
: _shapeTableLocator.Value.Lookup(null);
var displayingContext = new ShapeDisplayingContext {
Shape = shape,

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.DisplayManagement.Implementation;
using Orchard.DisplayManagement.Shapes;
namespace Orchard.DisplayManagement {
public class ShapeDisplay : IShapeDisplay {
private readonly IDisplayHelperFactory _displayHelperFactory;
private readonly IWorkContextAccessor _workContextAccessor;
private readonly HttpContextBase _httpContextBase;
private readonly RequestContext _requestContext;
public ShapeDisplay(
IDisplayHelperFactory displayHelperFactory,
IWorkContextAccessor workContextAccessor,
HttpContextBase httpContextBase,
RequestContext requestContext) {
_displayHelperFactory = displayHelperFactory;
_workContextAccessor = workContextAccessor;
_httpContextBase = httpContextBase;
_requestContext = requestContext;
}
public string Display(Shape shape) {
return Display((object) shape);
}
public string Display(object shape) {
var viewContext = new ViewContext {
HttpContext = _httpContextBase,
RequestContext = _requestContext
};
viewContext.RouteData.DataTokens["IWorkContextAccessor"] = _workContextAccessor;
var display = _displayHelperFactory.CreateHelper(viewContext, new ViewDataContainer());
return ((DisplayHelper)display).ShapeExecute(shape).ToString();
}
public IEnumerable<string> Display(IEnumerable<object> shapes) {
return shapes.Select(Display).ToArray();
}
private class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
public ViewDataContainer() {
ViewData = new ViewDataDictionary();
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Orchard.Logging;
namespace Orchard.Messaging.Services {
public class DefaultMessageService : Component, IMessageService {
private readonly IMessageChannelManager _messageChannelManager;
public DefaultMessageService(IMessageChannelManager messageChannelManager) {
_messageChannelManager = messageChannelManager;
}
public void Send(string type, IDictionary<string, object> parameters) {
var messageChannel = _messageChannelManager.GetMessageChannel(type, parameters);
if (messageChannel == null) {
Logger.Information("No channels where found to process a message of type {0}", type);
return;
}
messageChannel.Process(parameters);
}
}
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
namespace Orchard.Messaging.Services {
public interface IMessageChannel : IDependency {
void Process(string payload);
void Process(IDictionary<string, object> parameters);
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;
namespace Orchard.Messaging.Services {
public interface IMessageChannelManager : IDependency {
IMessageChannel GetMessageChannel(string type, IDictionary<string, object> parameters);
}
public class MessageChannelManager : IMessageChannelManager {
private readonly IEnumerable<IMessageChannelSelector> _messageChannelSelectors;
public MessageChannelManager(IEnumerable<IMessageChannelSelector> messageChannelSelectors) {
_messageChannelSelectors = messageChannelSelectors;
}
public IMessageChannel GetMessageChannel(string type, IDictionary<string, object> parameters) {
var messageChannelResult = _messageChannelSelectors
.Select(x => x.GetChannel(type, parameters))
.Where(x => x != null)
.OrderByDescending(x => x.Priority)
.FirstOrDefault();
return messageChannelResult == null ? null : messageChannelResult.MessageChannel();
}
}
}

View File

@@ -1,10 +1,12 @@
namespace Orchard.Messaging.Services {
using System;
namespace Orchard.Messaging.Services {
public interface IMessageChannelSelector : IDependency {
MessageChannelSelectorResult GetChannel(string messageType, object payload);
}
public class MessageChannelSelectorResult {
public int Priority { get; set; }
public IMessageChannel MessageChannel { get; set; }
public Func<IMessageChannel> MessageChannel { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
using Orchard.Events;
namespace Orchard.Messaging.Services {
public interface IMessageService : IEventHandler {
void Send(string type, IDictionary<string, object> parameters);
}
}

View File

@@ -1,7 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Web;
using System.Web.Instrumentation;
using System.Web.Mvc;
using System.Web.Routing;
using Autofac;
@@ -46,8 +49,9 @@ namespace Orchard.Mvc {
// which requires activating the Site content item, which in turn requires a UrlHelper, which in turn requires a RequestContext,
// thus preventing a StackOverflowException.
var baseUrl = new Func<string>(() => siteService.GetSiteSettings().BaseUrl);
return new HttpContextPlaceholder(baseUrl);
var httpContextBase = new HttpContextPlaceholder(baseUrl);
context.Resolve<IWorkContextAccessor>().CreateWorkContextScope(httpContextBase);
return httpContextBase;
}
static RequestContext RequestContextFactory(IComponentContext context) {
@@ -82,6 +86,7 @@ namespace Orchard.Mvc {
/// </summary>
class HttpContextPlaceholder : HttpContextBase {
private readonly Lazy<string> _baseUrl;
private readonly IDictionary _items = new Dictionary<object, object>();
public HttpContextPlaceholder(Func<string> baseUrl) {
_baseUrl = new Lazy<string>(baseUrl);
@@ -96,6 +101,14 @@ namespace Orchard.Mvc {
public override HttpResponseBase Response {
get { return new HttpResponsePlaceholder(); }
}
public override IDictionary Items {
get { return _items; }
}
public override PageInstrumentationService PageInstrumentation {
get { return new PageInstrumentationService(); }
}
}
private class HttpResponsePlaceholder : HttpResponseBase {
@@ -162,6 +175,10 @@ namespace Orchard.Mvc {
}
}
public override bool IsLocal {
get { return true; }
}
}
}
}

View File

@@ -221,6 +221,7 @@
<Compile Include="DisplayManagement\Implementation\IShapeFactoryEvents.cs" />
<Compile Include="DisplayManagement\INamedEnumerable.cs" />
<Compile Include="DisplayManagement\IShapeDisplay.cs" />
<Compile Include="DisplayManagement\ShapeDisplay.cs" />
<Compile Include="DisplayManagement\Shapes\Composite.cs" />
<Compile Include="DisplayManagement\Shapes\ShapeDebugView.cs" />
<Compile Include="DisplayManagement\Shapes\ITagBuilderFactory.cs" />
@@ -275,6 +276,12 @@
<Compile Include="Logging\OrchardLog4netFactory.cs" />
<Compile Include="Logging\OrchardLog4netLogger.cs" />
<Compile Include="Messaging\Services\DefaultMessageManager.cs" />
<Compile Include="Messaging\Services\DefaultMessageService.cs" />
<Compile Include="Messaging\Services\IMessageChannel.cs" />
<Compile Include="Messaging\Services\IMessageChannelProvider.cs" />
<Compile Include="Messaging\Services\IMessageChannelSelector.cs" />
<Compile Include="Messaging\Services\IMessageService.cs" />
<Compile Include="Messaging\Services\NullMessageChannelSelector.cs" />
<Compile Include="Mvc\DataAnnotations\LocalizedRegularExpressionAttribute.cs" />
<Compile Include="Mvc\DataAnnotations\LocalizedStringMaxLengthAttribute.cs" />
<Compile Include="Mvc\DataAnnotations\LocalizedRangeAttribute.cs" />