From 24695583b3f862488044d0dac42a1150cad4acc2 Mon Sep 17 00:00:00 2001 From: Hipposgrumm <60556345+Hipposgrumm@users.noreply.github.com> Date: Sat, 8 Feb 2025 02:19:43 -0700 Subject: [PATCH] Rating for Pods, Games, and (probably) other stuff (#16) Implements the rating system. Shipwreck Lagoon tracks theoretically work, but there's currently no way of testing them. There is a hack to make the ranked pods section of the blaster party board work. Don't know if that'll cause any issues (but I don't think so). SQLite database schema changes: ```sql CREATE TABLE "RatingRanks" ( "Id" INTEGER NOT NULL, "CategoryID" INTEGER NOT NULL, "RatedEntityID" INTEGER, "RatedUserID" TEXT, "Rank" INTEGER NOT NULL, "RatingAverage" REAL NOT NULL, "UpdateDate" TEXT NOT NULL, CONSTRAINT "PK_RatingRanks" PRIMARY KEY("Id" AUTOINCREMENT) ); CREATE TABLE "Ratings" ( "Id" INTEGER NOT NULL, "VikingId" INTEGER NOT NULL, "RankId" INTEGER NOT NULL, "Value" INTEGER NOT NULL, "Date" TEXT NOT NULL, CONSTRAINT "FK_Ratings_RatingRanks_RankId" FOREIGN KEY("RankId") REFERENCES "RatingRanks"("Id") ON DELETE CASCADE, CONSTRAINT "PK_Ratings" PRIMARY KEY("Id" AUTOINCREMENT), CONSTRAINT "FK_Ratings_Vikings_VikingId" FOREIGN KEY("VikingId") REFERENCES "Vikings"("Id") ON DELETE CASCADE ); ``` --------- Co-authored-by: Robert Paciorek --- src/Controllers/Common/RatingController.cs | 164 +++++++++++++++++++++ src/Model/DBContext.cs | 17 +++ src/Model/Rating.cs | 17 +++ src/Model/RatingRank.cs | 18 +++ src/Model/Viking.cs | 1 + src/Schema/ArrayOfUserRatingRankInfo.cs | 10 ++ src/Schema/RatingInfo.cs | 25 ++++ src/Schema/RatingRankInfo.cs | 41 ++++++ src/Schema/UserRatingRankInfo.cs | 13 ++ 9 files changed, 306 insertions(+) create mode 100644 src/Model/Rating.cs create mode 100644 src/Model/RatingRank.cs create mode 100644 src/Schema/ArrayOfUserRatingRankInfo.cs create mode 100644 src/Schema/RatingInfo.cs create mode 100644 src/Schema/RatingRankInfo.cs create mode 100644 src/Schema/UserRatingRankInfo.cs diff --git a/src/Controllers/Common/RatingController.cs b/src/Controllers/Common/RatingController.cs index cc945ef..b20fbc5 100644 --- a/src/Controllers/Common/RatingController.cs +++ b/src/Controllers/Common/RatingController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using sodoff.Attributes; using sodoff.Model; using sodoff.Schema; using sodoff.Util; @@ -8,6 +9,12 @@ namespace sodoff.Controllers.Common; public class RatingController : Controller { + private readonly DBContext ctx; + + public RatingController(DBContext ctx) { + this.ctx = ctx; + } + [HttpPost] [Produces("application/xml")] [Route("MissionWebService.asmx/GetPayout")] // used by World Of Jumpstart @@ -127,4 +134,161 @@ public class RatingController : Controller return Ok(5); } + // This method is the only thing that adds ratings. + private RatingInfo SetRating(Viking viking, int category, int? eID, string? uID, int value) { + RatingRank? rank; + Rating? rating = viking.Ratings.FirstOrDefault( + r => category == r.Rank.CategoryID && r.Rank.RatedEntityID == eID && r.Rank.RatedUserID == uID + ); + if (rating == null) { + rank = ctx.RatingRanks.FirstOrDefault(rr => rr.CategoryID == category && rr.RatedEntityID == eID && rr.RatedUserID == uID); + if (rank == null) { + rank = new RatingRank { + CategoryID = category, + RatedEntityID = eID, + RatedUserID = uID, + Rank = 0, + Ratings = new List() + }; + ctx.RatingRanks.Add(rank); + } + rating = new Rating { + VikingId = viking.Id, + Rank = rank + }; + ctx.Ratings.Add(rating); + } else { + rank = rating.Rank; + } + + rating.Value = value; + rank.RatingAverage = 0; + foreach (Rating r in rank.Ratings) { + rank.RatingAverage += (float)((decimal)r.Value / (decimal)rank.Ratings.Count); + } + + if (eID != -1 || uID != null) { // do not sort "single item" (eID == -1 and no uID) category + RatingRank[] ranks = ctx.RatingRanks + .Where(rr => rr != rank && rr.CategoryID == category) // Only rank by category. + .OrderBy(rr => rr.Rank) + .ToArray(); + bool resortOthers = false; + rank.Rank = 1; // Start here, work way down. + for (int i=0;i r.Viking == viking && r.Rank.CategoryID == categoryID && r.Rank.RatedEntityID == ratedEntityID && r.Rank.RatedUserID == null) + .Select(r => new RatingInfo { + Id = r.Id, + OwnerUid = r.Viking.Uid, + CategoryID = r.Rank.CategoryID, + RatedEntityID = r.Rank.RatedEntityID, + Value = r.Value, + Date = r.Date + } + ).ToArray(); + } + + [HttpPost] + [Produces("application/xml")] + [Route("RatingWebService.asmx/GetTopRatedByCategoryID")] + public RatingRankInfo[] GetTopRatedByCategoryID([FromForm] int categoryID, [FromForm] int numberOfRecord) { + return ctx.RatingRanks + .Where(rr => categoryID == rr.CategoryID) + .Take(numberOfRecord) + .Select(rr => new RatingRankInfo(rr)) + .ToArray(); + } + + [HttpPost] + [Produces("application/xml")] + [Route("RatingWebService.asmx/GetTopRatedUserByCategoryID")] + public IActionResult GetTopRatedUserByCategoryID([FromForm] int categoryID, [FromForm] int numberOfRecord) { + return Ok(new ArrayOfUserRatingRankInfo { + UserRatingRankInfo = ctx.RatingRanks + .Where(rr => rr.RatedUserID != null && (categoryID == rr.CategoryID + || (categoryID == 4 && rr.CategoryID == 5) // The party board searches for 4 but the pod rating is set in 5. + )) + .OrderBy(rr => rr.Rank) + .Take(numberOfRecord) + .Select(rr => new UserRatingRankInfo { RankInfo = new RatingRankInfo(rr), RatedUserID = new Guid(rr.RatedUserID) }) + .ToArray() + }); + } + + [HttpPost] + [Produces("application/xml")] + [Route("RatingWebService.asmx/GetEntityRatedRank")] + public IActionResult GetEntityRatedRank([FromForm] int categoryID, [FromForm] int ratedEntityID) { + // TODO: Add a shortcut here for shipwreck lagoon tracks. + RatingRank? rank = ctx.RatingRanks.FirstOrDefault(rr => categoryID == rr.CategoryID && rr.RatedEntityID == ratedEntityID); + if (rank == null) return Ok(); + return Ok(new RatingRankInfo(rank)); + } + + [HttpPost] + [Produces("application/xml")] + [Route("RatingWebService.asmx/GetRatingForRatedUser")] + [VikingSession] + public IActionResult GetRatingForRatedUser(Viking viking, [FromForm] int categoryID, [FromForm] string ratedUserID) { + Rating? rating = ctx.Ratings.FirstOrDefault( + r => r.Viking == viking && categoryID == r.Rank.CategoryID && r.Rank.RatedEntityID == null && r.Rank.RatedUserID == ratedUserID + ); + return Ok(rating?.Value ?? 0); + } + + [HttpPost] + [Produces("application/xml")] + [Route("RatingWebService.asmx/GetRatingForRatedEntity")] + [VikingSession] + public IActionResult GetRatingForRatedEntity(Viking viking, [FromForm] int categoryID, [FromForm] int ratedEntityID) { + Rating? rating = ctx.Ratings.FirstOrDefault( + r => r.Viking == viking && categoryID == r.Rank.CategoryID && r.Rank.RatedEntityID == ratedEntityID && r.Rank.RatedUserID == null + ); + return Ok(rating?.Value ?? 0); + } } diff --git a/src/Model/DBContext.cs b/src/Model/DBContext.cs index 0d287df..1e881e5 100644 --- a/src/Model/DBContext.cs +++ b/src/Model/DBContext.cs @@ -26,6 +26,8 @@ public class DBContext : DbContext { public DbSet Neighborhoods { get; set; } = null!; // we had a brief debate on whether it's neighborhoods or neighborheed public DbSet Groups { get; set; } = null!; + public DbSet Ratings { get; set; } = null!; + public DbSet RatingRanks { get; set; } = null!; private readonly IOptions config; @@ -140,6 +142,9 @@ public class DBContext : DbContext { builder.Entity().HasMany(v => v.Groups) .WithMany(e => e.Vikings); + builder.Entity().HasMany(v => v.Ratings) + .WithOne(r => r.Viking); + // Dragons builder.Entity().HasOne(d => d.Viking) .WithMany(e => e.Dragons) @@ -260,5 +265,17 @@ public class DBContext : DbContext { // Groups builder.Entity().HasMany(r => r.Vikings) .WithMany(e => e.Groups); + + // Rating + builder.Entity().HasOne(r => r.Viking) + .WithMany(v => v.Ratings) + .HasForeignKey(r => r.VikingId); + + builder.Entity().HasOne(r => r.Rank) + .WithMany(rr => rr.Ratings) + .HasForeignKey(r => r.RankId); + + builder.Entity().HasMany(rr => rr.Ratings) + .WithOne(r => r.Rank); } } diff --git a/src/Model/Rating.cs b/src/Model/Rating.cs new file mode 100644 index 0000000..565e736 --- /dev/null +++ b/src/Model/Rating.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace sodoff.Model; + +public class Rating { + [Key] + public int Id { get; set; } + + public int VikingId { get; set; } + public int RankId { get; set; } + + public int Value { get; set; } + public DateTime Date { get; set; } + + public virtual Viking Viking { get; set; } + public virtual RatingRank Rank { get; set; } +} diff --git a/src/Model/RatingRank.cs b/src/Model/RatingRank.cs new file mode 100644 index 0000000..1eebec1 --- /dev/null +++ b/src/Model/RatingRank.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace sodoff.Model; + +public class RatingRank { + [Key] + public int Id { get; set; } + + public int CategoryID { get; set; } + public int? RatedEntityID { get; set; } + public string? RatedUserID { get; set; } + + public int Rank { get; set; } + public float RatingAverage { get; set; } // On a scale of 1-5 + public DateTime UpdateDate { get; set; } + + public virtual ICollection Ratings { get; set; } = null!; +} diff --git a/src/Model/Viking.cs b/src/Model/Viking.cs index 418a1dc..048c413 100644 --- a/src/Model/Viking.cs +++ b/src/Model/Viking.cs @@ -39,6 +39,7 @@ public class Viking { public virtual ICollection MMORoles { get; set; } = null!; public virtual Neighborhood? Neighborhood { get; set; } = null!; public virtual ICollection Groups { get; set; } = null!; + public virtual ICollection Ratings { get; set; } = null!; public virtual Dragon? SelectedDragon { get; set; } public DateTime? CreationDate { get; set; } diff --git a/src/Schema/ArrayOfUserRatingRankInfo.cs b/src/Schema/ArrayOfUserRatingRankInfo.cs new file mode 100644 index 0000000..2ebcdd7 --- /dev/null +++ b/src/Schema/ArrayOfUserRatingRankInfo.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "ArrayOfUserRatingRankInfo", Namespace = "")] +[Serializable] +public class ArrayOfUserRatingRankInfo { + [XmlElement(ElementName = "UserRatingRankInfo")] + public UserRatingRankInfo[] UserRatingRankInfo; +} diff --git a/src/Schema/RatingInfo.cs b/src/Schema/RatingInfo.cs new file mode 100644 index 0000000..df11cf1 --- /dev/null +++ b/src/Schema/RatingInfo.cs @@ -0,0 +1,25 @@ +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "RatingInfo", Namespace = "")] +[Serializable] +public class RatingInfo { + [XmlElement(ElementName = "ID")] + public int Id; + + [XmlElement(ElementName = "UID")] + public Guid OwnerUid; + + [XmlElement(ElementName = "CID")] + public int CategoryID; + + [XmlElement(ElementName = "EID")] + public int? RatedEntityID; + + [XmlElement(ElementName = "RV")] + public int Value; + + [XmlElement(ElementName = "RD")] + public DateTime Date; +} diff --git a/src/Schema/RatingRankInfo.cs b/src/Schema/RatingRankInfo.cs new file mode 100644 index 0000000..65fcd09 --- /dev/null +++ b/src/Schema/RatingRankInfo.cs @@ -0,0 +1,41 @@ +using sodoff.Model; +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "RatingRankInfo", Namespace = "")] +[Serializable] +public class RatingRankInfo { + + public RatingRankInfo() {} + public RatingRankInfo(RatingRank rank) { + Id = rank.Id; + CategoryID = rank.CategoryID; + RatedEntityID = rank.RatedEntityID??0; + Rank = rank.Rank; + RatingAverage = rank.RatingAverage; + TotalVotes = rank.Ratings.Count; + UpdateDate = rank.UpdateDate; + } + + [XmlElement(ElementName = "ID")] + public int Id; + + [XmlElement(ElementName = "CID")] + public int CategoryID; + + [XmlElement(ElementName = "EID")] + public int? RatedEntityID; + + [XmlElement(ElementName = "R")] + public int Rank; + + [XmlElement(ElementName = "RA")] + public float RatingAverage; + + [XmlElement(ElementName = "TV")] + public int TotalVotes; + + [XmlElement(ElementName = "UD")] + public DateTime UpdateDate; +} \ No newline at end of file diff --git a/src/Schema/UserRatingRankInfo.cs b/src/Schema/UserRatingRankInfo.cs new file mode 100644 index 0000000..dbcd6eb --- /dev/null +++ b/src/Schema/UserRatingRankInfo.cs @@ -0,0 +1,13 @@ +using System.Xml.Serialization; + +namespace sodoff.Schema; + +[XmlRoot(ElementName = "URRI", Namespace = "")] +[Serializable] +public class UserRatingRankInfo { + [XmlElement(ElementName = "RI")] + public RatingRankInfo RankInfo; + + [XmlElement(ElementName = "RUID")] + public Guid RatedUserID; +}