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 <robert@opcode.eu.org>
This commit is contained in:
Hipposgrumm 2025-02-08 02:19:43 -07:00 committed by GitHub
parent c1d6850867
commit 24695583b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 306 additions and 0 deletions

View File

@ -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<Rating>()
};
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<ranks.Length;i++) {
if (!resortOthers && ranks[i].RatingAverage < rank.RatingAverage) {
rank.Rank = i+1;
resortOthers = true;
}
if (resortOthers) ranks[i].Rank = i+2;
else ranks[i].Rank = i+1;
}
if (!resortOthers) rank.Rank = ranks.Length+1;
}
rating.Date = DateTime.UtcNow;
rank.UpdateDate = rating.Date;
ctx.SaveChanges();
return new RatingInfo() {
Id = rating.Id,
OwnerUid = viking.Uid,
CategoryID = category,
RatedEntityID = eID,
Value = value,
Date = rating.Date
};
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/SetRating")]
[VikingSession]
public IActionResult SetRating(Viking viking, [FromForm] int categoryID, [FromForm] int ratedEntityID, [FromForm] int ratedValue) {
return Ok(SetRating(viking, categoryID, ratedEntityID, null, ratedValue));
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/SetUserRating")]
[VikingSession]
public IActionResult SetUserRating(Viking viking, [FromForm] int categoryID, [FromForm] string ratedUserID, [FromForm] int ratedValue) {
return Ok(SetRating(viking, categoryID, null, ratedUserID, ratedValue));
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetRatingByRatedEntity")]
[VikingSession]
public RatingInfo[] GetRatingByRatedEntity(Viking viking, [FromForm] int categoryID, [FromForm] int ratedEntityID) {
return ctx.Ratings
.Where(r => 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);
}
}

View File

@ -26,6 +26,8 @@ public class DBContext : DbContext {
public DbSet<Neighborhood> Neighborhoods { get; set; } = null!;
// we had a brief debate on whether it's neighborhoods or neighborheed
public DbSet<Group> Groups { get; set; } = null!;
public DbSet<Rating> Ratings { get; set; } = null!;
public DbSet<RatingRank> RatingRanks { get; set; } = null!;
private readonly IOptions<ApiServerConfig> config;
@ -140,6 +142,9 @@ public class DBContext : DbContext {
builder.Entity<Viking>().HasMany(v => v.Groups)
.WithMany(e => e.Vikings);
builder.Entity<Viking>().HasMany(v => v.Ratings)
.WithOne(r => r.Viking);
// Dragons
builder.Entity<Dragon>().HasOne(d => d.Viking)
.WithMany(e => e.Dragons)
@ -260,5 +265,17 @@ public class DBContext : DbContext {
// Groups
builder.Entity<Group>().HasMany(r => r.Vikings)
.WithMany(e => e.Groups);
// Rating
builder.Entity<Rating>().HasOne(r => r.Viking)
.WithMany(v => v.Ratings)
.HasForeignKey(r => r.VikingId);
builder.Entity<Rating>().HasOne(r => r.Rank)
.WithMany(rr => rr.Ratings)
.HasForeignKey(r => r.RankId);
builder.Entity<RatingRank>().HasMany(rr => rr.Ratings)
.WithOne(r => r.Rank);
}
}

17
src/Model/Rating.cs Normal file
View File

@ -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; }
}

18
src/Model/RatingRank.cs Normal file
View File

@ -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<Rating> Ratings { get; set; } = null!;
}

View File

@ -39,6 +39,7 @@ public class Viking {
public virtual ICollection<MMORole> MMORoles { get; set; } = null!;
public virtual Neighborhood? Neighborhood { get; set; } = null!;
public virtual ICollection<Group> Groups { get; set; } = null!;
public virtual ICollection<Rating> Ratings { get; set; } = null!;
public virtual Dragon? SelectedDragon { get; set; }
public DateTime? CreationDate { get; set; }

View File

@ -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;
}

25
src/Schema/RatingInfo.cs Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}