diff --git a/Refresh.Core/Extensions/PhotoExtensions.cs b/Refresh.Core/Extensions/PhotoExtensions.cs index 83e39d19..a5aa6d72 100644 --- a/Refresh.Core/Extensions/PhotoExtensions.cs +++ b/Refresh.Core/Extensions/PhotoExtensions.cs @@ -19,7 +19,7 @@ public static SerializedPhoto FromGamePhoto(GamePhoto photo, DataContext dataCon MediumHash = dataContext.Database.GetAssetFromHash(photo.MediumAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.MediumAsset.AssetHash, SmallHash = dataContext.Database.GetAssetFromHash(photo.SmallAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.SmallAsset.AssetHash, PlanHash = photo.PlanHash, - PhotoSubjects = new List(photo.Subjects.Count), + PhotoSubjects = [], }; foreach (GamePhotoSubject subject in photo.Subjects) diff --git a/Refresh.Database/GameDatabaseContext.Photos.cs b/Refresh.Database/GameDatabaseContext.Photos.cs index 9535abc7..1e547b1a 100644 --- a/Refresh.Database/GameDatabaseContext.Photos.cs +++ b/Refresh.Database/GameDatabaseContext.Photos.cs @@ -5,6 +5,7 @@ using Refresh.Database.Models.Photos; using Refresh.Database.Query; using Refresh.Database.Helpers; +using Bunkum.Core; namespace Refresh.Database; @@ -19,14 +20,21 @@ public partial class GameDatabaseContext // Photos .Include(p => p.Level) .Include(p => p.Level!.Publisher) .Include(p => p.Level!.Publisher!.Statistics) - .Include(p => p.Subject1User) - .Include(p => p.Subject1User!.Statistics) - .Include(p => p.Subject2User) - .Include(p => p.Subject2User!.Statistics) - .Include(p => p.Subject3User) - .Include(p => p.Subject3User!.Statistics) - .Include(p => p.Subject4User) - .Include(p => p.Subject4User!.Statistics); + .Include(p => p.Subjects.OrderBy(s => s.PlayerId)); + + private IQueryable GamePhotoSubjectsIncluded => this.GamePhotoSubjects + .Include(p => p.User) + .Include(p => p.User!.Statistics) + .Include(p => p.Photo) + .Include(p => p.Photo!.Publisher) + .Include(p => p.Photo!.Publisher.Statistics) + .Include(p => p.Photo!.Level) + .Include(p => p.Photo!.Level!.Publisher) + .Include(p => p.Photo!.Level!.Publisher!.Statistics) + .Include(p => p.Photo!.LargeAsset) + .Include(p => p.Photo!.MediumAsset) + .Include(p => p.Photo!.SmallAsset) + .Include(p => p.Photo!.Subjects.OrderBy(s => s.PlayerId)); public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjects, GameUser publisher, GameLevel? level) { @@ -47,29 +55,6 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable gameSubjects = new(subjects.Count()); - foreach (IPhotoUploadSubject subject in subjects) - { - GameUser? subjectUser = null; - - if (!string.IsNullOrEmpty(subject.Username)) - subjectUser = this.GetUserByUsername(subject.Username); - - float[] bounds = PhotoHelper.ParseBoundsList(subject.BoundsList); - - gameSubjects.Add(new GamePhotoSubject(subjectUser, subject.DisplayName, bounds)); - - if (subjectUser != null) - { - this.WriteEnsuringStatistics(subjectUser, () => - { - subjectUser.Statistics!.PhotosWithUserCount++; - }); - } - } - - newPhoto.Subjects = gameSubjects; - this.WriteEnsuringStatistics(publisher, () => { this.GamePhotos.Add(newPhoto); @@ -87,22 +72,124 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable subjectsList = subjects.ToList(); + List finalSubjectsList = []; + + // Take care of subjects after saving the photo itself to keep the photo, even if its subjects are malformed + for (int i = 0; i < subjectsList.Count; i++) + { + IPhotoUploadSubject subject = subjectsList[i]; + + float[] bounds = new float[PhotoHelper.SubjectBoundaryCount]; + try + { + bounds = PhotoHelper.ParseBoundsList(subject.BoundsList); + } + catch (Exception ex) + { + this._logger.LogWarning(BunkumCategory.UserPhotos, $"Could not parse {subject.DisplayName}'s photo bounds: {ex.GetType()} - {ex.Message}"); + } + + GameUser? subjectUser = string.IsNullOrWhiteSpace(subject.Username) ? null : this.GetUserByUsername(subject.Username); + finalSubjectsList.Add(new() + { + Photo = newPhoto, + User = subjectUser, + DisplayName = subject.DisplayName, + PlayerId = i + 1, // Player number 1 - 4 + Bounds = bounds + }); + + if (subjectUser != null) + { + this.WriteEnsuringStatistics(subjectUser, () => + { + subjectUser.Statistics!.PhotosWithUserCount++; + }); + } + } + + this.GamePhotoSubjects.AddRange(finalSubjectsList); + this.SaveChanges(); this.CreatePhotoUploadEvent(publisher, newPhoto); + + newPhoto.Subjects = finalSubjectsList.ToList(); return newPhoto; } + /// + /// Migration only!! + /// + public void MigratePhotoSubjects(GamePhoto photo, bool saveChanges) + { + List subjects = []; + +#pragma warning disable CS0618 // obsoletion + + // If DisplayName is not null, there is a subject in that spot + if (photo.Subject1DisplayName != null) + { + subjects.Add(new() + { + Photo = photo, + PlayerId = 1, + DisplayName = photo.Subject1DisplayName, + User = photo.Subject1User, + Bounds = photo.Subject1Bounds, + }); + } + + if (photo.Subject2DisplayName != null) + { + subjects.Add(new() + { + Photo = photo, + PlayerId = 2, + DisplayName = photo.Subject2DisplayName, + User = photo.Subject2User, + Bounds = photo.Subject2Bounds, + }); + } + + if (photo.Subject3DisplayName != null) + { + subjects.Add(new() + { + Photo = photo, + PlayerId = 3, + DisplayName = photo.Subject3DisplayName, + User = photo.Subject3User, + Bounds = photo.Subject3Bounds, + }); + } + + if (photo.Subject4DisplayName != null) + { + subjects.Add(new() + { + Photo = photo, + PlayerId = 4, + DisplayName = photo.Subject4DisplayName, + User = photo.Subject4User, + Bounds = photo.Subject4Bounds, + }); + } +#pragma warning restore CS0618 + + this.GamePhotoSubjects.AddRange(subjects); + if (saveChanges) this.SaveChanges(); + } + public void RemovePhoto(GamePhoto photo) { - foreach (GamePhotoSubject subject in photo.Subjects) + foreach (GameUser subjectUser in this.GetUsersInPhoto(photo).ToArray()) { - if (subject.User != null) + this.WriteEnsuringStatistics(subjectUser, () => { - this.WriteEnsuringStatistics(subject.User, () => - { - subject.User.Statistics!.PhotosWithUserCount--; - }); - } + subjectUser.Statistics!.PhotosWithUserCount--; + }); } if (photo.Level != null) @@ -124,6 +211,9 @@ public void RemovePhoto(GamePhoto photo) // Remove all events referencing the photo this.Events.RemoveRange(photoEvents); + + // Remove all subjects + this.GamePhotoSubjects.RemoveRange(s => s.PhotoId == photo.PhotoId); // Remove the photo this.GamePhotos.Remove(photo); @@ -132,6 +222,17 @@ public void RemovePhoto(GamePhoto photo) }); } + public IQueryable GetSubjectsInPhoto(GamePhoto photo) + => this.GamePhotoSubjectsIncluded + .Where(s => s.PhotoId == photo.PhotoId) + .OrderBy(s => s.PlayerId); + + public IQueryable GetUsersInPhoto(GamePhoto photo) + => this.GetSubjectsInPhoto(photo) + .Where(s => s.User != null) + .OrderBy(s => s.PlayerId) + .Select(s => s.User!); + public int GetTotalPhotoCount() => this.GamePhotos.Count(); [Pure] @@ -155,14 +256,14 @@ public int GetTotalPhotosByUser(GameUser user) [Pure] public DatabaseList GetPhotosWithUser(GameUser user, int count, int skip) => - new(this.GamePhotosIncluded - .Where(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId) + new(this.GamePhotoSubjectsIncluded + .Where(s => s.UserId == user.UserId) + .Select(s => s.Photo) .OrderByDescending(p => p.TakenAt), skip, count); [Pure] public int GetTotalPhotosWithUser(GameUser user) - => this.GamePhotos - .Count(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId); + => this.GamePhotoSubjects.Count(s => s.UserId == user.UserId); [Pure] public DatabaseList GetPhotosInLevel(GameLevel level, int count, int skip) diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 2ec99d8b..1dadca80 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -401,10 +401,11 @@ public void DeleteUser(GameUser user) user.PsnAuthenticationAllowed = false; user.RpcnAuthenticationAllowed = false; - foreach (GamePhoto photo in this.GetPhotosWithUser(user, int.MaxValue, 0).Items) - foreach (GamePhotoSubject subject in photo.Subjects.Where(s => s.User?.UserId == user.UserId)) - subject.User = null; - + foreach (GamePhotoSubject subject in this.GamePhotoSubjects.Where(s => s.UserId == user.UserId).ToList()) + { + subject.UserId = null; + } + this.FavouriteLevelRelations.RemoveRange(r => r.User == user); this.FavouriteUserRelations.RemoveRange(r => r.UserToFavourite == user); this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user); diff --git a/Refresh.Database/GameDatabaseContext.cs b/Refresh.Database/GameDatabaseContext.cs index a35eea0c..338416d5 100644 --- a/Refresh.Database/GameDatabaseContext.cs +++ b/Refresh.Database/GameDatabaseContext.cs @@ -53,6 +53,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext internal DbSet GameAssets { get; set; } internal DbSet GameNotifications { get; set; } internal DbSet GamePhotos { get; set; } + internal DbSet GamePhotoSubjects { get; set; } internal DbSet GameIpVerificationRequests { get; set; } internal DbSet GameAnnouncements { get; set; } internal DbSet QueuedRegistrations { get; set; } diff --git a/Refresh.Database/Helpers/PhotoHelper.cs b/Refresh.Database/Helpers/PhotoHelper.cs index 19db7895..f9391806 100644 --- a/Refresh.Database/Helpers/PhotoHelper.cs +++ b/Refresh.Database/Helpers/PhotoHelper.cs @@ -22,7 +22,7 @@ public static float[] ParseBoundsList(string input) string boundaryStr = boundsStr[i]; if (!float.TryParse(boundaryStr, NumberFormatInfo.InvariantInfo, out float f)) - throw new FormatException($"Boundary {boundaryStr} ({i+1}/{SubjectBoundaryCount}) is not a float"); + throw new FormatException($"Boundary '{boundaryStr}' ({i+1}/{SubjectBoundaryCount}) is not a float"); boundsParsed[i] = f; } diff --git a/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs b/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs new file mode 100644 index 00000000..687929b5 --- /dev/null +++ b/Refresh.Database/Migrations/20260309162631_SplitGamePhotoSubjects.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260309162631_SplitGamePhotoSubjects")] + public partial class SplitGamePhotoSubjects : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GamePhotoSubjects", + columns: table => new + { + PhotoId = table.Column(type: "integer", nullable: false), + PlayerId = table.Column(type: "integer", nullable: false), + UserId = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: false), + Bounds = table.Column(type: "real[]", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GamePhotoSubjects", x => new { x.PhotoId, x.PlayerId }); + table.ForeignKey( + name: "FK_GamePhotoSubjects_GamePhotos_PhotoId", + column: x => x.PhotoId, + principalTable: "GamePhotos", + principalColumn: "PhotoId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GamePhotoSubjects_GameUsers_UserId", + column: x => x.UserId, + principalTable: "GameUsers", + principalColumn: "UserId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_GamePhotoSubjects_UserId", + table: "GamePhotoSubjects", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GamePhotoSubjects"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index f57643c2..25abfcc5 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -793,6 +793,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("GamePhotos"); }); + modelBuilder.Entity("Refresh.Database.Models.Photos.GamePhotoSubject", b => + { + b.Property("PhotoId") + .HasColumnType("integer"); + + b.Property("PlayerId") + .HasColumnType("integer"); + + b.PrimitiveCollection("Bounds") + .IsRequired() + .HasColumnType("real[]"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("PhotoId", "PlayerId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePhotoSubjects"); + }); + modelBuilder.Entity("Refresh.Database.Models.Playlists.GamePlaylist", b => { b.Property("PlaylistId") @@ -2051,6 +2077,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Subject4User"); }); + modelBuilder.Entity("Refresh.Database.Models.Photos.GamePhotoSubject", b => + { + b.HasOne("Refresh.Database.Models.Photos.GamePhoto", "Photo") + .WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refresh.Database.Models.Users.GameUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Photo"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Refresh.Database.Models.Playlists.GamePlaylist", b => { b.HasOne("Refresh.Database.Models.Users.GameUser", "Publisher") diff --git a/Refresh.Database/Models/Photos/GamePhoto.cs b/Refresh.Database/Models/Photos/GamePhoto.cs index ff8741d11..d33115e2 100644 --- a/Refresh.Database/Models/Photos/GamePhoto.cs +++ b/Refresh.Database/Models/Photos/GamePhoto.cs @@ -43,118 +43,36 @@ public partial class GamePhoto : ISequentialId public string PlanHash { get; set; } #region Subjects - - [NotMapped] - public IReadOnlyList Subjects - { - get - { - List subjects = new(4); - - if (this.Subject1DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject1User, this.Subject1DisplayName, this.Subject1Bounds)); - else return subjects; - - if (this.Subject2DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject2User, this.Subject2DisplayName, this.Subject2Bounds)); - else return subjects; - - if (this.Subject3DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject3User, this.Subject3DisplayName, this.Subject3Bounds)); - else return subjects; - - if (this.Subject4DisplayName != null) - subjects.Add(new GamePhotoSubject(this.Subject4User, this.Subject4DisplayName, this.Subject4Bounds)); - - return subjects; - } - set - { - if (value.Count > 4) throw new InvalidOperationException("Too many subjects. Should be caught beforehand by input validation"); - this.ClearSubjects(); - - if (value.Count >= 1) - { - this.Subject1User = value[0].User; - this.Subject1DisplayName = value[0].DisplayName; - foreach (float bound in value[0].Bounds) - this.Subject1Bounds.Add(bound); - } - - if (value.Count >= 2) - { - this.Subject2User = value[1].User; - this.Subject2DisplayName = value[1].DisplayName; - foreach (float bound in value[1].Bounds) - this.Subject2Bounds.Add(bound); - } - - if (value.Count >= 3) - { - this.Subject3User = value[2].User; - this.Subject3DisplayName = value[2].DisplayName; - foreach (float bound in value[2].Bounds) - this.Subject3Bounds.Add(bound); - } - - if (value.Count >= 4) - { - this.Subject4User = value[3].User; - this.Subject4DisplayName = value[3].DisplayName; - foreach (float bound in value[3].Bounds) - this.Subject4Bounds.Add(bound); - } - } - } + /// + /// A list of subjects, initialized using Include() + /// + public List Subjects { get; set; } - private void ClearSubjects() - { - this.Subject1User = null; - this.Subject1DisplayName = null; - this.Subject1Bounds.Clear(); - - this.Subject2User = null; - this.Subject2DisplayName = null; - this.Subject2Bounds.Clear(); - - this.Subject3User = null; - this.Subject3DisplayName = null; - this.Subject3Bounds.Clear(); - - this.Subject4User = null; - this.Subject4DisplayName = null; - this.Subject4Bounds.Clear(); - } - -#nullable enable - #pragma warning disable CS8618 // realm forces us to have a non-nullable IList so we have to have these shenanigans + #nullable restore - public ObjectId? Subject1UserId { get; set; } - [ForeignKey(nameof(Subject1UserId))] - public GameUser? Subject1User { get; set; } - public string? Subject1DisplayName { get; set; } + private const string subjectObsoleteMessage = "GamePhotoSubjects are now in their own DB table. This attribute only exists to allow migration."; + + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject1UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject1User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject1DisplayName { get; set; } - public ObjectId? Subject2UserId { get; set; } - [ForeignKey(nameof(Subject2UserId))] - public GameUser? Subject2User { get; set; } - public string? Subject2DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject2UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject2User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject2DisplayName { get; set; } - public ObjectId? Subject3UserId { get; set; } - [ForeignKey(nameof(Subject3UserId))] - public GameUser? Subject3User { get; set; } - public string? Subject3DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject3UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject3User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject3DisplayName { get; set; } - public ObjectId? Subject4UserId { get; set; } - [ForeignKey(nameof(Subject4UserId))] - public GameUser? Subject4User { get; set; } - public string? Subject4DisplayName { get; set; } + [Obsolete(subjectObsoleteMessage)] public ObjectId? Subject4UserId { get; set; } + [Obsolete(subjectObsoleteMessage)] public GameUser? Subject4User { get; set; } + [Obsolete(subjectObsoleteMessage)] public string? Subject4DisplayName { get; set; } - public List Subject1Bounds { get; set; } = []; - public List Subject2Bounds { get; set; } = []; - public List Subject3Bounds { get; set; } = []; - public List Subject4Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject1Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject2Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject3Bounds { get; set; } = []; + [Obsolete(subjectObsoleteMessage)] public List Subject4Bounds { get; set; } = []; - #pragma warning restore CS8618 #nullable disable #endregion diff --git a/Refresh.Database/Models/Photos/GamePhotoSubject.cs b/Refresh.Database/Models/Photos/GamePhotoSubject.cs index 229a2792..124203fe 100644 --- a/Refresh.Database/Models/Photos/GamePhotoSubject.cs +++ b/Refresh.Database/Models/Photos/GamePhotoSubject.cs @@ -1,25 +1,22 @@ -using System.Xml.Serialization; +using Refresh.Database.Helpers; using Refresh.Database.Models.Users; +using MongoDB.Bson; namespace Refresh.Database.Models.Photos; -[XmlRoot("subject")] -[XmlType("subject")] +[PrimaryKey(nameof(PhotoId), nameof(PlayerId))] public class GamePhotoSubject { -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - [Obsolete("used for serialization. XML stuff should be moved to SerializedGamePhotoSubject", true)] - public GamePhotoSubject() {} + [ForeignKey(nameof(UserId))] + public GameUser? User { get; set; } + public ObjectId? UserId { get; set; } - public GamePhotoSubject(GameUser? user, string displayName, IList bounds) - { - this.User = user; - this.DisplayName = displayName; - this.Bounds = bounds; - } + [ForeignKey(nameof(PhotoId))] + [Required] public GamePhoto Photo { get; set; } = null!; + [Required] public int PhotoId { get; set; } - public GameUser? User { get; set; } - - public string DisplayName { get; set; } - public IList Bounds { get; } + [Required] public int PlayerId { get; set; } // player number + + public string DisplayName { get; set; } = ""; + public IList Bounds { get; set; } = new float[PhotoHelper.SubjectBoundaryCount]; } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs new file mode 100644 index 00000000..d895199d --- /dev/null +++ b/Refresh.Interfaces.Workers/Migrations/MoveSubjectsOutOfGamePhotosMigration.cs @@ -0,0 +1,22 @@ +using Refresh.Database.Models.Photos; +using Refresh.Workers; + +namespace Refresh.Interfaces.Workers.Migrations; + +public class MoveSubjectsOutOfGamePhotosMigration : MigrationJob +{ + protected override IQueryable SortAndFilter(IQueryable query) + { + return query.OrderBy(p => p.PhotoId); + } + + protected override void Migrate(WorkContext context, GamePhoto[] batch) + { + foreach (GamePhoto photo in batch) + { + context.Database.MigratePhotoSubjects(photo, false); + } + + context.Database.SaveChanges(); + } +} \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs index 61088c8a..b5307090 100644 --- a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs +++ b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs @@ -27,6 +27,7 @@ public static WorkerManager Create(Logger logger, IDataStore dataStore, GameData manager.AddJob(); manager.AddJob(); manager.AddJob(); + manager.AddJob(); return manager; } diff --git a/RefreshTests.GameServer/TestContext.cs b/RefreshTests.GameServer/TestContext.cs index ac81046d..852b9dde 100644 --- a/RefreshTests.GameServer/TestContext.cs +++ b/RefreshTests.GameServer/TestContext.cs @@ -145,7 +145,7 @@ public GameLevel CreateLevel(GameUser author, string title, string description, return level; } - public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameLevel? level = null) + public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameLevel? level = null, List? subjects = null) { // TODO: Return newly created GamePhoto if (this.Database.GetAssetFromHash(imageHash) == null) @@ -164,7 +164,7 @@ public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameL LargeHash = imageHash, }; - IEnumerable subjects = + subjects ??= [ new() { diff --git a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs index f6b406f0..d392a304 100644 --- a/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs @@ -157,15 +157,17 @@ public void UploadPhotoAndValidateAttributes() Assert.That(gamePhoto!.LargeAssetHash, Is.EqualTo(TEST_ASSET_HASH)); Assert.That(gamePhoto!.PlanHash, Is.EqualTo(TEST_ASSET_HASH)); - Assert.That(gamePhoto!.Subjects.Count, Is.EqualTo(2)); - Assert.That(gamePhoto!.Subjects[0].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("1,1,1,1"))); - Assert.That(gamePhoto!.Subjects[0].DisplayName, Is.EqualTo(user.Username)); - Assert.That(gamePhoto!.Subjects[0].User, Is.Not.Null); - Assert.That(gamePhoto!.Subjects[0].User!.UserId, Is.EqualTo(user.UserId)); - - Assert.That(gamePhoto!.Subjects[1].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("2,4,5,6"))); - Assert.That(gamePhoto!.Subjects[1].DisplayName, Is.EqualTo("SecretAlt")); - Assert.That(gamePhoto!.Subjects[1].User, Is.Null); + List subjects = context.Database.GetSubjectsInPhoto(gamePhoto).ToList(); + + Assert.That(subjects.Count, Is.EqualTo(2)); + Assert.That(subjects[0].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("1,1,1,1"))); + Assert.That(subjects[0].DisplayName, Is.EqualTo(user.Username)); + Assert.That(subjects[0].User, Is.Not.Null); + Assert.That(subjects[0].User!.UserId, Is.EqualTo(user.UserId)); + + Assert.That(subjects[1].Bounds, Is.EqualTo(PhotoHelper.ParseBoundsList("2,4,5,6"))); + Assert.That(subjects[1].DisplayName, Is.EqualTo("SecretAlt")); + Assert.That(subjects[1].User, Is.Null); } [Test] diff --git a/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs b/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs new file mode 100644 index 00000000..7da190f8 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Photos/PhotoTests.cs @@ -0,0 +1,124 @@ +using Refresh.Database.Helpers; +using Refresh.Database.Models.Levels; +using Refresh.Database.Models.Photos; +using Refresh.Database.Models.Users; + +namespace RefreshTests.GameServer.Tests.Photos; + +public class PhotoTests : GameServerTest +{ + private const string TEST_IMAGE_HASH = "0ec63b140374ba704a58fa0c743cb357683313dd"; + + [Test] + public void CreateAndGetPhotoWithSubjects() + { + using TestContext context = this.GetServer(); + GameUser publisher = context.CreateUser(); + GameUser player2 = context.CreateUser(); + GamePhoto createdPhoto = context.CreatePhotoWithSubject(publisher, TEST_IMAGE_HASH, subjects: + [ + new() + { + Username = publisher.Username, + DisplayName = publisher.Username, + BoundsList = "1,2,3,4", + }, + new() + { + Username = player2.Username, + DisplayName = player2.Username, + BoundsList = "4,5,6,7", + } + ]); + + List separateSubjects = context.Database.GetSubjectsInPhoto(createdPhoto).ToList(); + GamePhoto? fetchedPhoto = context.Database.GetRecentPhotos(10, 0).Items.FirstOrDefault(); + Assert.That(fetchedPhoto, Is.Not.Null); + Assert.That(fetchedPhoto!.PhotoId, Is.EqualTo(createdPhoto.PhotoId)); + + // these 3 subject lists are fetched/created in different ways, so ensure they all contain the correct data + Assert.That(separateSubjects.Count, Is.EqualTo(2)); + Assert.That(createdPhoto.Subjects.Count, Is.EqualTo(2)); + Assert.That(fetchedPhoto.Subjects.Count, Is.EqualTo(2)); + + // Assert first subject on all lists + Assert.That(separateSubjects[0].PlayerId, Is.EqualTo(1)); + Assert.That(createdPhoto.Subjects[0].PlayerId, Is.EqualTo(1)); + Assert.That(fetchedPhoto.Subjects[0].PlayerId, Is.EqualTo(1)); + + Assert.That(separateSubjects[0].DisplayName, Is.EqualTo(publisher.Username)); + Assert.That(createdPhoto.Subjects[0].DisplayName, Is.EqualTo(publisher.Username)); + Assert.That(fetchedPhoto.Subjects[0].DisplayName, Is.EqualTo(publisher.Username)); + + Assert.That(separateSubjects[0].User, Is.Not.Null); + Assert.That(createdPhoto.Subjects[0].User, Is.Not.Null); + Assert.That(fetchedPhoto.Subjects[0].User, Is.Not.Null); + + Assert.That(separateSubjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + Assert.That(createdPhoto.Subjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + Assert.That(fetchedPhoto.Subjects[0].User!.UserId, Is.EqualTo(publisher.UserId)); + + float[] subject1Bounds = PhotoHelper.ParseBoundsList("1,2,3,4"); + Assert.That(separateSubjects[0].Bounds, Is.EqualTo(subject1Bounds)); + Assert.That(createdPhoto.Subjects[0].Bounds, Is.EqualTo(subject1Bounds)); + Assert.That(fetchedPhoto.Subjects[0].Bounds, Is.EqualTo(subject1Bounds)); + + // Assert second subject on all lists + Assert.That(separateSubjects[1].PlayerId, Is.EqualTo(2)); + Assert.That(createdPhoto.Subjects[1].PlayerId, Is.EqualTo(2)); + Assert.That(fetchedPhoto.Subjects[1].PlayerId, Is.EqualTo(2)); + + Assert.That(separateSubjects[1].DisplayName, Is.EqualTo(player2.Username)); + Assert.That(createdPhoto.Subjects[1].DisplayName, Is.EqualTo(player2.Username)); + Assert.That(fetchedPhoto.Subjects[1].DisplayName, Is.EqualTo(player2.Username)); + + Assert.That(separateSubjects[1].User, Is.Not.Null); + Assert.That(createdPhoto.Subjects[1].User, Is.Not.Null); + Assert.That(fetchedPhoto.Subjects[1].User, Is.Not.Null); + + Assert.That(separateSubjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + Assert.That(createdPhoto.Subjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + Assert.That(fetchedPhoto.Subjects[1].User!.UserId, Is.EqualTo(player2.UserId)); + + float[] subject2Bounds = PhotoHelper.ParseBoundsList("4,5,6,7"); + Assert.That(separateSubjects[1].Bounds, Is.EqualTo(subject2Bounds)); + Assert.That(createdPhoto.Subjects[1].Bounds, Is.EqualTo(subject2Bounds)); + Assert.That(fetchedPhoto.Subjects[1].Bounds, Is.EqualTo(subject2Bounds)); + } + + [Test] + public void DeletePhotoAndSubjectsWhenDeletingUser() + { + using TestContext context = this.GetServer(); + GameUser publisher = context.CreateUser(); + GameUser subject = context.CreateUser(); + GamePhoto photo = context.CreatePhotoWithSubject(publisher, TEST_IMAGE_HASH, subjects: + [ + new() + { + Username = publisher.Username, + DisplayName = publisher.Username, + BoundsList = "1,2,3,4", + }, + new() + { + Username = subject.Username, + DisplayName = subject.Username, + BoundsList = "4,5,6,7", + } + ]); + + // Initial subjects checks + List subjects = context.Database.GetSubjectsInPhoto(photo).ToList(); + Assert.That(subjects.Count, Is.EqualTo(2)); + Assert.That(photo.Subjects.Count, Is.EqualTo(2)); + + // Delete publisher and re-check + context.Database.DeleteUser(publisher); + context.Database.Refresh(); + + // GetSubjectsInPhoto only compares the photo IDs, so if the subjects weren't cascade-deleted, they should still be found + subjects = context.Database.GetSubjectsInPhoto(photo).ToList(); + Assert.That(subjects.Count, Is.Zero); + } +} \ No newline at end of file