Skip to content
Merged
49 changes: 36 additions & 13 deletions Refresh.Database/GameDatabaseContext.Contests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,30 @@ namespace Refresh.Database;

public partial class GameDatabaseContext // Contests
{
public void CreateContest(GameContest contest)
public GameContest CreateContest(string contestId, ICreateContestInfo createInfo, GameUser organizer, GameLevel? templateLevel = null)
{
if (this.GetContestById(contest.ContestId) != null) throw new InvalidOperationException("Contest already exists.");
if (this.GetContestById(contestId) != null) throw new InvalidOperationException("Contest already exists.");

this.Write(() =>
GameContest contest = new()
{
contest.CreationDate = this._time.Now;
this.GameContests.Add(contest);
});
ContestId = contestId,
Organizer = organizer,
CreationDate = this._time.Now,
StartDate = createInfo.StartDate!.Value,
EndDate = createInfo.EndDate!.Value,
ContestTitle = createInfo.ContestTitle!,
BannerUrl = createInfo.BannerUrl ?? "",
ContestTag = createInfo.ContestTag ?? $"#{contestId}",
ContestSummary = createInfo.ContestSummary ?? "",
ContestDetails = createInfo.ContestDetails ?? "",
ContestTheme = createInfo.ContestTheme ?? "",
AllowedGames = createInfo.AllowedGames ?? [],
TemplateLevel = templateLevel
};

this.GameContests.Add(contest);
this.SaveChanges();
return contest;
}

public void DeleteContest(GameContest contest)
Expand Down Expand Up @@ -57,7 +72,7 @@ public IEnumerable<GameContest> GetAllContests()
.FirstOrDefault();
}

public GameContest UpdateContest(ICreateContestInfo body, GameContest contest, GameUser? newOrganizer = null)
public GameContest UpdateContest(ICreateContestInfo body, GameContest contest, GameUser? newOrganizer = null, GameLevel? newTemplate = null)
{
this.Write(() =>
{
Expand All @@ -82,8 +97,9 @@ public GameContest UpdateContest(ICreateContestInfo body, GameContest contest, G
contest.ContestTheme = body.ContestTheme;
if (body.AllowedGames != null)
contest.AllowedGames = body.AllowedGames;
if (body.TemplateLevelId != null)
contest.TemplateLevel = this.GetLevelById((int)body.TemplateLevelId);

if (newTemplate != null)
contest.TemplateLevel = newTemplate;
});

return contest;
Expand All @@ -92,10 +108,17 @@ public GameContest UpdateContest(ICreateContestInfo body, GameContest contest, G
[Pure]
public DatabaseList<GameLevel> GetLevelsFromContest(GameContest contest, int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings)
{
return new DatabaseList<GameLevel>(((IQueryable<GameLevel>)this.GetLevelsByGameVersion(levelFilterSettings.GameVersion)).FilterByLevelFilterSettings(user, levelFilterSettings)
IQueryable<GameLevel> levels = this.GetLevelsByGameVersion(levelFilterSettings.GameVersion)
.FilterByLevelFilterSettings(user, levelFilterSettings)
.Where(l => l.Title.Contains(contest.ContestTag))
.Where(l => l.PublishDate >= contest.StartDate && l.PublishDate < contest.EndDate)
.AsEnumerable() // This shouldn't be a noticeable performance hit, since levels that aren't for the contest have already been filtered out
.Where(l => contest.AllowedGames.Contains(l.GameVersion)), skip, count);
.Where(l => l.PublishDate >= contest.StartDate && l.PublishDate < contest.EndDate);

// Allow levels from all games if there are no allowed games specified
if (contest.AllowedGames.Count > 0)
{
levels = levels.Where(l => contest.AllowedGames.Contains(l.GameVersion));
}

return new(levels.ToArray(), skip, count);
}
}
11 changes: 10 additions & 1 deletion Refresh.Database/Models/Contests/GameContest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ public partial class GameContest

[CanBeNull] public string ContestTheme { get; set; }

/// <summary>
/// Specifies what games the submitted levels must be made in/published from.
/// If empty, all games will be allowed.
/// </summary>
public IList<TokenGame> AllowedGames { get; set; } = [];

[CanBeNull] public GameLevel TemplateLevel { get; set; }
#nullable restore

/// <summary>
/// Optional template level, useful to see what the organizer expects.
/// </summary>
public GameLevel? TemplateLevel { get; set; }
}
2 changes: 0 additions & 2 deletions Refresh.Database/Query/ICreateContestInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace Refresh.Database.Query;

public interface ICreateContestInfo
{
string? OrganizerId { get; set; }
DateTimeOffset? StartDate { get; set; }
DateTimeOffset? EndDate { get; set; }
string? ContestTag { get; set; }
Expand All @@ -14,5 +13,4 @@ public interface ICreateContestInfo
string? ContestDetails { get; set; }
string? ContestTheme { get; set; }
TokenGame[]? AllowedGames { get; set; }
int? TemplateLevelId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Contests;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
Expand All @@ -18,42 +19,39 @@ namespace Refresh.Interfaces.APIv3.Endpoints.Admin;
public class AdminContestApiEndpoints : EndpointGroup
{
[ApiV3Endpoint("admin/contests/{id}", HttpMethods.Post), MinimumRole(GameUserRole.Admin)]
[DocSummary("Creates a contest.")]
[DocSummary("Creates a contest. You must at least include the contest's title, aswell as its start and end date.")]
[DocError(typeof(ApiValidationError), ApiValidationError.ResourceExistsErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.ObjectIdParseErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.ContestOrganizerIdParseErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ContestOrganizerMissingErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.TemplateLevelMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.ContestDataMissingErrorWhen)]
public ApiResponse<ApiContestResponse> CreateContest(RequestContext context, GameDatabaseContext database,
ApiContestRequest body, string id, DataContext dataContext)
{
if (database.GetContestById(id) != null)
return ApiValidationError.ResourceExistsError;

bool parsed = ObjectId.TryParse(body.OrganizerId, out ObjectId organizerId);
if (!parsed)
return ApiValidationError.ObjectIdParseError;
bool organizerParsed = ObjectId.TryParse(body.OrganizerId, out ObjectId organizerId);
if (!organizerParsed)
return ApiValidationError.ContestOrganizerIdParseError;

GameUser? organizer = database.GetUserByObjectId(organizerId);
if (organizer == null)
return ApiNotFoundError.UserMissingError;

GameContest contest = new()
return ApiNotFoundError.ContestOrganizerMissingError;

// Allow template level to either be unspecified or valid, but not an ID which is invalid
GameLevel? templateLevel = null;
if (body.TemplateLevelId != null)
{
ContestId = id,
Organizer = organizer,
BannerUrl = body.BannerUrl,
ContestTitle = body.ContestTitle,
ContestSummary = body.ContestSummary,
ContestTag = body.ContestTag,
ContestDetails = body.ContestDetails,
StartDate = body.StartDate!.Value,
EndDate = body.EndDate!.Value,
ContestTheme = body.ContestTheme,
AllowedGames = body.AllowedGames,
TemplateLevel = body.TemplateLevelId != null ? database.GetLevelById((int)body.TemplateLevelId) : null,
};

database.CreateContest(contest);

templateLevel = database.GetLevelById(body.TemplateLevelId.Value);
if (templateLevel == null)
return ApiNotFoundError.TemplateLevelMissingError;
}

if (body.StartDate == null || body.EndDate == null || body.ContestTitle == null)
return ApiValidationError.ContestDataMissingError;

GameContest contest = database.CreateContest(id, body, organizer, templateLevel);
return ApiContestResponse.FromOld(contest, dataContext);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public class ApiNotFoundError : ApiError
public const string ContestMissingErrorWhen = "The contest could not be found";
public static readonly ApiNotFoundError ContestMissingError = new(ContestMissingErrorWhen);

public const string ContestOrganizerMissingErrorWhen = "The contest organizer could not be found";
public static readonly ApiNotFoundError ContestOrganizerMissingError = new(ContestOrganizerMissingErrorWhen);

public const string TemplateLevelMissingErrorWhen = "The template level specified by ID could not be found";
public static readonly ApiNotFoundError TemplateLevelMissingError = new(TemplateLevelMissingErrorWhen);

public const string VerifiedIpMissingErrorWhen = "The verified IP could not be found";
public static readonly ApiNotFoundError VerifiedIpMissingError = new(VerifiedIpMissingErrorWhen);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public class ApiValidationError : ApiError

public const string BadUserLookupIdTypeWhen = "The ID type used to specify the user is not supported";
public static readonly ApiValidationError BadUserLookupIdType = new(BadUserLookupIdTypeWhen);

public const string ContestOrganizerIdParseErrorWhen = "The organizer's user ID could not be parsed by the server";
public static readonly ApiValidationError ContestOrganizerIdParseError = new(ContestOrganizerIdParseErrorWhen);

public const string ContestDataMissingErrorWhen = "The contest must at least have a title, aswell as a start and end date specified";
public static readonly ApiValidationError ContestDataMissingError = new(ContestDataMissingErrorWhen);

public ApiValidationError(string message) : base(message) {}
}
}
39 changes: 28 additions & 11 deletions Refresh.Interfaces.APIv3/Endpoints/ContestApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Contests;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
Expand Down Expand Up @@ -41,6 +42,9 @@ public ApiResponse<ApiContestResponse> GetContest(RequestContext context, GameDa
[DocSummary("Allows an admin/organizer to update their contest details")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ContestMissingErrorWhen)]
[DocError(typeof(ApiAuthenticationError), ApiAuthenticationError.NoPermissionsForObjectWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.ContestOrganizerIdParseErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.ContestOrganizerMissingErrorWhen)]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.TemplateLevelMissingErrorWhen)]
public ApiResponse<ApiContestResponse> UpdateContest(RequestContext context, GameDatabaseContext database,
string id, ApiContestRequest body, GameUser user, DataContext dataContext)
{
Expand All @@ -50,16 +54,29 @@ public ApiResponse<ApiContestResponse> UpdateContest(RequestContext context, Gam
if (!Equals(user, contest.Organizer) && user.Role != GameUserRole.Admin)
return ApiAuthenticationError.NoPermissionsForObject;

bool parsed = ObjectId.TryParse(body.OrganizerId, out ObjectId organizerId);
if (!parsed)
return ApiValidationError.ObjectIdParseError;

GameUser? organizer = database.GetUserByObjectId(organizerId);
if (organizer == null)
return ApiNotFoundError.UserMissingError;

database.UpdateContest(body, contest, organizer);

return ApiContestResponse.FromOld(contest, dataContext);
GameUser? newOrganizer = null;
if (body.OrganizerId != null)
{
bool organizerParsed = ObjectId.TryParse(body.OrganizerId, out ObjectId organizerId);
if (!organizerParsed)
return ApiValidationError.ContestOrganizerIdParseError;

GameUser? organizer = database.GetUserByObjectId(organizerId);
if (organizer == null)
return ApiNotFoundError.ContestOrganizerMissingError;

newOrganizer = database.GetUserByObjectId(organizerId);
}

GameLevel? newTemplate = null;
if (body.TemplateLevelId != null)
{
newTemplate = database.GetLevelById(body.TemplateLevelId.Value);
if (newTemplate == null)
return ApiNotFoundError.TemplateLevelMissingError;
}

GameContest updatedContest = database.UpdateContest(body, contest, newOrganizer, newTemplate);
return ApiContestResponse.FromOld(updatedContest, dataContext);
}
}
Loading