Add proper relationships to Comments (#838)

* Add specific foreign keys for comment targets

* Remove inheritdoc tags from migration

* Fix punctuation of deleted comment message and add mod deletion message

* Fix broken merge

* Cleanup comment queries
This commit is contained in:
Josh 2023-08-24 13:58:03 -05:00 committed by GitHub
parent 9cb9fb62e4
commit a316c866c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 192 additions and 45 deletions

View file

@ -64,12 +64,16 @@ public class CommentController : ControllerBase
PaginationData pageData = this.Request.GetPaginationData(); PaginationData pageData = this.Request.GetPaginationData();
IQueryable<CommentEntity> baseQuery = this.database.Comments.Where(c => c.Type == type);
if (type == CommentType.Level) if (type == CommentType.Level)
{ {
targetId = await this.database.Slots.Where(s => s.SlotId == slotId) targetId = await this.database.Slots.Where(s => s.SlotId == slotId)
.Where(s => s.CommentsEnabled && !s.Hidden) .Where(s => s.CommentsEnabled && !s.Hidden)
.Select(s => s.SlotId) .Select(s => s.SlotId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
baseQuery = baseQuery.Where(c => c.TargetSlotId == targetId);
} }
else else
{ {
@ -77,6 +81,8 @@ public class CommentController : ControllerBase
.Where(u => u.CommentsEnabled) .Where(u => u.CommentsEnabled)
.Select(u => u.UserId) .Select(u => u.UserId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
baseQuery = baseQuery.Where(c => c.TargetUserId == targetId);
} }
if (targetId == 0) return this.NotFound(); if (targetId == 0) return this.NotFound();
@ -86,11 +92,10 @@ public class CommentController : ControllerBase
where blockedProfile.UserId == token.UserId where blockedProfile.UserId == token.UserId
select blockedProfile.BlockedUserId).ToListAsync(); select blockedProfile.BlockedUserId).ToListAsync();
List<GameComment> comments = (await this.database.Comments.Where(p => p.TargetId == targetId && p.Type == type) List<GameComment> comments = (await baseQuery.OrderByDescending(c => c.Timestamp)
.OrderByDescending(p => p.Timestamp) .Where(c => !blockedUsers.Contains(c.PosterUserId))
.Where(p => !blockedUsers.Contains(p.PosterUserId))
.Include(c => c.Poster) .Include(c => c.Poster)
.Where(p => p.Poster.PermissionLevel != PermissionLevel.Banned) .Where(c => c.Poster.PermissionLevel != PermissionLevel.Banned)
.ApplyPagination(pageData) .ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(c => GameComment.CreateFromEntity(c, token.UserId)); .ToListAsync()).ToSerializableList(c => GameComment.CreateFromEntity(c, token.UserId));
@ -164,15 +169,15 @@ public class CommentController : ControllerBase
bool canDelete; bool canDelete;
if (comment.Type == CommentType.Profile) if (comment.Type == CommentType.Profile)
{ {
canDelete = comment.PosterUserId == token.UserId || comment.TargetId == token.UserId; canDelete = comment.PosterUserId == token.UserId || comment.TargetUserId == token.UserId;
} }
else else
{ {
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
if (slotId != comment.TargetId) return this.BadRequest(); if (slotId != comment.TargetSlotId) return this.BadRequest();
int slotCreator = await this.database.Slots.Where(s => s.SlotId == comment.TargetId) int slotCreator = await this.database.Slots.Where(s => s.SlotId == comment.TargetSlotId)
.Where(s => s.CommentsEnabled) .Where(s => s.CommentsEnabled)
.Select(s => s.CreatorId) .Select(s => s.CreatorId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();

View file

@ -59,13 +59,13 @@ public class ModerationRemovalController : ControllerBase
switch (comment.Type) switch (comment.Type)
{ {
case CommentType.Level: case CommentType.Level:
int slotCreatorId = await this.database.Slots.Where(s => s.SlotId == comment.TargetId) int slotCreatorId = await this.database.Slots.Where(s => s.SlotId == comment.TargetSlotId)
.Select(s => s.CreatorId) .Select(s => s.CreatorId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
canDelete = user.UserId == comment.PosterUserId || user.UserId == slotCreatorId; canDelete = user.UserId == comment.PosterUserId || user.UserId == slotCreatorId;
break; break;
case CommentType.Profile: case CommentType.Profile:
canDelete = user.UserId == comment.PosterUserId || user.UserId == comment.TargetId; canDelete = user.UserId == comment.PosterUserId || user.UserId == comment.TargetUserId;
break; break;
default: throw new ArgumentOutOfRangeException(nameof(commentId)); default: throw new ArgumentOutOfRangeException(nameof(commentId));
} }

View file

@ -57,9 +57,9 @@ public class SlotPage : BaseLayout
this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && this.Slot.CommentsEnabled; this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && this.Slot.CommentsEnabled;
if (this.CommentsEnabled) if (this.CommentsEnabled)
{ {
this.Comments = await this.Database.Comments.Include(p => p.Poster) this.Comments = await this.Database.Comments.Include(c => c.Poster)
.OrderByDescending(p => p.Timestamp) .OrderByDescending(c => c.Timestamp)
.Where(c => c.TargetId == id && c.Type == CommentType.Level) .Where(c => c.Type == CommentType.Level && c.TargetSlotId == id)
.Where(c => !blockedUsers.Contains(c.PosterUserId)) .Where(c => !blockedUsers.Contains(c.PosterUserId))
.Include(c => c.Poster) .Include(c => c.Poster)
.Where(c => c.Poster.PermissionLevel != PermissionLevel.Banned) .Where(c => c.Poster.PermissionLevel != PermissionLevel.Banned)

View file

@ -96,7 +96,7 @@ public class UserPage : BaseLayout
this.Comments = await this.Database.Comments.Include(p => p.Poster) this.Comments = await this.Database.Comments.Include(p => p.Poster)
.OrderByDescending(p => p.Timestamp) .OrderByDescending(p => p.Timestamp)
.Where(p => p.TargetId == userId && p.Type == CommentType.Profile) .Where(p => p.TargetUserId == userId && p.Type == CommentType.Profile)
.Where(p => !blockedUsers.Contains(p.PosterUserId)) .Where(p => !blockedUsers.Contains(p.PosterUserId))
.Take(50) .Take(50)
.ToDictionaryAsync(c => c, _ => (RatedCommentEntity?)null); .ToDictionaryAsync(c => c, _ => (RatedCommentEntity?)null);

View file

@ -89,17 +89,15 @@ public partial class DatabaseContext
if (await this.IsUserBlockedBy(userId, creatorId)) return false; if (await this.IsUserBlockedBy(userId, creatorId)) return false;
} }
this.Comments.Add this.Comments.Add(new CommentEntity
( {
new CommentEntity PosterUserId = userId,
{ TargetUserId = type == CommentType.Profile ? targetId : null,
PosterUserId = userId, TargetSlotId = type == CommentType.Level ? targetId : null,
TargetId = targetId, Type = type,
Type = type, Message = message,
Message = message, Timestamp = TimeHelper.TimestampMillis,
Timestamp = TimeHelper.TimestampMillis, });
}
);
await this.SaveChangesAsync(); await this.SaveChangesAsync();
return true; return true;

View file

@ -59,7 +59,6 @@ namespace ProjectLighthouse.Migrations
table: "Slots"); table: "Slots");
} }
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropColumn( migrationBuilder.DropColumn(

View file

@ -0,0 +1,46 @@
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20230714212153_AddUserIdAndSlotIdToComment")]
public partial class AddUserIdAndSlotIdToComment : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TargetSlotId",
table: "Comments",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "TargetUserId",
table: "Comments",
type: "int",
nullable: true);
// Set SlotId and UserId to null if there isn't an existing slot or user
migrationBuilder.Sql("UPDATE Comments AS c INNER JOIN Users AS u ON c.TargetId = u.UserId AND c.Type = 0 SET c.TargetUserId = u.UserId");
migrationBuilder.Sql("UPDATE Comments AS c INNER JOIN Slots AS s ON c.TargetId = s.SlotId AND c.Type = 1 SET c.TargetSlotId = s.SlotId");
// Delete rows that have null SlotIds or UserIds
migrationBuilder.Sql("DELETE FROM Comments WHERE TargetUserId IS NULL AND TargetSlotId IS NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TargetSlotId",
table: "Comments");
migrationBuilder.DropColumn(
name: "TargetUserId",
table: "Comments");
}
}
}

View file

@ -0,0 +1,72 @@
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20230714212234_AddForeignKeyConstraintToComment")]
public partial class AddForeignKeyConstraintToComment : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TargetId",
table: "Comments");
migrationBuilder.CreateIndex(
name: "IX_Comments_TargetSlotId",
table: "Comments",
column: "TargetSlotId");
migrationBuilder.CreateIndex(
name: "IX_Comments_TargetUserId",
table: "Comments",
column: "TargetUserId");
migrationBuilder.AddForeignKey(
name: "FK_Comments_Slots_TargetSlotId",
table: "Comments",
column: "TargetSlotId",
principalTable: "Slots",
principalColumn: "SlotId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Comments_Users_TargetUserId",
table: "Comments",
column: "TargetUserId",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Comments_Slots_TargetSlotId",
table: "Comments");
migrationBuilder.DropForeignKey(
name: "FK_Comments_Users_TargetUserId",
table: "Comments");
migrationBuilder.DropIndex(
name: "IX_Comments_TargetSlotId",
table: "Comments");
migrationBuilder.DropIndex(
name: "IX_Comments_TargetUserId",
table: "Comments");
migrationBuilder.AddColumn<int>(
name: "TargetId",
table: "Comments",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View file

@ -640,7 +640,10 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("PosterUserId") b.Property<int>("PosterUserId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("TargetId") b.Property<int?>("TargetSlotId")
.HasColumnType("int");
b.Property<int?>("TargetUserId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("ThumbsDown") b.Property<int>("ThumbsDown")
@ -659,6 +662,10 @@ namespace ProjectLighthouse.Migrations
b.HasIndex("PosterUserId"); b.HasIndex("PosterUserId");
b.HasIndex("TargetSlotId");
b.HasIndex("TargetUserId");
b.ToTable("Comments"); b.ToTable("Comments");
}); });
@ -1325,7 +1332,19 @@ namespace ProjectLighthouse.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "TargetSlot")
.WithMany()
.HasForeignKey("TargetSlotId");
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser")
.WithMany()
.HasForeignKey("TargetUserId");
b.Navigation("Poster"); b.Navigation("Poster");
b.Navigation("TargetSlot");
b.Navigation("TargetUser");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.LastContactEntity", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.LastContactEntity", b =>

View file

@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile;
@ -18,11 +19,23 @@ public class CommentEntity
public int PosterUserId { get; set; } public int PosterUserId { get; set; }
public int TargetId { get; set; }
[ForeignKey(nameof(PosterUserId))] [ForeignKey(nameof(PosterUserId))]
public UserEntity Poster { get; set; } public UserEntity Poster { get; set; }
public CommentType Type { get; set; }
#nullable enable
public int? TargetSlotId { get; set; }
[ForeignKey(nameof(TargetSlotId))]
public SlotEntity? TargetSlot { get; set; }
public int? TargetUserId { get; set; }
[ForeignKey(nameof(TargetUserId))]
public UserEntity? TargetUser { get; set; }
#nullable disable
public bool Deleted { get; set; } public bool Deleted { get; set; }
public string DeletedType { get; set; } public string DeletedType { get; set; }
@ -33,29 +46,25 @@ public class CommentEntity
public string Message { get; set; } public string Message { get; set; }
public CommentType Type { get; set; }
public int ThumbsUp { get; set; } public int ThumbsUp { get; set; }
public int ThumbsDown { get; set; } public int ThumbsDown { get; set; }
public string GetCommentMessage(DatabaseContext database) public string GetCommentMessage(DatabaseContext database)
{ {
if (!this.Deleted) if (!this.Deleted) return this.Message;
{
return this.Message;
}
if (this.DeletedBy == this.Poster.Username) if (this.DeletedBy == this.Poster.Username) return "This comment has been deleted by the author.";
{
return "This comment has been deleted by the author.";
}
UserEntity deletedBy = database.Users.FirstOrDefault(u => u.Username == this.DeletedBy); UserEntity deletedBy = database.Users.FirstOrDefault(u => u.Username == this.DeletedBy);
if (deletedBy != null && deletedBy.UserId == this.TargetId) if (deletedBy == null) return "This comment has been deleted.";
{
// If the owner of the comment section deletes
if (deletedBy.UserId == this.TargetUserId || deletedBy.UserId == database.Slots.Find(this.TargetSlotId)?.CreatorId)
return "This comment has been deleted by the player."; return "This comment has been deleted by the player.";
}
if (this.DeletedType == "moderator" && deletedBy.IsModerator)
return "This comment has been deleted by a moderator.";
return "This comment has been deleted."; return "This comment has been deleted.";
} }

View file

@ -37,7 +37,7 @@ public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization
var stats = await database.Slots.Where(s => s.SlotId == this.SlotId) var stats = await database.Slots.Where(s => s.SlotId == this.SlotId)
.Select(_ => new .Select(_ => new
{ {
CommentCount = database.Comments.Count(c => c.TargetId == this.SlotId && c.Type == CommentType.Level), CommentCount = database.Comments.Count(c => c.Type == CommentType.Level && c.TargetSlotId == this.SlotId),
PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId), PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId),
}) })
.OrderBy(_ => 1) .OrderBy(_ => 1)

View file

@ -168,7 +168,7 @@ public class GameUser : ILbpSerializable, INeedsPreparationForSerialization
BonusSlots = database.Users.Where(u => u.UserId == this.UserId).Select(u => u.AdminGrantedSlots).First(), BonusSlots = database.Users.Where(u => u.UserId == this.UserId).Select(u => u.AdminGrantedSlots).First(),
PlaylistCount = database.Playlists.Count(p => p.CreatorId == this.UserId), PlaylistCount = database.Playlists.Count(p => p.CreatorId == this.UserId),
ReviewCount = database.Reviews.Count(r => r.ReviewerId == this.UserId), ReviewCount = database.Reviews.Count(r => r.ReviewerId == this.UserId),
CommentCount = database.Comments.Count(c => c.TargetId == this.UserId && c.Type == CommentType.Profile), CommentCount = database.Comments.Count(c => c.TargetUserId == this.UserId),
HeartCount = database.HeartedProfiles.Count(h => h.HeartedUserId == this.UserId), HeartCount = database.HeartedProfiles.Count(h => h.HeartedUserId == this.UserId),
PhotosByMeCount = database.Photos.Count(p => p.CreatorId == this.UserId), PhotosByMeCount = database.Photos.Count(p => p.CreatorId == this.UserId),
PhotosWithMeCount = database.Photos.Include(p => p.PhotoSubjects) PhotosWithMeCount = database.Photos.Include(p => p.PhotoSubjects)

View file

@ -11,7 +11,6 @@ using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Users; using LBPUnion.ProjectLighthouse.Types.Users;
@ -241,7 +240,7 @@ public class GameUserSlot : SlotBase, INeedsPreparationForSerialization
ThumbsUp = database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1), ThumbsUp = database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1),
ThumbsDown = database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1), ThumbsDown = database.RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1),
ReviewCount = database.Reviews.Count(r => r.SlotId == this.SlotId), ReviewCount = database.Reviews.Count(r => r.SlotId == this.SlotId),
CommentCount = database.Comments.Count(c => c.TargetId == this.SlotId && c.Type == CommentType.Level), CommentCount = database.Comments.Count(c => c.TargetSlotId == this.SlotId),
PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId), PhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId),
AuthorPhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId && p.CreatorId == this.CreatorId), AuthorPhotoCount = database.Photos.Count(p => p.SlotId == this.SlotId && p.CreatorId == this.CreatorId),
HeartCount = database.HeartedLevels.Count(h => h.SlotId == this.SlotId), HeartCount = database.HeartedLevels.Count(h => h.SlotId == this.SlotId),