diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b57935c..ccf7214 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,8 +68,7 @@ Bug: {short description} ### C# -- Add tests for any new feature or bug fix, to ensure things continue to work. -- Early returns are great, they help reduce nesting! +- Please refer to the [style guide](https://github.com/leaderboardsgg/leaderboard-backend/wiki/Style-Guide). ### Git diff --git a/LeaderboardBackend.Test/Judgements.cs b/LeaderboardBackend.Test/Judgements.cs new file mode 100644 index 0000000..a5da48f --- /dev/null +++ b/LeaderboardBackend.Test/Judgements.cs @@ -0,0 +1,104 @@ +using LeaderboardBackend.Models.Entities; +using LeaderboardBackend.Models.Requests; +using LeaderboardBackend.Test.Lib; +using LeaderboardBackend.Test.TestApi; +using LeaderboardBackend.Test.TestApi.Extensions; +using LeaderboardBackend.Models.ViewModels; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace LeaderboardBackend.Test; + +[TestFixture] +internal class Judgements +{ + private static TestApiFactory Factory = null!; + private static TestApiClient ApiClient = null!; + private static Leaderboard DefaultLeaderboard = null!; + private static string? Jwt; + + private static readonly string ValidUsername = "Test"; + private static readonly string ValidPassword = "c00l_pAssword"; + private static readonly string ValidEmail = "test@email.com"; + + [SetUp] + public static async Task SetUp() + { + Factory = new TestApiFactory(); + ApiClient = Factory.CreateTestApiClient(); + + // Set up a default Leaderboard and a mod user for that leaderboard to use as the Jwt for tests + string adminJwt = (await ApiClient.LoginAdminUser()).Token; + User mod = await ApiClient.RegisterUser( + ValidUsername, + ValidEmail, + ValidPassword + ); + DefaultLeaderboard = await ApiClient.Post( + "/api/leaderboards", + new() + { + Body = new CreateLeaderboardRequest + { + Name = Generators.GenerateRandomString(), + Slug = Generators.GenerateRandomString(), + }, + Jwt = adminJwt, + } + ); + Modship modship = await ApiClient.Post( + "/api/modships", + new() + { + Body = new CreateModshipRequest + { + LeaderboardId = DefaultLeaderboard.Id, + UserId = mod.Id, + }, + Jwt = adminJwt, + } + ); + + Jwt = (await ApiClient.LoginUser(ValidEmail, ValidPassword)).Token; + } + + [Test] + public async Task CreateJudgement_OK() + { + Run run = await CreateRun(); + + JudgementViewModel? createdJudgement = await ApiClient.Post( + "/api/judgements", + new() + { + Body = new CreateJudgementRequest + { + RunId = run.Id, + Note = "It is a cool run", + Approved = true, + }, + Jwt = Jwt, + } + ); + + Assert.NotNull(createdJudgement); + } + + private async Task CreateRun() + { + return await ApiClient.Post( + "/api/runs", + new() + { + Body = new CreateRunRequest + { + Played = DateTime.UtcNow, + Submitted = DateTime.UtcNow, + Status = RunStatus.SUBMITTED, + }, + Jwt = Jwt, + } + ); + } +} diff --git a/LeaderboardBackend.Test/TestApi/TestApiClient.cs b/LeaderboardBackend.Test/TestApi/TestApiClient.cs index 46353f3..36cac39 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiClient.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiClient.cs @@ -20,7 +20,7 @@ internal sealed class RequestFailureException : Exception { public HttpResponseMessage Response { get; private set; } - public RequestFailureException(HttpResponseMessage response) : base("The attempted request failed") + public RequestFailureException(HttpResponseMessage response) : base($"The attempted request failed with status code {response.StatusCode}") { Response = response; } diff --git a/LeaderboardBackend/Authorization/UserTypeAuthorizationHandler.cs b/LeaderboardBackend/Authorization/UserTypeAuthorizationHandler.cs index 9c4e24e..6a34c26 100644 --- a/LeaderboardBackend/Authorization/UserTypeAuthorizationHandler.cs +++ b/LeaderboardBackend/Authorization/UserTypeAuthorizationHandler.cs @@ -12,6 +12,7 @@ public class UserTypeAuthorizationHandler : AuthorizationHandler requirement.Type switch { UserTypes.Admin => user.Admin, - UserTypes.Mod => IsMod(user), + UserTypes.Mod => user.Admin || IsMod(user), UserTypes.User => true, _ => false, }; - //private bool IsAdmin(User user) => user.Admin; - - // FIXME: Users don't get automagically populated with Modships when on creation of the latter. - private bool IsMod(User user) => user.Modships?.Count() > 0; + private bool IsMod(User user) => _modshipService.LoadUserModships(user.Id).Result.Count() > 0; private bool TryGetJwtFromHttpContext(AuthorizationHandlerContext context, out string token) { diff --git a/LeaderboardBackend/Controllers/JudgementsController.cs b/LeaderboardBackend/Controllers/JudgementsController.cs new file mode 100644 index 0000000..167a6f9 --- /dev/null +++ b/LeaderboardBackend/Controllers/JudgementsController.cs @@ -0,0 +1,95 @@ +using LeaderboardBackend.Authorization; +using LeaderboardBackend.Controllers.Annotations; +using LeaderboardBackend.Models.Entities; +using LeaderboardBackend.Models.Requests; +using LeaderboardBackend.Services; +using LeaderboardBackend.Models.ViewModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LeaderboardBackend.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Produces("application/json")] +public class JudgementsController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IJudgementService _judgementService; + private readonly IRunService _runService; + private readonly IUserService _userService; + + public JudgementsController( + ILogger logger, + IJudgementService judgementService, + IRunService runService, + IUserService userService + ) + { + _logger = logger; + _judgementService = judgementService; + _runService = runService; + _userService = userService; + } + + /// Gets a Judgement from its ID. + /// The Judgement with the provided ID. + /// If no Judgement can be found. + [ApiConventionMethod(typeof(Conventions), + nameof(Conventions.Get))] + [AllowAnonymous] + [HttpGet("{id}")] + public async Task> GetJudgement(long id) + { + Judgement? judgement = await _judgementService.GetJudgement(id); + if (judgement is null) + { + return NotFound(); + } + + return Ok(new JudgementViewModel(judgement)); + } + + /// Creates a judgement for a run. + /// The created judgement. + /// The request body is malformed. + /// For an invalid judgement. + [ApiConventionMethod(typeof(Conventions), + nameof(Conventions.Post))] + [Authorize(Policy = UserTypes.Mod)] + [HttpPost] + public async Task> CreateJudgement([FromBody] CreateJudgementRequest body) + { + User? mod = await _userService.GetUserFromClaims(HttpContext.User); + Run? run = await _runService.GetRun(body.RunId); + + if (run is null) + { + _logger.LogError($"CreateJudgement: run is null. ID = {body.RunId}"); + return NotFound($"Run not found for ID = {body.RunId}"); + } + + if (run.Status == RunStatus.CREATED) + { + _logger.LogError($"CreateJudgement: run has pending participations (i.e. run status == CREATED). ID = {body.RunId}"); + return BadRequest($"Run has pending Participations. ID = {body.RunId}"); + } + + // TODO: Update run status on body.Approved's value + Judgement judgement = new() + { + Approved = body.Approved, + Mod = mod!, + ModId = mod!.Id, + Note = body.Note, + Run = run, + RunId = run.Id, + }; + + await _judgementService.CreateJudgement(judgement); + + JudgementViewModel judgementView = new(judgement); + + return CreatedAtAction(nameof(GetJudgement), new { id = judgementView.Id }, judgementView); + } +} diff --git a/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.Designer.cs b/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.Designer.cs new file mode 100644 index 0000000..128c333 --- /dev/null +++ b/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.Designer.cs @@ -0,0 +1,444 @@ +// +using System; +using LeaderboardBackend.Models.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LeaderboardBackend.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20220508171859_Judgements_ChangeApproverFieldsToModFields")] + partial class Judgements_ChangeApproverFieldsToModFields + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannedUserId") + .HasColumnType("uuid") + .HasColumnName("banned_user_id"); + + b.Property("BanningUserId") + .HasColumnType("uuid") + .HasColumnName("banning_user_id"); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("pk_bans"); + + b.HasIndex("BannedUserId") + .HasDatabaseName("ix_bans_banned_user_id"); + + b.HasIndex("BanningUserId") + .HasDatabaseName("ix_bans_banning_user_id"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_bans_leaderboard_id"); + + b.ToTable("bans", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PlayersMax") + .HasColumnType("integer") + .HasColumnName("players_max"); + + b.Property("PlayersMin") + .HasColumnType("integer") + .HasColumnName("players_min"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_categories_leaderboard_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Approved") + .HasColumnType("boolean") + .HasColumnName("approved"); + + b.Property("ModId") + .HasColumnType("uuid") + .HasColumnName("mod_id"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("timestamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.HasKey("Id") + .HasName("pk_judgements"); + + b.HasIndex("ModId") + .HasDatabaseName("ix_judgements_mod_id"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_judgements_run_id"); + + b.ToTable("judgements", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_leaderboards"); + + b.ToTable("leaderboards", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_modships"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_modships_leaderboard_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_modships_user_id"); + + b.ToTable("modships", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text") + .HasColumnName("comment"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("RunnerId") + .HasColumnType("uuid") + .HasColumnName("runner_id"); + + b.Property("Vod") + .HasColumnType("text") + .HasColumnName("vod"); + + b.HasKey("Id") + .HasName("pk_participations"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_participations_run_id"); + + b.HasIndex("RunnerId") + .HasDatabaseName("ix_participations_runner_id"); + + b.ToTable("participations", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Played") + .HasColumnType("timestamp with time zone") + .HasColumnName("played"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Submitted") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted"); + + b.HasKey("Id") + .HasName("pk_runs"); + + b.ToTable("runs", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("About") + .HasColumnType("text") + .HasColumnName("about"); + + b.Property("Admin") + .HasColumnType("boolean") + .HasColumnName("admin"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "BannedUser") + .WithMany("BansReceived") + .HasForeignKey("BannedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_bans_users_banned_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "BanningUser") + .WithMany("BansGiven") + .HasForeignKey("BanningUserId") + .HasConstraintName("fk_bans_users_banning_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Bans") + .HasForeignKey("LeaderboardId") + .HasConstraintName("fk_bans_leaderboards_leaderboard_id"); + + b.Navigation("BannedUser"); + + b.Navigation("BanningUser"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Categories") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_leaderboards_leaderboard_id"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_users_mod_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Judgements") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_runs_run_id"); + + b.Navigation("Mod"); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Modships") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_leaderboards_leaderboard_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "User") + .WithMany("Modships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_users_user_id"); + + b.Navigation("Leaderboard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Participations") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_runs_run_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "Runner") + .WithMany("Participations") + .HasForeignKey("RunnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_users_runner_id"); + + b.Navigation("Run"); + + b.Navigation("Runner"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Navigation("Bans"); + + b.Navigation("Categories"); + + b.Navigation("Modships"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Navigation("Judgements"); + + b.Navigation("Participations"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Navigation("BansGiven"); + + b.Navigation("BansReceived"); + + b.Navigation("Modships"); + + b.Navigation("Participations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.cs b/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.cs new file mode 100644 index 0000000..5b8649c --- /dev/null +++ b/LeaderboardBackend/Migrations/20220508171859_Judgements_ChangeApproverFieldsToModFields.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LeaderboardBackend.Migrations +{ + public partial class Judgements_ChangeApproverFieldsToModFields : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_judgements_users_approver_id", + table: "judgements"); + + migrationBuilder.RenameColumn( + name: "approver_id", + table: "judgements", + newName: "mod_id"); + + migrationBuilder.RenameIndex( + name: "ix_judgements_approver_id", + table: "judgements", + newName: "ix_judgements_mod_id"); + + migrationBuilder.AddForeignKey( + name: "fk_judgements_users_mod_id", + table: "judgements", + column: "mod_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_judgements_users_mod_id", + table: "judgements"); + + migrationBuilder.RenameColumn( + name: "mod_id", + table: "judgements", + newName: "approver_id"); + + migrationBuilder.RenameIndex( + name: "ix_judgements_mod_id", + table: "judgements", + newName: "ix_judgements_approver_id"); + + migrationBuilder.AddForeignKey( + name: "fk_judgements_users_approver_id", + table: "judgements", + column: "approver_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.Designer.cs b/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.Designer.cs new file mode 100644 index 0000000..2ecced7 --- /dev/null +++ b/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.Designer.cs @@ -0,0 +1,446 @@ +// +using System; +using LeaderboardBackend.Models.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LeaderboardBackend.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20220508171950_Judgements_ChangeTimestampToCreatedAt")] + partial class Judgements_ChangeTimestampToCreatedAt + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannedUserId") + .HasColumnType("uuid") + .HasColumnName("banned_user_id"); + + b.Property("BanningUserId") + .HasColumnType("uuid") + .HasColumnName("banning_user_id"); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("pk_bans"); + + b.HasIndex("BannedUserId") + .HasDatabaseName("ix_bans_banned_user_id"); + + b.HasIndex("BanningUserId") + .HasDatabaseName("ix_bans_banning_user_id"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_bans_leaderboard_id"); + + b.ToTable("bans", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PlayersMax") + .HasColumnType("integer") + .HasColumnName("players_max"); + + b.Property("PlayersMin") + .HasColumnType("integer") + .HasColumnName("players_min"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_categories_leaderboard_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Approved") + .HasColumnType("boolean") + .HasColumnName("approved"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ModId") + .HasColumnType("uuid") + .HasColumnName("mod_id"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.HasKey("Id") + .HasName("pk_judgements"); + + b.HasIndex("ModId") + .HasDatabaseName("ix_judgements_mod_id"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_judgements_run_id"); + + b.ToTable("judgements", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_leaderboards"); + + b.ToTable("leaderboards", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_modships"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_modships_leaderboard_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_modships_user_id"); + + b.ToTable("modships", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text") + .HasColumnName("comment"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("RunnerId") + .HasColumnType("uuid") + .HasColumnName("runner_id"); + + b.Property("Vod") + .HasColumnType("text") + .HasColumnName("vod"); + + b.HasKey("Id") + .HasName("pk_participations"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_participations_run_id"); + + b.HasIndex("RunnerId") + .HasDatabaseName("ix_participations_runner_id"); + + b.ToTable("participations", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Played") + .HasColumnType("timestamp with time zone") + .HasColumnName("played"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Submitted") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted"); + + b.HasKey("Id") + .HasName("pk_runs"); + + b.ToTable("runs", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("About") + .HasColumnType("text") + .HasColumnName("about"); + + b.Property("Admin") + .HasColumnType("boolean") + .HasColumnName("admin"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "BannedUser") + .WithMany("BansReceived") + .HasForeignKey("BannedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_bans_users_banned_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "BanningUser") + .WithMany("BansGiven") + .HasForeignKey("BanningUserId") + .HasConstraintName("fk_bans_users_banning_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Bans") + .HasForeignKey("LeaderboardId") + .HasConstraintName("fk_bans_leaderboards_leaderboard_id"); + + b.Navigation("BannedUser"); + + b.Navigation("BanningUser"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Categories") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_leaderboards_leaderboard_id"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_users_mod_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Judgements") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_runs_run_id"); + + b.Navigation("Mod"); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Modships") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_leaderboards_leaderboard_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "User") + .WithMany("Modships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_users_user_id"); + + b.Navigation("Leaderboard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Participations") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_runs_run_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "Runner") + .WithMany("Participations") + .HasForeignKey("RunnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_users_runner_id"); + + b.Navigation("Run"); + + b.Navigation("Runner"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Navigation("Bans"); + + b.Navigation("Categories"); + + b.Navigation("Modships"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Navigation("Judgements"); + + b.Navigation("Participations"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Navigation("BansGiven"); + + b.Navigation("BansReceived"); + + b.Navigation("Modships"); + + b.Navigation("Participations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.cs b/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.cs new file mode 100644 index 0000000..b56f0a5 --- /dev/null +++ b/LeaderboardBackend/Migrations/20220508171950_Judgements_ChangeTimestampToCreatedAt.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LeaderboardBackend.Migrations +{ + public partial class Judgements_ChangeTimestampToCreatedAt : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "timestamp", + table: "judgements"); + + migrationBuilder.AddColumn( + name: "created_at", + table: "judgements", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "created_at", + table: "judgements"); + + migrationBuilder.AddColumn( + name: "timestamp", + table: "judgements", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + } +} diff --git a/LeaderboardBackend/Migrations/ApplicationContextModelSnapshot.cs b/LeaderboardBackend/Migrations/ApplicationContextModelSnapshot.cs index f9c8dd5..006e3ef 100644 --- a/LeaderboardBackend/Migrations/ApplicationContextModelSnapshot.cs +++ b/LeaderboardBackend/Migrations/ApplicationContextModelSnapshot.cs @@ -1,442 +1,444 @@ -// -using System; -using LeaderboardBackend.Models.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LeaderboardBackend.Migrations -{ - [DbContext(typeof(ApplicationContext))] - partial class ApplicationContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BannedUserId") - .HasColumnType("uuid") - .HasColumnName("banned_user_id"); - - b.Property("BanningUserId") - .HasColumnType("uuid") - .HasColumnName("banning_user_id"); - - b.Property("LeaderboardId") - .HasColumnType("bigint") - .HasColumnName("leaderboard_id"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("Time") - .HasColumnType("timestamp with time zone") - .HasColumnName("time"); - - b.HasKey("Id") - .HasName("pk_bans"); - - b.HasIndex("BannedUserId") - .HasDatabaseName("ix_bans_banned_user_id"); - - b.HasIndex("BanningUserId") - .HasDatabaseName("ix_bans_banning_user_id"); - - b.HasIndex("LeaderboardId") - .HasDatabaseName("ix_bans_leaderboard_id"); - - b.ToTable("bans", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("LeaderboardId") - .HasColumnType("bigint") - .HasColumnName("leaderboard_id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("PlayersMax") - .HasColumnType("integer") - .HasColumnName("players_max"); - - b.Property("PlayersMin") - .HasColumnType("integer") - .HasColumnName("players_min"); - - b.Property("Rules") - .HasColumnType("text") - .HasColumnName("rules"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("Id") - .HasName("pk_categories"); - - b.HasIndex("LeaderboardId") - .HasDatabaseName("ix_categories_leaderboard_id"); - - b.ToTable("categories", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Approved") - .HasColumnType("boolean") - .HasColumnName("approved"); - - b.Property("ApproverId") - .HasColumnType("uuid") - .HasColumnName("approver_id"); - - b.Property("Note") - .IsRequired() - .HasColumnType("text") - .HasColumnName("note"); - - b.Property("RunId") - .HasColumnType("uuid") - .HasColumnName("run_id"); - - b.Property("timestamp") - .HasColumnType("timestamp with time zone") - .HasColumnName("timestamp"); - - b.HasKey("Id") - .HasName("pk_judgements"); - - b.HasIndex("ApproverId") - .HasDatabaseName("ix_judgements_approver_id"); - - b.HasIndex("RunId") - .HasDatabaseName("ix_judgements_run_id"); - - b.ToTable("judgements", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Rules") - .HasColumnType("text") - .HasColumnName("rules"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.HasKey("Id") - .HasName("pk_leaderboards"); - - b.ToTable("leaderboards", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("LeaderboardId") - .HasColumnType("bigint") - .HasColumnName("leaderboard_id"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_modships"); - - b.HasIndex("LeaderboardId") - .HasDatabaseName("ix_modships_leaderboard_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_modships_user_id"); - - b.ToTable("modships", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Comment") - .HasColumnType("text") - .HasColumnName("comment"); - - b.Property("RunId") - .HasColumnType("uuid") - .HasColumnName("run_id"); - - b.Property("RunnerId") - .HasColumnType("uuid") - .HasColumnName("runner_id"); - - b.Property("Vod") - .HasColumnType("text") - .HasColumnName("vod"); - - b.HasKey("Id") - .HasName("pk_participations"); - - b.HasIndex("RunId") - .HasDatabaseName("ix_participations_run_id"); - - b.HasIndex("RunnerId") - .HasDatabaseName("ix_participations_runner_id"); - - b.ToTable("participations", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Played") - .HasColumnType("timestamp with time zone") - .HasColumnName("played"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("Submitted") - .HasColumnType("timestamp with time zone") - .HasColumnName("submitted"); - - b.HasKey("Id") - .HasName("pk_runs"); - - b.ToTable("runs", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("About") - .HasColumnType("text") - .HasColumnName("about"); - - b.Property("Admin") - .HasColumnType("boolean") - .HasColumnName("admin"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text") - .HasColumnName("email"); - - b.Property("Password") - .IsRequired() - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => - { - b.HasOne("LeaderboardBackend.Models.Entities.User", "BannedUser") - .WithMany("BansReceived") - .HasForeignKey("BannedUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_bans_users_banned_user_id"); - - b.HasOne("LeaderboardBackend.Models.Entities.User", "BanningUser") - .WithMany("BansGiven") - .HasForeignKey("BanningUserId") - .HasConstraintName("fk_bans_users_banning_user_id"); - - b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") - .WithMany("Bans") - .HasForeignKey("LeaderboardId") - .HasConstraintName("fk_bans_leaderboards_leaderboard_id"); - - b.Navigation("BannedUser"); - - b.Navigation("BanningUser"); - - b.Navigation("Leaderboard"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => - { - b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") - .WithMany("Categories") - .HasForeignKey("LeaderboardId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_categories_leaderboards_leaderboard_id"); - - b.Navigation("Leaderboard"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => - { - b.HasOne("LeaderboardBackend.Models.Entities.User", "Approver") - .WithMany() - .HasForeignKey("ApproverId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_judgements_users_approver_id"); - - b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") - .WithMany("Judgements") - .HasForeignKey("RunId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_judgements_runs_run_id"); - - b.Navigation("Approver"); - - b.Navigation("Run"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => - { - b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") - .WithMany("Modships") - .HasForeignKey("LeaderboardId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_modships_leaderboards_leaderboard_id"); - - b.HasOne("LeaderboardBackend.Models.Entities.User", "User") - .WithMany("Modships") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_modships_users_user_id"); - - b.Navigation("Leaderboard"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => - { - b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") - .WithMany("Participations") - .HasForeignKey("RunId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_participations_runs_run_id"); - - b.HasOne("LeaderboardBackend.Models.Entities.User", "Runner") - .WithMany("Participations") - .HasForeignKey("RunnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_participations_users_runner_id"); - - b.Navigation("Run"); - - b.Navigation("Runner"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => - { - b.Navigation("Bans"); - - b.Navigation("Categories"); - - b.Navigation("Modships"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => - { - b.Navigation("Judgements"); - - b.Navigation("Participations"); - }); - - modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => - { - b.Navigation("BansGiven"); - - b.Navigation("BansReceived"); - - b.Navigation("Modships"); - - b.Navigation("Participations"); - }); -#pragma warning restore 612, 618 - } - } -} +// +using System; +using LeaderboardBackend.Models.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LeaderboardBackend.Migrations +{ + [DbContext(typeof(ApplicationContext))] + partial class ApplicationContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BannedUserId") + .HasColumnType("uuid") + .HasColumnName("banned_user_id"); + + b.Property("BanningUserId") + .HasColumnType("uuid") + .HasColumnName("banning_user_id"); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("pk_bans"); + + b.HasIndex("BannedUserId") + .HasDatabaseName("ix_bans_banned_user_id"); + + b.HasIndex("BanningUserId") + .HasDatabaseName("ix_bans_banning_user_id"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_bans_leaderboard_id"); + + b.ToTable("bans", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PlayersMax") + .HasColumnType("integer") + .HasColumnName("players_max"); + + b.Property("PlayersMin") + .HasColumnType("integer") + .HasColumnName("players_min"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_categories"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_categories_leaderboard_id"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Approved") + .HasColumnType("boolean") + .HasColumnName("approved"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("now()"); + + b.Property("ModId") + .HasColumnType("uuid") + .HasColumnName("mod_id"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.HasKey("Id") + .HasName("pk_judgements"); + + b.HasIndex("ModId") + .HasDatabaseName("ix_judgements_mod_id"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_judgements_run_id"); + + b.ToTable("judgements", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Rules") + .HasColumnType("text") + .HasColumnName("rules"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.HasKey("Id") + .HasName("pk_leaderboards"); + + b.ToTable("leaderboards", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LeaderboardId") + .HasColumnType("bigint") + .HasColumnName("leaderboard_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_modships"); + + b.HasIndex("LeaderboardId") + .HasDatabaseName("ix_modships_leaderboard_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_modships_user_id"); + + b.ToTable("modships", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasColumnType("text") + .HasColumnName("comment"); + + b.Property("RunId") + .HasColumnType("uuid") + .HasColumnName("run_id"); + + b.Property("RunnerId") + .HasColumnType("uuid") + .HasColumnName("runner_id"); + + b.Property("Vod") + .HasColumnType("text") + .HasColumnName("vod"); + + b.HasKey("Id") + .HasName("pk_participations"); + + b.HasIndex("RunId") + .HasDatabaseName("ix_participations_run_id"); + + b.HasIndex("RunnerId") + .HasDatabaseName("ix_participations_runner_id"); + + b.ToTable("participations", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Played") + .HasColumnType("timestamp with time zone") + .HasColumnName("played"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Submitted") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted"); + + b.HasKey("Id") + .HasName("pk_runs"); + + b.ToTable("runs", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("About") + .HasColumnType("text") + .HasColumnName("about"); + + b.Property("Admin") + .HasColumnType("boolean") + .HasColumnName("admin"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Ban", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "BannedUser") + .WithMany("BansReceived") + .HasForeignKey("BannedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_bans_users_banned_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "BanningUser") + .WithMany("BansGiven") + .HasForeignKey("BanningUserId") + .HasConstraintName("fk_bans_users_banning_user_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Bans") + .HasForeignKey("LeaderboardId") + .HasConstraintName("fk_bans_leaderboards_leaderboard_id"); + + b.Navigation("BannedUser"); + + b.Navigation("BanningUser"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Category", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Categories") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_categories_leaderboards_leaderboard_id"); + + b.Navigation("Leaderboard"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Judgement", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.User", "Mod") + .WithMany() + .HasForeignKey("ModId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_users_mod_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Judgements") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_judgements_runs_run_id"); + + b.Navigation("Mod"); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Modship", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Leaderboard", "Leaderboard") + .WithMany("Modships") + .HasForeignKey("LeaderboardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_leaderboards_leaderboard_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "User") + .WithMany("Modships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_modships_users_user_id"); + + b.Navigation("Leaderboard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Participation", b => + { + b.HasOne("LeaderboardBackend.Models.Entities.Run", "Run") + .WithMany("Participations") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_runs_run_id"); + + b.HasOne("LeaderboardBackend.Models.Entities.User", "Runner") + .WithMany("Participations") + .HasForeignKey("RunnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_participations_users_runner_id"); + + b.Navigation("Run"); + + b.Navigation("Runner"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Leaderboard", b => + { + b.Navigation("Bans"); + + b.Navigation("Categories"); + + b.Navigation("Modships"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.Run", b => + { + b.Navigation("Judgements"); + + b.Navigation("Participations"); + }); + + modelBuilder.Entity("LeaderboardBackend.Models.Entities.User", b => + { + b.Navigation("BansGiven"); + + b.Navigation("BansReceived"); + + b.Navigation("Modships"); + + b.Navigation("Participations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LeaderboardBackend/Models/Annotations/NoteAttribute.cs b/LeaderboardBackend/Models/Annotations/NoteAttribute.cs new file mode 100644 index 0000000..71177d5 --- /dev/null +++ b/LeaderboardBackend/Models/Annotations/NoteAttribute.cs @@ -0,0 +1,21 @@ +using LeaderboardBackend.Models.Requests; +using System.ComponentModel.DataAnnotations; + +namespace LeaderboardBackend.Models.Annotations; + +/// Asserts that Note is non-empty for non-approval judgements (Approved is false or null). +public class NoteAttribute : ValidationAttribute { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + string note = (string)value!; + + CreateJudgementRequest request = (CreateJudgementRequest)validationContext.ObjectInstance; + + if (request.Note.Length == 0 && (request.Approved is null || request.Approved is false)) + { + return new ValidationResult("Notes must be provided with this judgement."); + } + + return ValidationResult.Success; + } +} diff --git a/LeaderboardBackend/Models/Entities/ApplicationContext.cs b/LeaderboardBackend/Models/Entities/ApplicationContext.cs index 091ae77..4b4b376 100644 --- a/LeaderboardBackend/Models/Entities/ApplicationContext.cs +++ b/LeaderboardBackend/Models/Entities/ApplicationContext.cs @@ -11,8 +11,15 @@ public ApplicationContext(DbContextOptions options) public DbSet Categories { get; set; } = null!; public DbSet Judgements { get; set; } = null!; public DbSet Leaderboards { get; set; } = null!; - public DbSet Modships { get; set; } = null!; - public DbSet Runs { get; set; } = null!; - public DbSet Participations { get; set; } = null!; + public DbSet Modships { get; set; } = null!; + public DbSet Runs { get; set; } = null!; + public DbSet Participations { get; set; } = null!; public DbSet Users { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .Property(j => j.CreatedAt) + .HasDefaultValueSql("now()"); + } } diff --git a/LeaderboardBackend/Models/Entities/Judgement.cs b/LeaderboardBackend/Models/Entities/Judgement.cs index 9623b02..4b2f2e8 100644 --- a/LeaderboardBackend/Models/Entities/Judgement.cs +++ b/LeaderboardBackend/Models/Entities/Judgement.cs @@ -1,29 +1,61 @@ using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; namespace LeaderboardBackend.Models.Entities; +/// A decision by a mod on a run submission. +/// +/// The latest judgement on a run updates its status. +/// A judgement can be one of these three types:
+/// - an approval if Approval == true;
+/// - a rejection if Approval == false; and
+/// - a comment if Approval == null.
+/// Judgements are NOT created if:
+/// - its related run has "CREATED" status;
+/// - its Note is empty while its Approved is false or null.
+/// I.e. for the second point, a mod MUST add a note if they want to reject or simply comment on a submission.
+/// Moderators CANNOT modify their judgements once made. +///
public class Judgement { + /// Generated on creation. public long Id { get; set; } + /// + /// Defines this judgement, which in turn defines the status of its related run.
+ /// If: + ///
    + ///
  • true, run is approved;
  • + ///
  • false, run is rejected;
  • + ///
  • null, run is commented on.
  • + ///
+ /// For the latter two, Note MUST be non-empty. + ///
public bool? Approved { get; set; } + /// When the judgement was made. [Required] - public DateTime timestamp { get; set; } + public DateTime CreatedAt { get; set; } + /// + /// Comments on the judgement. + /// MUST be non-empty for rejections or comments (Approved ∈ {false, null}). + /// [Required] - public string Note { get; set; } = null!; + public string Note { get; set; } = ""; + /// ID of the mod that made this judgement. [Required] - public Guid ApproverId { get; set; } + public Guid ModId { get; set; } - [JsonIgnore] - public User? Approver { get; set; } + /// Model of the mod that made this judgement. + [Required] + public User Mod { get; set; } = null!; + /// ID of the related run. [Required] public Guid RunId { get; set; } - [JsonIgnore] - public Run? Run { get; set; } = null!; + /// Model of the related run. + [Required] + public Run Run { get; set; } = null!; } diff --git a/LeaderboardBackend/Models/Requests/JudgementRequests.cs b/LeaderboardBackend/Models/Requests/JudgementRequests.cs new file mode 100644 index 0000000..ff8c5bf --- /dev/null +++ b/LeaderboardBackend/Models/Requests/JudgementRequests.cs @@ -0,0 +1,14 @@ +using LeaderboardBackend.Models.Annotations; + +namespace LeaderboardBackend.Models.Requests; + +/// Request object sent when creating a Judgement. +/// GUID of the run. +/// +/// Judgement comments. Must be provided if not outright approving a run ( is false or null). +/// Acts as mod feedback for the runner. +/// +/// +/// The judgement result. Can be true, false, or null. For the latter two, must be non-empty. +/// +public readonly record struct CreateJudgementRequest(Guid RunId, [Note] string Note, bool? Approved); diff --git a/LeaderboardBackend/Models/ViewModels/JudgementViewModel.cs b/LeaderboardBackend/Models/ViewModels/JudgementViewModel.cs new file mode 100644 index 0000000..b73e8c0 --- /dev/null +++ b/LeaderboardBackend/Models/ViewModels/JudgementViewModel.cs @@ -0,0 +1,45 @@ +using LeaderboardBackend.Models.Entities; + +namespace LeaderboardBackend.Models.ViewModels; + +/// A decision by a mod on a run submission. See Models/Entities/Judgement.cs. +public record JudgementViewModel +{ + /// The newly-made judgement's ID. + public long Id { get; set; } + + /// + /// The judgement result. Can be true, false, or null. In the latter two, Note will be non-empty. + /// + public bool? Approved { get; set; } + + /// + /// When the judgement was made. Follows RFC 3339. + /// + /// 2022-01-01T12:34:56Z / 2022-01-01T12:34:56+01:00 + public string CreatedAt { get; set; } + + /// + /// Judgement comments. Acts as mod feedback for the runner. Will be non-empty for + /// non-approval judgements (Approved is false or null). + /// + public string? Note { get; set; } + + /// ID of mod who made this judgement. + public Guid ModId { get; set; } + + /// ID of run this judgement's for. + public Guid RunId { get; set; } + + public JudgementViewModel() { } + + public JudgementViewModel(Judgement judgement) + { + Id = judgement.Id; + Approved = judgement.Approved; + CreatedAt = judgement.CreatedAt.ToLongDateString(); + Note = judgement.Note; + ModId = judgement.ModId; + RunId = judgement.RunId; + } +} diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 18f0317..f0c5bf5 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -37,6 +37,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add controllers to the container. builder.Services.AddControllers(opt => diff --git a/LeaderboardBackend/Services/IJudgementService.cs b/LeaderboardBackend/Services/IJudgementService.cs new file mode 100644 index 0000000..506a224 --- /dev/null +++ b/LeaderboardBackend/Services/IJudgementService.cs @@ -0,0 +1,9 @@ +using LeaderboardBackend.Models.Entities; + +namespace LeaderboardBackend.Services; + +public interface IJudgementService +{ + Task GetJudgement(long id); + Task CreateJudgement(Judgement judgement); +} diff --git a/LeaderboardBackend/Services/Impl/JudgementService.cs b/LeaderboardBackend/Services/Impl/JudgementService.cs new file mode 100644 index 0000000..ece04d2 --- /dev/null +++ b/LeaderboardBackend/Services/Impl/JudgementService.cs @@ -0,0 +1,24 @@ +using LeaderboardBackend.Models.Entities; + +namespace LeaderboardBackend.Services; + +public class JudgementService : IJudgementService +{ + private readonly ApplicationContext _applicationContext; + + public JudgementService(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + public async Task GetJudgement(long id) + { + return await _applicationContext.Judgements.FindAsync(id); + } + + public async Task CreateJudgement(Judgement judgement) + { + _applicationContext.Judgements.Add(judgement); + await _applicationContext.SaveChangesAsync(); + } +}