Skip to content

Commit

Permalink
v3.26.1 (#566)
Browse files Browse the repository at this point in the history
* Remove unnecessary exception nonsense

* Add fields to challenges report

* Partially address #254 - allow alt names on certificates

* MInor bug fixes

* Fix name settings bug

* Cleanup

* Fix team session start bug

* Unit test for ToLookup and snippet update

* Tests

* Add initial tests

* Tests

* Feedback templates wiP

* WIP feedback templates

* WIP feedback template stuff

* Ungunk feedback submissions schema and snapshots

* Finish feedback submission migrations

* More feedback wip

* MVP of feedback template stuff

* Fixed an issue that constrained the practice area to the game's gamespace limit

* Fixed feedback report export and certain feedback updates
  • Loading branch information
sei-bstein authored Dec 12, 2024
1 parent e04751b commit ba43a37
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 65 deletions.
19 changes: 19 additions & 0 deletions src/Gameboard.Api/Data/Store/Store.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> setProper
Task<IEnumerable<TEntity>> SaveAddRange<TEntity>(params TEntity[] entities) where TEntity : class, IEntity;
Task SaveRemoveRange<TEntity>(params TEntity[] entities) where TEntity : class, IEntity;
Task<TEntity> SaveUpdate<TEntity>(TEntity entity, CancellationToken cancellationToken) where TEntity : class, IEntity;
Task<TEntity> SaveUpdate<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> property, CancellationToken cancellationToken) where TEntity : class, IEntity;
Task SaveUpdateRange<TEntity>(params TEntity[] entities) where TEntity : class, IEntity;
Task<TEntity> SingleAsync<TEntity>(string id, CancellationToken cancellationToken) where TEntity : class, IEntity;
Task<TEntity> SingleOrDefaultAsync<TEntity>(CancellationToken cancellationToken) where TEntity : class, IEntity;
Expand Down Expand Up @@ -169,6 +170,24 @@ public async Task<TEntity> SaveUpdate<TEntity>(TEntity entity, CancellationToken
return entity;
}

public async Task<TEntity> SaveUpdate<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> property, CancellationToken cancellationToken) where TEntity : class, IEntity
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbContext.Attach(entity);
}

if (property is not null)
{
_dbContext.Entry(entity).Property(property).IsModified = true;
}

_dbContext.Update(entity);
await _dbContext.SaveChangesAsync(cancellationToken);

return entity;
}

public async Task SaveUpdateRange<TEntity>(params TEntity[] entities) where TEntity : class, IEntity
{
foreach (var entity in entities)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,49 +83,10 @@ await _validator
// ultimately our retval
var submissionModel = default(FeedbackSubmission);

// we also need the template so we can be sure to save an answer for every question, even if not supplied previously
var template = await _store
.WithNoTracking<FeedbackTemplate>()
.Where(t => t.Id == request.Request.FeedbackTemplateId)
.SingleAsync(cancellationToken);

// if updating, update
if (existingSubmission is not null)
{
if (request.Request.AttachedEntity.EntityType == FeedbackSubmissionAttachedEntityType.ChallengeSpec)
{
submissionModel = await _store
.WithNoTracking<FeedbackSubmissionChallengeSpec>()
.Where(s => s.Id == existingSubmission.Id)
.SingleAsync(cancellationToken);
}
else if (request.Request.AttachedEntity.EntityType == FeedbackSubmissionAttachedEntityType.Game)
{
submissionModel = await _store
.WithNoTracking<FeedbackSubmissionGame>()
.Where(s => s.Id == existingSubmission.Id)
.SingleAsync(cancellationToken);
}

submissionModel.WhenEdited = _nowService.Get();
submissionModel.Responses.Clear();

foreach (var question in _feedbackService.BuildQuestionConfigFromTemplate(template).Questions)
{
submissionModel.Responses.Add(new QuestionSubmission
{
Id = question.Prompt,
Answer = submissionModel.Responses.SingleOrDefault(r => r.Id == question.Id)?.Answer,
Prompt = question.Prompt,
ShortName = question.ShortName,
});
}

if (request.Request.IsFinalized && submissionModel.WhenFinalized is null)
{
submissionModel.WhenFinalized = _nowService.Get();
}
submissionModel = await _store.SaveUpdate(submissionModel, cancellationToken);
submissionModel = await UpdateSubmission(existingSubmission, request.Request, cancellationToken);
}
else
{
Expand Down Expand Up @@ -169,4 +130,64 @@ await _validator
cancellationToken
);
}

private async Task<FeedbackSubmission> UpdateSubmission(FeedbackSubmissionView existingSubmission, UpsertFeedbackSubmissionRequest request, CancellationToken cancellationToken)
{
var submissionModel = default(FeedbackSubmission);

// we also need the template so we can be sure to save an answer for every question, even if not supplied previously
var template = await _store
.WithNoTracking<FeedbackTemplate>()
.Where(t => t.Id == request.FeedbackTemplateId)
.SingleAsync(cancellationToken);

await _store.DoTransaction(async dbContext =>
{
if (request.AttachedEntity.EntityType == FeedbackSubmissionAttachedEntityType.ChallengeSpec)
{
submissionModel = await _store
.WithTracking<FeedbackSubmissionChallengeSpec>()
.Where(s => s.Id == existingSubmission.Id)
.SingleAsync(cancellationToken);
}
else if (request.AttachedEntity.EntityType == FeedbackSubmissionAttachedEntityType.Game)
{
submissionModel = await _store
.WithTracking<FeedbackSubmissionGame>()
.Where(s => s.Id == existingSubmission.Id)
.SingleAsync(cancellationToken);
}

submissionModel.WhenEdited = _nowService.Get();

foreach (var question in _feedbackService.BuildQuestionConfigFromTemplate(template).Questions)
{
var existingResponse = submissionModel.Responses.SingleOrDefault(r => r.Id == question.Id);
if (existingResponse is not null)
{
existingResponse.Answer = request.Responses.SingleOrDefault(r => r.Id == question.Id)?.Answer;
dbContext.Entry(existingResponse).Property(r => r.Answer).IsModified = true;
}
else
{
submissionModel.Responses.Add(new QuestionSubmission
{
Id = question.Prompt,
Answer = request.Responses.SingleOrDefault(r => r.Id == question.Id)?.Answer,
Prompt = question.Prompt,
ShortName = question.ShortName,
});
}
}

if (request.IsFinalized && submissionModel.WhenFinalized is null)
{
submissionModel.WhenFinalized = _nowService.Get();
}

await dbContext.SaveChangesAsync(cancellationToken);
}, cancellationToken);

return submissionModel;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Dynamic;
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Features.Users;
Expand All @@ -8,43 +8,57 @@

namespace Gameboard.Api.Features.Reports;

public record FeedbackReportExportQuery(FeedbackReportParameters Parameters) : IRequest<IEnumerable<FeedbackReportExportRecord>>;
public record FeedbackReportExportQuery(FeedbackReportParameters Parameters) : IRequest<FeedbackReportExportContainer>;

internal sealed class FeedbackReportExportHandler
(
IFeedbackReportService reportService,
IValidatorService validatorService
) : IRequestHandler<FeedbackReportExportQuery, IEnumerable<FeedbackReportExportRecord>>
) : IRequestHandler<FeedbackReportExportQuery, FeedbackReportExportContainer>
{
private readonly IFeedbackReportService _reportService = reportService;
private readonly IValidatorService _validator = validatorService;

public async Task<IEnumerable<FeedbackReportExportRecord>> Handle(FeedbackReportExportQuery request, CancellationToken cancellationToken)
public async Task<FeedbackReportExportContainer> Handle(FeedbackReportExportQuery request, CancellationToken cancellationToken)
{
await _validator
.Auth(c => c.RequirePermissions(PermissionKey.Reports_View))
.Validate(cancellationToken);

var results = await _reportService.GetBaseQuery(request.Parameters, cancellationToken);

return results.Select(r => new FeedbackReportExportRecord
// we have to use the dynamic type here to accommodate the questions/answers
var records = new List<dynamic>();
foreach (var r in results)
{
Id = r.Id,
ChallengeSpecId = r.ChallengeSpec?.Id,
ChallengeSpecName = r.ChallengeSpec?.Name,
GameId = r.LogicalGame.Id,
GameName = r.LogicalGame.Name,
GameSeason = r.LogicalGame.Season,
GameSeries = r.LogicalGame.Series,
GameTrack = r.LogicalGame.Track,
IsTeamGame = r.LogicalGame.IsTeamGame,
SponsorId = r.Sponsor.Id,
SponsorName = r.Sponsor.Name,
UserId = r.User.Id,
UserName = r.User.Name,
WhenCreated = r.WhenCreated,
WhenEdited = r.WhenEdited,
WhenFinalized = r.WhenFinalized
}).ToArray();
dynamic record = new ExpandoObject();

record.Id = r.Id;
record.ChallengeSpecId = r.ChallengeSpec?.Id;
record.ChallengeSpecName = r.ChallengeSpec?.Name;
record.GameId = r.LogicalGame.Id;
record.GameName = r.LogicalGame.Name;
record.GameSeason = r.LogicalGame.Season;
record.GameSeries = r.LogicalGame.Series;
record.GameTrack = r.LogicalGame.Track;
record.IsTeamGame = r.LogicalGame.IsTeamGame;
record.SponsorId = r.Sponsor.Id;
record.SponsorName = r.Sponsor.Name;
record.UserId = r.User.Id;
record.UserName = r.User.Name;
record.WhenCreated = r.WhenCreated;
record.WhenEdited = r.WhenEdited;
record.WhenFinalized = r.WhenFinalized;

var asDict = record as IDictionary<string, object>;
foreach (var question in r.Responses)
{
asDict.Add(question.Prompt, question.Answer);
}

records.Add(asDict);
}

return new FeedbackReportExportContainer { Records = records };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public sealed class FeedbackReportRecord
public required DateTimeOffset? WhenFinalized { get; set; }
}

public sealed class FeedbackReportExportContainer
{
public required IEnumerable<dynamic> Records { get; set; }
}

public sealed class FeedbackReportExportRecord
{
public required string Id { get; set; }
Expand Down
5 changes: 3 additions & 2 deletions src/Gameboard.Api/Features/Reports/ReportsExportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ServiceStack.Text;

namespace Gameboard.Api.Features.Reports;

Expand Down Expand Up @@ -38,7 +39,7 @@ public async Task<IActionResult> GetEnrollmentReportExport([FromQuery] Enrollmen
public async Task<IActionResult> GetFeedbackReportExport([FromQuery] FeedbackReportParameters parameters)
{
var results = await _mediator.Send(new FeedbackReportExportQuery(parameters));
return new FileContentResult(GetReportExport(results), MimeTypes.TextCsv);
return new FileContentResult(GetReportExport(results.Records), MimeTypes.TextCsv);
}

[HttpGet("players")]
Expand Down Expand Up @@ -67,7 +68,7 @@ public async Task<IActionResult> GetSupportReport([FromQuery] SupportReportParam

private byte[] GetReportExport<T>(IEnumerable<T> records)
{
var csvText = ServiceStack.StringExtensions.ToCsv(records);
var csvText = CsvSerializer.SerializeToCsv(records);
return Encoding.UTF8.GetBytes(csvText.ToString());
}
}
2 changes: 1 addition & 1 deletion src/Gameboard.Api/Gameboard.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PackageReference Include="mediatr" Version="12.0.1" />
<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
<PackageReference Include="IdentityModel" Version="3.10.10" />
<PackageReference Include="ServiceStack.Text" Version="6.9.0" />
<PackageReference Include="ServiceStack.Text" Version="8.5.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="TopoMojo.Api.Client" Version="2.3.1" />
Expand Down

0 comments on commit ba43a37

Please sign in to comment.