Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to Remove User from a Role (Issue #14632) #14652

Merged
merged 14 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
}
}

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