Skip to content

Commit

Permalink
Add a way to Remove User from a Role (Issue #14632) (#14652)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Mike Alhayek <mike@crestapps.com>
  • Loading branch information
elaurentin and MikeAlhayek committed Nov 21, 2023
1 parent ba3521d commit aa8f2be
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Roles.Core\OrchardCore.Roles.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Users.Core\OrchardCore.Users.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Workflows.Abstractions\OrchardCore.Workflows.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@using OrchardCore.Workflows.ViewModels
@using OrchardCore.Workflows.Helpers
@using OrchardCore.Roles.Workflows.Activities
@using OrchardCore.Roles.Workflows.ViewModels

@model ActivityViewModel<GetUsersByRoleTask>

<header>
<h4>
<i class="fa-solid fa-user" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Get users in roles"])
</h4>
</header>
<em>@T["Get users in roles {0} to {1}", string.Join(',', Model.Activity.Roles), Model.Activity.OutputKeyName]</em>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@using OrchardCore.Roles.Workflows.ViewModels

@model GetUsersByRoleTaskViewModel

<div class="mb-3" asp-validation-class-for="OutputKeyName">
<label asp-for="OutputKeyName">@T["Output Key Name"]</label>
<input type="text" asp-for="OutputKeyName" class="form-control code" />
<span asp-validation-for="OutputKeyName"></span>
<span class="hint">@T["The designated key name for storing the users' list in the workflow output. This key enables later access to the list within the workflow.With Liquid support."]</span>
</div>

<div class="mb-3" asp-validation-class-for="Roles">
<label asp-for="Roles">@T["Roles"]</label>
@await Component.InvokeAsync("SelectRoles", new { selectedRoles = Model.Roles, htmlName = Html.NameFor(m => m.Roles), except = new[] { "Anonymous", "Authenticated" } })
<span asp-validation-for="Roles"></span>
<span class="hint">@T["Choose the roles used to identify users during list generation."]</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h4 class="card-title">
<i class="fa-solid fa-user" aria-hidden="true"></i>@T["Get users in roles"]
</h4>
<p>@T["Get users in roles."]</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@using OrchardCore.Workflows.ViewModels
@using OrchardCore.Workflows.Helpers
@using OrchardCore.Roles.Workflows.Activities
@using OrchardCore.Roles.Workflows.ViewModels

@model ActivityViewModel<UnassignUserRoleTask>

<header>
<h4>
<i class="fa-solid fa-user" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Unassign user from roles"])
</h4>
</header>
<em>@T["Unassign {0} from roles {1}", Model.Activity.UserName, string.Join(",", Model.Activity.Roles)]</em>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@using OrchardCore.Roles.Workflows.ViewModels

@model UnassignUserRoleTaskViewModel

<div class="mb-3" asp-validation-class-for="UserName">
<label asp-for="UserName">@T["UserName"]</label>
<input type="text" asp-for="UserName" class="form-control code" />
<span asp-validation-for="UserName"></span>
<span class="hint">@T["The User to update. With Liquid support."]</span>
</div>

<div class="mb-3" asp-validation-class-for="Roles">
<label asp-for="Roles">@T["Roles"]</label>
@await Component.InvokeAsync("SelectRoles", new { selectedRoles = Model.Roles, htmlName = Html.NameFor(m => m.Roles), except = new[] { "Anonymous", "Authenticated" } })
<span asp-validation-for="Roles"></span>
<span class="hint">@T["The Roles to remove."]</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h4 class="card-title">
<i class="fa-solid fa-user" aria-hidden="true"></i>@T["Unassign user from roles"]
</h4>
<p>@T["Unassign a user from roles."]</p>
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
@model SelectRolesViewModel

<div class="d-flex flex-wrap">
<div class="mb-3">
@for (var i = 0; i < Model.RoleSelections.Count; i++)
{
var role = Model.RoleSelections[i];
<div class="checkbox pr-2">
<label for="@Html.IdFor(m => m.RoleSelections[i])">
<input id="@Html.IdFor(m => m.RoleSelections[i])" name="@Model.HtmlName" type="checkbox" checked="@role.IsSelected" value="@role.Item" />
@role.Item
var htmlId = Html.IdFor(m => m.RoleSelections[i]);

<div class="checkbox form-check">
<input id="@htmlId" class="form-check-input" name="@Model.HtmlName" type="checkbox" checked="@role.IsSelected" value="@role.Item" />
<label for="@htmlId" class="form-check-label">
@role.Item
</label>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Fluid;
using Fluid.Values;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using OrchardCore.Users;
using OrchardCore.Users.Models;
using OrchardCore.Workflows.Abstractions.Models;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.Services;

namespace OrchardCore.Roles.Workflows.Activities;

public class GetUsersByRoleTask : TaskActivity<GetUsersByRoleTask>
{
private readonly UserManager<IUser> _userManager;
private readonly IWorkflowExpressionEvaluator _expressionEvaluator;
protected readonly IStringLocalizer S;

public GetUsersByRoleTask(UserManager<IUser> userManager, IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer<GetUsersByRoleTask> localizer)
{
_userManager = userManager;
_expressionEvaluator = expressionEvaluator;
S = localizer;
}

public override LocalizedString DisplayText => S["Get Users by Role Task"];

public override LocalizedString Category => S["User"];

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

public IEnumerable<string> Roles
{
get => GetProperty(() => new List<string>());
set => SetProperty(value);
}

public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
=> Outcomes(S["Done"], S["Failed"]);

public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
{
var outputKeyName = await _expressionEvaluator.EvaluateAsync(OutputKeyName, workflowContext, null);

if (!string.IsNullOrEmpty(outputKeyName))
{
var usersInRole = new Dictionary<string, string>();

foreach (var role in Roles)
{
foreach (var u in await _userManager.GetUsersInRoleAsync(role))
{
if (u is not User user)
{
continue;
}

usersInRole.TryAdd(user.Id.ToString(), user.UserId);
}
}

workflowContext.Output[outputKeyName] = FluidValue.Create(usersInRole, new TemplateOptions());

return Outcomes("Done");
}

return Outcomes("Failed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using OrchardCore.Users;
using OrchardCore.Users.Models;
using OrchardCore.Users.Services;
using OrchardCore.Workflows.Abstractions.Models;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.Services;

namespace OrchardCore.Roles.Workflows.Activities;

public class UnassignUserRoleTask : TaskActivity<UnassignUserRoleTask>
{
private readonly UserManager<IUser> _userManager;
private readonly IUserService _userService;
private readonly IWorkflowExpressionEvaluator _expressionEvaluator;
protected readonly IStringLocalizer S;

public UnassignUserRoleTask(UserManager<IUser> userManager, IUserService userService, IWorkflowExpressionEvaluator expressionvaluator, IStringLocalizer<UnassignUserRoleTask> localizer)
{
_userManager = userManager;
_userService = userService;
_expressionEvaluator = expressionvaluator;
S = localizer;
}

public override LocalizedString DisplayText => S["Unassign User Role Task"];

public override LocalizedString Category => S["User"];

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

public IEnumerable<string> Roles
{
get => GetProperty(() => new List<string>());
set => SetProperty(value);
}

public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
=> Outcomes(S["Done"], S["Failed"]);

public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
{
var userName = await _expressionEvaluator.EvaluateAsync(UserName, workflowContext, null);

var u = await _userService.GetUserAsync(userName);

if (u is User user)
{
foreach(var role in Roles)
{
if (user.RoleNames.Contains(role))
{
await _userManager.RemoveFromRoleAsync(user, role);
}
}

return Outcomes("Done");
}

return Outcomes("Failed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using OrchardCore.Roles.Workflows.Activities;
using OrchardCore.Roles.Workflows.ViewModels;
using OrchardCore.Workflows.Display;
using OrchardCore.Workflows.Models;

namespace OrchardCore.Roles.Workflows.Drivers;

public class GetUsersByRoleTaskDisplayDriver : ActivityDisplayDriver<GetUsersByRoleTask, GetUsersByRoleTaskViewModel>
{
protected override void EditActivity(GetUsersByRoleTask activity, GetUsersByRoleTaskViewModel model)
{
model.OutputKeyName = activity.OutputKeyName.Expression;
model.Roles = activity.Roles;
}

protected override void UpdateActivity(GetUsersByRoleTaskViewModel model, GetUsersByRoleTask activity)
{
activity.OutputKeyName = new WorkflowExpression<string>(model.OutputKeyName);
activity.Roles = model.Roles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using OrchardCore.Roles.Workflows.Activities;
using OrchardCore.Roles.Workflows.ViewModels;
using OrchardCore.Workflows.Display;
using OrchardCore.Workflows.Models;

namespace OrchardCore.Roles.Workflows.Drivers;

public class UnassignUserRoleTaskDisplayDriver : ActivityDisplayDriver<UnassignUserRoleTask, UnassignUserRoleTaskViewModel>
{
protected override void EditActivity(UnassignUserRoleTask activity, UnassignUserRoleTaskViewModel model)
{
model.UserName = activity.UserName.Expression;
model.Roles = activity.Roles;
}

protected override void UpdateActivity(UnassignUserRoleTaskViewModel model, UnassignUserRoleTask activity)
{
activity.UserName = new WorkflowExpression<string>(model.UserName);
activity.Roles = model.Roles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Modules;
using OrchardCore.Roles.Workflows.Activities;
using OrchardCore.Roles.Workflows.Drivers;
using OrchardCore.Workflows.Helpers;

namespace OrchardCore.Roles.Workflows;

[RequireFeatures("OrchardCore.Workflows")]
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddActivity<UnassignUserRoleTask, UnassignUserRoleTaskDisplayDriver>();
services.AddActivity<GetUsersByRoleTask, GetUsersByRoleTaskDisplayDriver>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace OrchardCore.Roles.Workflows.ViewModels;

public class GetUsersByRoleTaskViewModel
{
[Required]
public string OutputKeyName { get; set; }

[Required]
public IEnumerable<string> Roles { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace OrchardCore.Roles.Workflows.ViewModels;

public class UnassignUserRoleTaskViewModel
{
[Required]
public string UserName { get; set; }

[Required]
public IEnumerable<string> Roles { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fluid;
Expand All @@ -21,6 +22,34 @@ public UsersByIdFilter(ISession session)

public async ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext ctx)
{
if (input.Type == FluidValues.Dictionary)
{
if (input.ToObjectValue() is IFluidIndexable values)
{
var items = new Dictionary<long, string>();

foreach (var key in values.Keys)
{
if (long.TryParse(key, out var id) && values.TryGetValue(key, out var value))
{
items.Add(id, value.ToStringValue());
}
}

var cachedUsers = await _session.GetAsync<User>(items.Keys.ToArray());

var cachedUserIds = cachedUsers.Select(x => x.UserId).ToHashSet();

var missingUserIds = items.Values.Where(userId => !cachedUserIds.Contains(userId)).ToList();

var missingUsers = await _session.Query<User, UserIndex>(x => x.UserId.IsIn(missingUserIds)).ListAsync();

return FluidValue.Create(missingUsers.Concat(cachedUsers).ToList(), ctx.Options);
}

return NilValue.Empty;
}

if (input.Type == FluidValues.Array)
{
// List of user ids
Expand Down

0 comments on commit aa8f2be

Please sign in to comment.