V14: Create member filter (#15877)

* Update filter to include membergroup name

* add filter by isApproved

* Add isLockedOut

* Implement member filter

* Move filter logic to repository

* Add more fields to sort by

* Update openApi

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Nikolaj Geisle
2024-03-14 14:24:10 +01:00
committed by GitHub
parent 88a1768b1d
commit 6e1924d054
7 changed files with 170 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.Filter;
@@ -13,16 +14,13 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Filter;
[ApiVersion("1.0")]
public class FilterMemberFilterController : MemberFilterControllerBase
{
private readonly IMemberTypeService _memberTypeService;
private readonly IMemberService _memberService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
public FilterMemberFilterController(
IMemberTypeService memberTypeService,
IMemberService memberService,
IMemberPresentationFactory memberPresentationFactory)
{
_memberTypeService = memberTypeService;
_memberService = memberService;
_memberPresentationFactory = memberPresentationFactory;
}
@@ -33,38 +31,30 @@ public class FilterMemberFilterController : MemberFilterControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Filter(
Guid? memberTypeId = null,
string? memberGroupName = null,
bool? isApproved = null,
bool? isLockedOut = null,
string orderBy = "username",
Direction orderDirection = Direction.Ascending,
string? filter = null,
int skip = 0,
int take = 100)
{
// TODO: Move to service once we have FilterAsync method for members
string? memberTypeAlias = null;
if (memberTypeId.HasValue)
var memberFilter = new MemberFilter()
{
IMemberType? memberType = await _memberTypeService.GetAsync(memberTypeId.Value);
if (memberType == null)
{
return MemberTypeNotFound();
}
MemberTypeId = memberTypeId,
MemberGroupName = memberGroupName,
IsApproved = isApproved,
IsLockedOut = isLockedOut,
Filter = filter,
};
memberTypeAlias = memberType.Alias;
}
IEnumerable<IMember> members = await Task.FromResult(_memberService.GetAll(
skip,
take,
out var totalRecords,
orderBy,
orderDirection,
memberTypeAlias,
filter ?? string.Empty));
PagedModel<IMember> members = await _memberService.FilterAsync(memberFilter, orderBy, orderDirection, skip, take);
var pageViewModel = new PagedViewModel<MemberResponseModel>
{
Items = await _memberPresentationFactory.CreateMultipleAsync(members),
Total = totalRecords,
Items = await _memberPresentationFactory.CreateMultipleAsync(members.Items),
Total = members.Total,
};
return Ok(pageViewModel);

View File

@@ -16636,6 +16636,27 @@
"format": "uuid"
}
},
{
"name": "memberGroupName",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "isApproved",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isLockedOut",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "orderBy",
"in": "query",

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Core.Models.Membership;
public class MemberFilter
{
public Guid? MemberTypeId { get; set; }
public string? MemberGroupName { get; set; }
public bool? IsApproved { get; set; }
public bool? IsLockedOut { get; set; }
public string? Filter { get; set; }
}

View File

@@ -1,5 +1,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.Persistence.Repositories;
@@ -38,4 +40,6 @@ public interface IMemberRepository : IContentRepository<int, IMember>
/// <param name="query"></param>
/// <returns></returns>
int GetCountByQuery(IQuery<IMember>? query);
Task<PagedModel<IMember>> GetPagedByFilterAsync(MemberFilter memberFilter,int skip, int take, Ordering? ordering = null);
}

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
namespace Umbraco.Cms.Core.Services;
@@ -91,6 +92,13 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
string? memberTypeAlias,
string filter);
public Task<PagedModel<IMember>> FilterAsync(
MemberFilter memberFilter,
string orderBy = "username",
Direction orderDirection = Direction.Ascending,
int skip = 0,
int take = 100);
/// <summary>
/// Creates an <see cref="IMember" /> object without persisting it
/// </summary>

View File

@@ -99,6 +99,20 @@ namespace Umbraco.Cms.Core.Services
#region Create
public async Task<PagedModel<IMember>> FilterAsync(
MemberFilter memberFilter,
string orderBy = "username",
Direction orderDirection = Direction.Ascending,
int skip = 0,
int take = 100)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.MemberTypes);
scope.ReadLock(Constants.Locks.MemberTree);
return await _memberRepository.GetPagedByFilterAsync(memberFilter, skip, take, Ordering.By(orderBy, orderDirection));
}
/// <summary>
/// Creates an <see cref="IMember"/> object without persisting it
/// </summary>

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
@@ -202,6 +203,100 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
return Database.ExecuteScalar<int>(fullSql);
}
public async Task<PagedModel<IMember>> GetPagedByFilterAsync(MemberFilter memberFilter, int skip, int take, Ordering? ordering = null)
{
Sql<ISqlContext> sql = Sql().Select<NodeDto>(x => x.NodeId)
.From<NodeDto>()
.InnerJoin<MemberDto>().On<NodeDto, MemberDto>((n, m) => n.NodeId == m.NodeId);
if (memberFilter.MemberTypeId.HasValue)
{
sql = sql
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((memberNode, memberContent) => memberContent.NodeId == memberNode.NodeId)
.InnerJoin<NodeDto>("mtn").On<NodeDto, ContentDto>((memberTypeNode, memberContent) => memberContent.ContentTypeId == memberTypeNode.NodeId && memberTypeNode.UniqueId == memberFilter.MemberTypeId, "mtn");
}
if (memberFilter.MemberGroupName.IsNullOrWhiteSpace() is false)
{
sql = sql
.InnerJoin<Member2MemberGroupDto>().On<MemberDto, Member2MemberGroupDto>((m, memberToGroup) => m.NodeId == memberToGroup.Member)
.InnerJoin<NodeDto>("mgn").On<NodeDto, Member2MemberGroupDto>((memberGroupNode, memberToGroup) => memberToGroup.MemberGroup == memberGroupNode.NodeId && memberGroupNode.Text == memberFilter.MemberGroupName, "mgn");
}
if (memberFilter.IsApproved is not null)
{
sql = sql.Where<MemberDto>(member => member.IsApproved == memberFilter.IsApproved);
}
if (memberFilter.IsLockedOut is not null)
{
sql = sql.Where<MemberDto>(member => member.IsLockedOut == memberFilter.IsLockedOut);
}
if (memberFilter.Filter is not null)
{
var whereClauses = new List<Func<Sql<ISqlContext>, Sql<ISqlContext>>>()
{
(x) => x.Where<NodeDto>(memberNode => memberNode.Text != null && memberNode.Text.Contains(memberFilter.Filter)),
(x) => x.Where<MemberDto>(memberNode => memberNode.Email.Contains(memberFilter.Filter)),
(x) => x.Where<MemberDto>(memberNode => memberNode.LoginName.Contains(memberFilter.Filter)),
};
if (int.TryParse(memberFilter.Filter, out int filterAsIntId))
{
whereClauses.Add((x) => x.Where<NodeDto>(memberNode => memberNode.NodeId == filterAsIntId));
}
if (Guid.TryParse(memberFilter.Filter, out Guid filterAsGuid))
{
whereClauses.Add((x) => x.Where<NodeDto>(memberNode => memberNode.UniqueId == filterAsGuid));
}
sql = sql.WhereAny(whereClauses.ToArray());
}
if (ordering is not null)
{
ApplyOrdering(ref sql, ordering);
}
var pageIndex = skip / take;
Page<MemberDto>? pageResult = await Database.PageAsync<MemberDto>(pageIndex+1, take, sql);
// shortcut so our join is not too big, but we also hope these are cached, so we don't have to map them again.
var nodeIds = pageResult.Items.Select(x => x.NodeId).ToArray();
return new PagedModel<IMember>(pageResult.TotalItems, nodeIds.Any() ? GetMany(nodeIds) : Array.Empty<IMember>());
}
private void ApplyOrdering(ref Sql<ISqlContext> sql, Ordering ordering)
{
ArgumentNullException.ThrowIfNull(sql);
ArgumentNullException.ThrowIfNull(ordering);
if (ordering.OrderBy.IsNullOrWhiteSpace())
{
return;
}
var orderBy = ordering.OrderBy.ToLowerInvariant() switch
{
"username" => sql.GetAliasedField(SqlSyntax.GetFieldName<MemberDto>(x => x.LoginName)),
"name" => sql.GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.Text)),
"email" => sql.GetAliasedField(SqlSyntax.GetFieldName<MemberDto>(x => x.Email)),
_ => throw new NotSupportedException("Ordering not supported"),
};
if (ordering.Direction == Direction.Ascending)
{
sql.OrderBy(orderBy);
}
else
{
sql.OrderByDescending(orderBy);
}
}
/// <summary>
/// Gets paged member results.
/// </summary>