Skip to content

Commit

Permalink
Add a way to specify username to send notifications to (#16073)
Browse files Browse the repository at this point in the history
Co-authored-by: Zoltán Lehóczky <[email protected]>
  • Loading branch information
MikeAlhayek and Piedone authored May 17, 2024
1 parent 07ddde9 commit 0f57494
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,17 @@ public LiquidViewTemplate GetCachedTemplate(string source)

public bool Validate(string template, out IEnumerable<string> errors)
{
if (string.IsNullOrEmpty(template))
{
errors = [];

return true;
}

var success = _liquidViewParser.TryParse(template, out _, out var error);
errors = new[] { error };

errors = [error];

return success;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OrchardCore.Users;
using OrchardCore.Users.Indexes;
using OrchardCore.Users.Models;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.Services;
using YesSql;
using YesSql.Services;

namespace OrchardCore.Notifications.Activities;

public class NotifyUserTask : NotifyUserTaskActivity<NotifyUserTask>
{
private readonly ISession _session;

public NotifyUserTask(
INotificationService notificationCoordinator,
IWorkflowExpressionEvaluator expressionEvaluator,
HtmlEncoder htmlEncoder,
ILogger<NotifyUserTask> logger,
IStringLocalizer<NotifyUserTask> localizer
IStringLocalizer<NotifyUserTask> stringLocalizer,
ISession session
) : base(notificationCoordinator,
expressionEvaluator,
htmlEncoder,
logger,
localizer)
stringLocalizer)
{
_session = session;
}

public override LocalizedString DisplayText => S["Notify User Task"];
public override LocalizedString DisplayText => S["Notify Specific Users Task"];

public WorkflowExpression<string> UserNames
{
get => GetProperty(() => new WorkflowExpression<string>());
set => SetProperty(value);
}

protected override Task<IEnumerable<IUser>> GetUsersAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
protected override async Task<IEnumerable<IUser>> GetUsersAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
{
if (workflowContext.Input.TryGetValue("User", out var userObject) && userObject is User user && user.IsEnabled)
if (!string.IsNullOrEmpty(UserNames.Expression))
{
return Task.FromResult<IEnumerable<IUser>>(new[] { user });
var expression = await _expressionEvaluator.EvaluateAsync(UserNames, workflowContext, null);

var userNames = expression.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);

if (userNames.Length > 0)
{
var users = new List<User>();

foreach (var page in userNames.PagesOf(1000))
{
users.AddRange(await _session.Query<User, UserIndex>(user => user.NormalizedUserName.IsIn(page)).ListAsync());
}

return users;
}
}

return Task.FromResult(Enumerable.Empty<IUser>());
return [];
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
Expand All @@ -18,15 +18,15 @@ namespace OrchardCore.Notifications.Drivers;

public abstract class NotifyUserTaskActivityDisplayDriver<TActivity, TEditViewModel> : ActivityDisplayDriver<TActivity, TEditViewModel>
where TActivity : NotifyUserTaskActivity
where TEditViewModel : NotifyUserTaskActivityViewModel, new()
where TEditViewModel : class, new()
{
private readonly IHtmlSanitizerService _htmlSanitizerService;
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly NotificationOptions _notificationOptions;

protected readonly IStringLocalizer S;

protected virtual string EditShapeType => $"{nameof(NotifyUserTaskActivity)}_Fields_Edit";
protected virtual string EditShapeType { get; } = null;

public NotifyUserTaskActivityDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
Expand All @@ -40,45 +40,69 @@ public NotifyUserTaskActivityDisplayDriver(
S = stringLocalizer;
}

public override IDisplayResult Edit(TActivity model)
public override IDisplayResult Edit(TActivity activity)
{
return Initialize<TEditViewModel>(EditShapeType, viewModel =>
var results = new List<IDisplayResult>();

if (!string.IsNullOrEmpty(EditShapeType))
{
return EditActivityAsync(model, viewModel);
}).Location("Content");
results.Add(Initialize<TEditViewModel>(EditShapeType, viewModel =>
{
return EditActivityAsync(activity, viewModel);
}).Location("Content"));
}

results.Add(Initialize<NotifyUserTaskActivityViewModel>("NotifyUserTaskActivity_Fields_Edit", model =>
{
model.Subject = activity.Subject.Expression;
model.Summary = activity.Summary.Expression;
model.TextBody = activity.TextBody.Expression;
model.HtmlBody = activity.HtmlBody.Expression;
model.IsHtmlPreferred = activity.IsHtmlPreferred;
}).Location("Content"));

return Combine(results);
}

public async override Task<IDisplayResult> UpdateAsync(TActivity model, IUpdateModel updater)
public async override Task<IDisplayResult> UpdateAsync(TActivity activity, IUpdateModel updater)
{
var viewModel = new TEditViewModel();
await updater.TryUpdateModelAsync(viewModel, Prefix);

if (!_liquidTemplateManager.Validate(viewModel.Subject, out var subjectErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.Subject), S["Subject field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', subjectErrors)]);
}
var model = new NotifyUserTaskActivityViewModel();

await updater.TryUpdateModelAsync(model, Prefix);

if (!_liquidTemplateManager.Validate(viewModel.Summary, out var summaryErrors))
if (!_liquidTemplateManager.Validate(model.Subject, out var subjectErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.Summary), S["Summary field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', summaryErrors)]);
updater.ModelState.AddModelError(Prefix, nameof(model.Subject), S["Subject field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', subjectErrors)]);
}

if (!_liquidTemplateManager.Validate(viewModel.TextBody, out var textBodyErrors))
if (!_liquidTemplateManager.Validate(model.Summary, out var summaryErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.TextBody), S["Text Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', textBodyErrors)]);
updater.ModelState.AddModelError(Prefix, nameof(model.Summary), S["Summary field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', summaryErrors)]);
}

if (!_liquidTemplateManager.Validate(viewModel.HtmlBody, out var htmlBodyErrors))
if (!_liquidTemplateManager.Validate(model.TextBody, out var textBodyErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.HtmlBody), S["HTML Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', htmlBodyErrors)]);
updater.ModelState.AddModelError(Prefix, nameof(model.TextBody), S["Text Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', textBodyErrors)]);
}

if (updater.ModelState.IsValid)
if (!_liquidTemplateManager.Validate(model.HtmlBody, out var htmlBodyErrors))
{
await UpdateActivityAsync(viewModel, model);
updater.ModelState.AddModelError(Prefix, nameof(model.HtmlBody), S["HTML Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', htmlBodyErrors)]);
}

return Edit(model);
activity.Subject = new WorkflowExpression<string>(model.Subject);
activity.Summary = new WorkflowExpression<string>(_htmlSanitizerService.Sanitize(model.Summary ?? string.Empty));
activity.TextBody = new WorkflowExpression<string>(model.TextBody);
activity.HtmlBody = new WorkflowExpression<string>(_notificationOptions.DisableNotificationHtmlBodySanitizer ? model.HtmlBody : _htmlSanitizerService.Sanitize(model.HtmlBody ?? string.Empty));
activity.IsHtmlPreferred = model.IsHtmlPreferred;

var modelOfT = new TEditViewModel();

await updater.TryUpdateModelAsync(modelOfT, Prefix);

await UpdateActivityAsync(modelOfT, activity);

return Edit(activity);
}

/// <summary>
Expand All @@ -96,15 +120,10 @@ protected override ValueTask EditActivityAsync(TActivity activity, TEditViewMode
/// </summary>
protected override void EditActivity(TActivity activity, TEditViewModel model)
{
model.Subject = activity.Subject.Expression;
model.Summary = activity.Summary.Expression;
model.TextBody = activity.TextBody.Expression;
model.HtmlBody = activity.HtmlBody.Expression;
model.IsHtmlPreferred = activity.IsHtmlPreferred;
}

/// <summary>
/// Updates the activity when the view model is validated.
/// Updates the activity.
/// </summary>
protected override Task UpdateActivityAsync(TEditViewModel model, TActivity activity)
{
Expand All @@ -114,23 +133,19 @@ protected override Task UpdateActivityAsync(TEditViewModel model, TActivity acti
}

/// <summary>
/// Updates the activity when the view model is validated.
/// Updates the activity.
/// </summary>
protected override void UpdateActivity(TEditViewModel model, TActivity activity)
{
activity.Subject = new WorkflowExpression<string>(model.Subject);
activity.Summary = new WorkflowExpression<string>(_htmlSanitizerService.Sanitize(model.Summary));
activity.TextBody = new WorkflowExpression<string>(model.TextBody);
activity.HtmlBody = new WorkflowExpression<string>(_notificationOptions.DisableNotificationHtmlBodySanitizer ? model.HtmlBody : _htmlSanitizerService.Sanitize(model.HtmlBody));
activity.IsHtmlPreferred = model.IsHtmlPreferred;

}

public override IDisplayResult Display(TActivity activity)
{
return Combine(
Shape($"{typeof(TActivity).Name}_Fields_Thumbnail", new ActivityViewModel<TActivity>(activity))
Shape($"{ActivityName}_Fields_Thumbnail", new ActivityViewModel<TActivity>(activity))
.Location("Thumbnail", "Content"),
Shape($"{typeof(TActivity).Name}_Fields_Design", new ActivityViewModel<TActivity>(activity))
Shape($"{ActivityName}_Fields_Design", new ActivityViewModel<TActivity>(activity))
.Location("Design", "Content")
);
}
Expand All @@ -147,4 +162,6 @@ public NotifyUserTaskActivityDisplayDriver(
: base(htmlSanitizerService, liquidTemplateManager, notificationOptions, stringLocalizer)
{
}

sealed protected override string EditShapeType { get; } = null;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Infrastructure.Html;
using OrchardCore.Liquid;
using OrchardCore.Mvc.ModelBinding;
using OrchardCore.Notifications.Activities;
using OrchardCore.Notifications.Models;
using OrchardCore.Notifications.ViewModels;
using OrchardCore.Workflows.Models;

namespace OrchardCore.Notifications.Drivers;

public class NotifyUserTaskDisplayDriver : NotifyUserTaskActivityDisplayDriver<NotifyUserTask>
public class NotifyUserTaskDisplayDriver : NotifyUserTaskActivityDisplayDriver<NotifyUserTask, NotifyUserTaskViewModel>
{
public NotifyUserTaskDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
Expand All @@ -17,4 +25,33 @@ public NotifyUserTaskDisplayDriver(
: base(htmlSanitizerService, liquidTemplateManager, notificationOptions, stringLocalizer)
{
}

protected override string EditShapeType { get; } = $"{ActivityName}_Fields_Edit";

public override async Task<IDisplayResult> UpdateAsync(NotifyUserTask activity, IUpdateModel updater)
{
var viewModel = new NotifyUserTaskViewModel();
await updater.TryUpdateModelAsync(viewModel, Prefix);

var userNames = viewModel.UserNames?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.OrdinalIgnoreCase) ?? [];

if (!userNames.Any())
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.UserNames), S["Please provide at least one username to notify."]);
}
else
{
activity.UserNames = new WorkflowExpression<string>(string.Join(", ", userNames));
}

return await base.UpdateAsync(activity, updater);
}

protected override void EditActivity(NotifyUserTask activity, NotifyUserTaskViewModel model)
{
base.EditActivity(activity, model);

model.UserNames = activity.UserNames.Expression;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OrchardCore.Notifications.ViewModels;

public class NotifyUserTaskViewModel
{
public string UserNames { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
@model ActivityViewModel<NotifyUserTask>

<header>
<h4><i class="fa-solid fa-envelope" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Notify user"])</h4>
<h4><i class="fa-solid fa-envelope" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Notify Specific Users"])</h4>
</header>
<em>&quot;@Model.Activity.Subject&quot;</em>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@model NotifyUserTaskViewModel

<div class="mb-3" asp-validation-class-for="UserNames">
<label asp-for="UserNames" class="form-label">@T["User names"]</label>
<input type="text" asp-for="UserNames" class="form-control" />
<span asp-validation-for="UserNames"></span>
<span class="hint">@T["Please provide a comma separated list of user names. You may use Liquid syntax."]</span>
</div>
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<h4 class="card-title"><i class="fa-solid fa-envelope" aria-hidden="true"></i>@T["Notify user"]</h4>
<p>@T["Notify user"]</p>
<h4 class="card-title"><i class="fa-solid fa-envelope" aria-hidden="true"></i>@T["Notify Specific Users"]</h4>
<p>@T["Notify specific users"]</p>
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
<div class="mb-3" asp-validation-class-for="TextBody">
<label asp-for="TextBody" class="form-label">@T["Text Body"]</label>
<textarea asp-for="TextBody" rows="5" class="form-control"></textarea>
<span asp-validation-for="TextBody"></span>
<span class="hint">@T["This optional text body does not support HTML. You may use Liquid syntax."]</span>
</div>

<div class="mb-3" asp-validation-class-for="HtmlBody">
<label asp-for="HtmlBody" class="form-label">@T["HTML Body"]</label>
<textarea asp-for="HtmlBody" rows="10" class="form-control"></textarea>
<span asp-validation-for="HtmlBody"></span>
<span class="hint">@T["HTML message will only be sent if the notification provider supports it. You may use Liquid syntax."]</span>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public abstract class DisplayDriver<TModel, TDisplayContext, TEditorContext, TUp
where TEditorContext : BuildEditorContext
where TUpdateContext : UpdateEditorContext
{
protected static readonly string ModelName = typeof(TModel).Name;

/// <summary>
/// Returns <c>true</c> if the model can be handle by the current driver.
/// </summary>
Expand Down Expand Up @@ -116,11 +118,11 @@ protected virtual void BuildPrefix(TModel model, string htmlFieldPrefix)
{
if (!string.IsNullOrEmpty(htmlFieldPrefix))
{
Prefix = $"{htmlFieldPrefix}.{typeof(TModel).Name}";
Prefix = $"{htmlFieldPrefix}.{ModelName}";
}
else
{
Prefix = typeof(TModel).Name;
Prefix = ModelName;
}
}
}
Expand Down
Loading

0 comments on commit 0f57494

Please sign in to comment.