Skip to content

Commit

Permalink
Fix bugs and allow upsert of feedback for other users by admins
Browse files Browse the repository at this point in the history
  • Loading branch information
sei-bstein committed Dec 12, 2024
1 parent ba43a37 commit 3bddcbb
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 7 deletions.
4 changes: 4 additions & 0 deletions src/Gameboard.Api/Features/Feedback/FeedbackController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ public Task<FeedbackTemplateView> GetTemplate([FromRoute] string templateId)
public Task<ListFeedbackTemplatesResponse> ListTemplates()
=> _mediator.Send(new ListFeedbackTemplatesQuery());

[HttpPut("template/{templateId}")]
public Task<FeedbackTemplateView> UpdateTemplate([FromQuery] string templateId, [FromBody] UpdateFeedbackTemplateRequest request)
=> _mediator.Send(new UpdateFeedbackTemplateCommand(request));

/// <summary>
/// Create a new feedback response.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Gameboard.Api.Data;
using Gameboard.Api.Services;
using Gameboard.Api.Structure.MediatR;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Features.Feedback;

public record UpdateFeedbackTemplateCommand(UpdateFeedbackTemplateRequest Request) : IRequest<FeedbackTemplateView>;

internal sealed class UpdateFeedbackTemplateHandler
(
IMapper mapper,
IStore store,
IValidatorService validator
) : IRequestHandler<UpdateFeedbackTemplateCommand, FeedbackTemplateView>
{
private readonly IMapper _mapper = mapper;
private readonly IStore _store = store;
private readonly IValidatorService _validator = validator;

public async Task<FeedbackTemplateView> Handle(UpdateFeedbackTemplateCommand request, CancellationToken cancellationToken)
{
await _validator
.Auth(c => c.RequirePermissions(Users.PermissionKey.Games_CreateEditDelete))
.AddEntityExistsValidator<FeedbackTemplate>(request.Request.Id)
.AddValidator(request.Request.Name.IsEmpty(), new MissingRequiredInput<string>(nameof(request.Request.Name)))
.AddValidator(request.Request.Content.IsEmpty(), new MissingRequiredInput<string>(nameof(request.Request.Content)))
.AddValidator(async ctx =>
{
if (await _store.WithNoTracking<FeedbackTemplate>().AnyAsync(t => t.Name == request.Request.Name && t.Id != request.Request.Id))
{
ctx.AddValidationException(new DuplicateFeedbackTemplateNameException(request.Request.Name));
}
})
.Validate(cancellationToken);

await _store
.WithNoTracking<FeedbackTemplate>()
.Where(t => t.Id == request.Request.Id)
.ExecuteUpdateAsync
(
up => up
.SetProperty(t => t.HelpText, request.Request.HelpText)
.SetProperty(t => t.Content, request.Request.Content)
.SetProperty(t => t.Name, request.Request.Name),
cancellationToken
);

return await _mapper
.ProjectTo<FeedbackTemplateView>(_store.WithNoTracking<FeedbackTemplate>().Where(t => t.Id == request.Request.Id))
.SingleAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Gameboard.Api.Features.Feedback;

public sealed class UpdateFeedbackTemplateRequest
{
public required string Id { get; set; }
public required string Content { get; set; }
public string HelpText { get; set; }
public required string Name { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Users;
using Gameboard.Api.Services;
using Gameboard.Api.Structure.MediatR;
using MediatR;
Expand All @@ -19,22 +20,23 @@ internal sealed class UpsertFeedbackSubmissionHandler
IActingUserService actingUserService,
FeedbackService feedbackService,
INowService now,
IUserRolePermissionsService permissions,
IStore store,
IValidatorService validatorService
) : IRequestHandler<UpsertFeedbackSubmissionCommand, FeedbackSubmissionView>
{
private readonly IActingUserService _actingUserService = actingUserService;
private readonly FeedbackService _feedbackService = feedbackService;
private readonly INowService _nowService = now;
private readonly IUserRolePermissionsService _permissions = permissions;
private readonly IStore _store = store;
private readonly IValidatorService _validator = validatorService;

public async Task<FeedbackSubmissionView> Handle(UpsertFeedbackSubmissionCommand request, CancellationToken cancellationToken)
{
var actingUserId = _actingUserService.Get()?.Id;

await _validator
.Auth(c => c.RequireAuthentication())
.AddEntityExistsValidator<FeedbackTemplate>(request.Request.FeedbackTemplateId)
.AddValidator(ctx =>
{
if (request.Request.AttachedEntity.EntityType != FeedbackSubmissionAttachedEntityType.ChallengeSpec && request.Request.AttachedEntity.EntityType != FeedbackSubmissionAttachedEntityType.Game)
Expand All @@ -52,7 +54,14 @@ await _validator
}

})
.AddEntityExistsValidator<FeedbackTemplate>(request.Request.FeedbackTemplateId)
.AddValidator(async ctx =>
{
// you can pass a non-you userid here, but if you do, you have to have Admin_View
if (request.Request.UserId.IsNotEmpty() && request.Request.UserId != _actingUserService.Get().Id && !await _permissions.Can(PermissionKey.Admin_View))
{
ctx.AddValidationException(new CantUpdateOtherUserFeedback(request.Request.UserId));
}
})
.AddValidator(async ctx =>
{
var existingSubmission = await _feedbackService.ResolveExistingSubmission
Expand All @@ -70,11 +79,14 @@ await _validator
})
.Validate(cancellationToken);

// the user ID we're going to work on is the one in the request OR the logged in one if that's blank
var updateForUserId = request.Request.UserId.IsEmpty() ? _actingUserService.Get().Id : request.Request.UserId;

// we don't have them update by id since the user id + entity are a unique key
// so load any previous submission to check for update
var existingSubmission = await _feedbackService.ResolveExistingSubmission
(
_actingUserService.Get().Id,
updateForUserId,
request.Request.AttachedEntity.EntityType,
request.Request.AttachedEntity.Id,
cancellationToken
Expand All @@ -99,7 +111,7 @@ await _validator
ChallengeSpecId = request.Request.AttachedEntity.Id,
FeedbackTemplateId = request.Request.FeedbackTemplateId,
Responses = [.. request.Request.Responses],
UserId = actingUserId,
UserId = updateForUserId,
WhenCreated = _nowService.Get(),
}, cancellationToken);
}
Expand All @@ -111,7 +123,7 @@ await _validator
GameId = request.Request.AttachedEntity.Id,
FeedbackTemplateId = request.Request.FeedbackTemplateId,
Responses = [.. request.Request.Responses],
UserId = actingUserId,
UserId = updateForUserId,
WhenCreated = _nowService.Get(),
}, cancellationToken);
}
Expand All @@ -124,7 +136,7 @@ await _validator

return await _feedbackService.ResolveExistingSubmission
(
_actingUserService.Get().Id,
updateForUserId,
request.Request.AttachedEntity.EntityType,
request.Request.AttachedEntity.Id,
cancellationToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ public sealed class UpsertFeedbackSubmissionRequest
public required string FeedbackTemplateId { get; set; }
public required bool IsFinalized { get; set; }
public IEnumerable<QuestionSubmission> Responses { get; set; }
public string UserId { get; set; }
}

public sealed class UpsertFeedbackSubmissionResponse
{
public required FeedbackSubmissionView Submission { get; set; }
}

public sealed class CantUpdateOtherUserFeedback : GameboardValidationException
{
public CantUpdateOtherUserFeedback(string otherUserId)
: base($"Can't update feedback for other user {otherUserId}") { }
}

0 comments on commit 3bddcbb

Please sign in to comment.