Skip to content

Commit

Permalink
Merge pull request #2 from FIRSTinMI/event-teams
Browse files Browse the repository at this point in the history
Event teams
  • Loading branch information
NixFey authored Jan 6, 2025
2 parents 75b47e2 + f8adf4f commit c0d085b
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 5 deletions.
2 changes: 2 additions & 0 deletions FiMAdminApi.Data/DataContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class DataContext(DbContextOptions<DataContext> options) : DbContext(opti
public DbSet<Match> Matches { get; init; }
public DbSet<ScheduleDeviation> ScheduleDeviations { get; init; }
public DbSet<TruckRoute> TruckRoutes { get; init; }
public DbSet<EventTeam> EventTeams { get; init; }
public DbSet<EventTeamStatus> EventTeamStatuses { get; set; }

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
Expand Down
14 changes: 14 additions & 0 deletions FiMAdminApi.Data/Models/EventTeam.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace FiMAdminApi.Data.Models;

public class EventTeam
{
public int Id { get; set; }
public required Guid EventId { get; set; }
public virtual Event? Event { get; set; }
public required int TeamNumber { get; set; }
public required int LevelId { get; set; }
public virtual Level? Level { get; set; }
public string? Notes { get; set; }
public required string StatusId { get; set; }
public virtual EventTeamStatus? Status { get; set; }
}
25 changes: 25 additions & 0 deletions FiMAdminApi.Data/Models/EventTeamStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;

namespace FiMAdminApi.Data.Models;

public class EventTeamStatus
{
/// <summary>
/// Unique identifier for the status, but still understandable by humans
/// </summary>
[Key]
public required string Id { get; set; }

public required string Name { get; set; }

/// <summary>
/// Higher numbers are teams closer to ready for matches
/// </summary>
public required int Ordinal { get; set; }
}

public static class KnownEventTeamStatuses
{
public const string Dropped = "Dropped";
public const string NotArrived = "NotArrived";
}
2 changes: 2 additions & 0 deletions FiMAdminApi/Clients/BlueAllianceDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ private static DateTime DateStringToDate(string? input, bool isEndOfDay = false)
/// </summary>
private HttpRequestMessage BuildGetRequest(FormattableString endpoint, Dictionary<string, string>? queryParams = default)
{
Logger.LogWarning("TBA is not fully supported at this time. Some data may not sync properly.");

var request = new HttpRequestMessage();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
request.Headers.Add("X-TBA-Auth-Key", _apiKey);
Expand Down
89 changes: 89 additions & 0 deletions FiMAdminApi/Endpoints/EventsEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using FiMAdminApi.Data;
using FiMAdminApi.Data.Enums;
using FiMAdminApi.Data.Models;
using FiMAdminApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -33,6 +34,10 @@ public static WebApplication RegisterEventsEndpoints(this WebApplication app, Ap
.WithDescription("Remove a staff user for an event");
eventsGroup.MapPost("{eventId:guid}/notes", CreateEventNote)
.WithDescription("Create an event note");
eventsGroup.MapPut("{eventId:guid}/teams", RefreshEventTeams)
.WithDescription("Refresh event teams");
eventsGroup.MapPut("{eventId:guid}/teams/{eventTeamId:int}", UpdateEventTeam)
.WithDescription("Update event team");

return app;
}
Expand Down Expand Up @@ -218,6 +223,66 @@ private static async Task<Results<Ok<EventNote>, NotFound, ForbidHttpResult, Val

return TypedResults.Ok(note);
}

private static async Task<Results<Ok, NotFound, ForbidHttpResult>> RefreshEventTeams(
[FromRoute] Guid eventId,
[FromServices] DataContext dbContext,
[FromServices] EventTeamsService teamsService,
ClaimsPrincipal user,
[FromServices] IAuthorizationService authSvc)
{
var evt = await dbContext.Events.Include(e => e.Season).FirstOrDefaultAsync(e => e.Id == eventId);
if (evt is null)
return TypedResults.NotFound();

var isAuthorized = await authSvc.AuthorizeAsync(user, eventId, new EventAuthorizationRequirement
{
NeededEventPermission = EventPermission.Event_ManageTeams,
NeededGlobalPermission = GlobalPermission.Events_Manage
});
if (!isAuthorized.Succeeded) return TypedResults.Forbid();

await teamsService.UpsertEventTeams(evt);

return TypedResults.Ok();
}

private static async Task<Results<Ok<EventTeam>, NotFound, ForbidHttpResult, ValidationProblem>> UpdateEventTeam(
[FromRoute] Guid eventId,
[FromRoute] int eventTeamId,
[FromBody] UpdateEventTeamRequest request,
[FromServices] DataContext dbContext,
ClaimsPrincipal user,
[FromServices] IAuthorizationService authSvc)
{
var (isValid, validationErrors) = await MiniValidator.TryValidateAsync(request);
if (!isValid) return TypedResults.ValidationProblem(validationErrors);

var evt = await dbContext.Events.Include(e => e.Season).FirstOrDefaultAsync(e => e.Id == eventId);
if (evt is null)
return TypedResults.NotFound();

var isAuthorized = await authSvc.AuthorizeAsync(user, eventId, new EventAuthorizationRequirement
{
NeededEventPermission = EventPermission.Event_ManageTeams,
NeededGlobalPermission = GlobalPermission.Events_Manage
});
if (!isAuthorized.Succeeded) return TypedResults.Forbid();

var eventTeam = await dbContext.EventTeams.FirstOrDefaultAsync(t => t.EventId == evt.Id && t.Id == eventTeamId);
if (eventTeam is null)
return TypedResults.NotFound();

eventTeam.StatusId = request.StatusId;
eventTeam.Notes = request.Notes;

await dbContext.SaveChangesAsync();

// Make sure we have the new status in the object we return
await dbContext.Entry(eventTeam).Reference(t => t.Status).LoadAsync();

return TypedResults.Ok(eventTeam);
}

public class UpdateBasicInfoRequest
{
Expand Down Expand Up @@ -263,4 +328,28 @@ public class CreateEventNoteRequest
[MaxLength(4000)]
public required string Content { get; set; }
}

public class UpdateEventTeamRequest : IValidatableObject
{
[Required]
[MaxLength(100)]
public required string StatusId { get; set; }

[MaxLength(4000)]
public string? Notes { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var matchingStatus = validationContext.GetRequiredService<DataContext>().EventTeamStatuses
.Where(s => s.Id == StatusId).Select(s => s.Id).FirstOrDefault();
if (string.IsNullOrEmpty(matchingStatus))
{
yield return new ValidationResult($"Unknown status '{StatusId}'.", new[] { nameof(StatusId) });
}
else
{
StatusId = matchingStatus;
}
}
}
}
1 change: 1 addition & 0 deletions FiMAdminApi/EventSync/EventSyncServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static void AddEventSyncSteps(this IServiceCollection services)
var steps = new[]
{
typeof(InitialSync),
typeof(PopulateEventTeams),
typeof(LoadQualSchedule),
typeof(UpdateQualResults),
typeof(UpdateQualRankings),
Expand Down
13 changes: 13 additions & 0 deletions FiMAdminApi/EventSync/Steps/PopulateEventTeams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FiMAdminApi.Clients;
using FiMAdminApi.Data.Models;
using FiMAdminApi.Services;

namespace FiMAdminApi.EventSync.Steps;

public class PopulateEventTeams(EventTeamsService teamsService) : EventSyncStep([EventStatus.NotStarted])
{
public override async Task RunStep(Event evt, IDataClient eventDataClient)
{
await teamsService.UpsertEventTeams(evt);
}
}
3 changes: 3 additions & 0 deletions FiMAdminApi/Infrastructure/SerializerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ namespace FiMAdminApi.Infrastructure;
[JsonSerializable(typeof(EventsEndpoints.UpdateBasicInfoRequest))]
[JsonSerializable(typeof(EventsEndpoints.UpsertEventStaffRequest))]
[JsonSerializable(typeof(EventsEndpoints.CreateEventNoteRequest))]
[JsonSerializable(typeof(EventsEndpoints.UpdateEventTeamRequest))]
[JsonSerializable(typeof(User[]))]
[JsonSerializable(typeof(EventStaff))]
[JsonSerializable(typeof(EventTeam))]
[JsonSerializable(typeof(EventTeamStatus))]
[JsonSerializable(typeof(EventsEndpoints.EventStaffInfo[]))]
[JsonSerializable(typeof(EventNote))]
[JsonSerializable(typeof(HealthEndpoints.ThinHealthReport))]
Expand Down
13 changes: 8 additions & 5 deletions FiMAdminApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
using Npgsql.NameTranslation;

var builder = WebApplication.CreateSlimBuilder(args);
// builder.Logging.ClearProviders();
builder.Logging.AddConsole(opt =>
{
builder.Configuration.GetSection("Logging:Console").Bind(opt);
Expand Down Expand Up @@ -66,7 +65,8 @@
o => o.MapEnum<TournamentLevel>("tournament_level", nameTranslator: new NpgsqlNullNameTranslator()));
});

// For authn/authz we're using tokens directly from Supabase. These tokens get validated by the supabase auth infrastructure
// For most authn/authz we're using tokens directly from Supabase. These tokens get validated by the supabase auth
// infrastructure
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddScheme<JwtBearerOptions, SupabaseJwtHandler>(JwtBearerDefaults.AuthenticationScheme, _ => { })
.AddScheme<EventSyncAuthOptions, EventSyncAuthHandler>(EventSyncAuthHandler.EventSyncAuthScheme, _ => { });
Expand All @@ -89,7 +89,8 @@
{
opt.AddDefaultPolicy(pol =>
{
pol.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? (string[])["http://localhost:5173"]);
pol.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ??
(string[]) ["http://localhost:5173"]);
pol.AllowCredentials();
pol.AllowAnyMethod();
pol.AllowAnyHeader();
Expand All @@ -98,6 +99,7 @@

builder.Services.AddScoped<UpsertEventsService>();
builder.Services.AddScoped<EventSyncService>();
builder.Services.AddScoped<EventTeamsService>();
builder.Services.AddClients();
builder.Services.AddEventSyncSteps();
builder.Services.AddOutputCache();
Expand All @@ -107,9 +109,10 @@
app.UseOutputCache();
app.UseApiConfiguration();

if (!bool.TryParse(app.Configuration["RUNNING_IN_CONTAINER"], out var inContainer) ||
!inContainer)
// When running in a container, traffic is served over HTTP with an external load balancer handling SSL termination
if (!bool.TryParse(app.Configuration["RUNNING_IN_CONTAINER"], out var inContainer) || !inContainer)
app.UseHttpsRedirection();

app.UseCors();

app.UseApiDocumentation();
Expand Down
45 changes: 45 additions & 0 deletions FiMAdminApi/Services/EventTeamsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using FiMAdminApi.Clients;
using FiMAdminApi.Data;
using FiMAdminApi.Data.Models;
using Microsoft.EntityFrameworkCore;

namespace FiMAdminApi.Services;

public class EventTeamsService(DataContext dbContext, IServiceProvider serviceProvider)
{
public async Task UpsertEventTeams(Event evt)
{
var dataClient = serviceProvider.GetKeyedService<IDataClient>(evt.SyncSource);

if (evt.Code is null || dataClient is null)
{
throw new ApplicationException("Unable to get data client for event");
}

if (evt.Season is null)
{
throw new ApplicationException("Season must be included to populate teams");
}

var existingTeams = await dbContext.EventTeams.Where(t => t.EventId == evt.Id).ToListAsync();
var apiTeams = await dataClient.GetTeamsForEvent(evt.Season, evt.Code);

// Delete removed teams
var removedTeamNumbers = existingTeams.Select(et => et.TeamNumber)
.Except(apiTeams.Select(at => at.TeamNumber));
await dbContext.EventTeams.Where(t => t.EventId == evt.Id && removedTeamNumbers.Contains(t.TeamNumber))
.ExecuteUpdateAsync(b => b.SetProperty(t => t.StatusId, KnownEventTeamStatuses.Dropped));

// Insert new teams
var addedTeamNumbers = apiTeams.ExceptBy(existingTeams.Select(et => et.TeamNumber), at => at.TeamNumber);
await dbContext.EventTeams.AddRangeAsync(addedTeamNumbers.Select(at => new EventTeam
{
EventId = evt.Id,
TeamNumber = at.TeamNumber,
LevelId = evt.Season.LevelId,
Notes = null,
StatusId = KnownEventTeamStatuses.NotArrived
}));
await dbContext.SaveChangesAsync();
}
}

0 comments on commit c0d085b

Please sign in to comment.