Rating system (NOT COMPLETE!)

```sql
CREATE TABLE [Ratings] (
  [Id] INTEGER PRIMARY KEY AUTOINCREMENT,
  [OwnerId] INTEGER NOT NULL,
  [RankId] INTEGER NOT NULL,
  [CategoryID] INTEGER NOT NULL,
  [RatedEntityID] INTEGER NULL,
  [RatedUserID] TEXT NULL,
  [Value] INTEGER NOT NULL,
  [Date] TEXT NOT NULL
);
CREATE TABLE [RatingRanks] (
  [Id] INTEGER PRIMARY KEY AUTOINCREMENT,
  [CategoryID] INTEGER NOT NULL,
  [RatedEntityID] INTEGER NULL,
  [RatedUserID] TEXT NULL,
  [Rank] INTEGER NOT NULL,
  [RatingAverage] REAL NOT NULL,
  [TotalVotes] INTEGER NOT NULL,
  [UpdateDate] TEXT NOT NULL
);
```
This commit is contained in:
Hipposgrumm 2025-01-26 15:37:07 -07:00
parent 0127aab379
commit 3f355ff9f7
9 changed files with 346 additions and 0 deletions

View File

@ -1,13 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using sodoff.Attributes;
using sodoff.Configuration;
using sodoff.Model;
using sodoff.Schema;
using sodoff.Services;
using sodoff.Util;
using System.Security.Cryptography;
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
@ -103,4 +114,177 @@ public class RatingController : Controller
return Ok(5);
}
// This method is the only thing that adds ratings.
public RatingInfo SubmitRating(Viking viking, int category, int? eID, string? uID, int value) {
RatingRank? rank;
Rating? rating = viking.Ratings.FirstOrDefault(
r => category == r.CategoryID && r.RatedEntityID == eID && r.RatedUserID == uID
);
bool newRating = rating == null;
if (newRating) {
rating = new Rating {
OwnerId = viking.Id,
CategoryID = category,
RatedEntityID = eID,
RatedUserID = uID
};
ctx.Ratings.Add(rating);
rank = ctx.RatingRanks.FirstOrDefault(rr => rr.CategoryID == category && rr.RatedEntityID == eID && rr.RatedUserID == uID);
} else {
rank = rating.Rank;
}
if (rank == null) {
rank = new RatingRank {
CategoryID = category,
RatedEntityID = eID,
RatedUserID = uID,
Rank = 0 // Start here, work way down.
};
ctx.RatingRanks.Add(rank);
}
rank.TotalVotes = rank.Ratings?.Count??1;
if (newRating) {
rating.Rank = rank;
rank.TotalVotes++;
}
if (rank.Ratings != null) {
rank.RatingAverage = 0;
foreach (Rating r in rank.Ratings) {
if (r == rating) continue;
rank.RatingAverage += (float)((decimal)r.Value / (decimal)rank.TotalVotes);
}
rank.RatingAverage += (float)((decimal)value / (decimal)rank.TotalVotes);
} else {
rank.RatingAverage = value;
}
if (eID != -1 || uID != null) {
RatingRank[] ranks = ctx.RatingRanks
.Where(rr => rr.CategoryID == category) // Only rank by category.
.OrderBy(rr => rr.Rank)
.ToArray();
bool resortOthers = false;
for (int i=0;i<ranks.Length;i++) {
if (ranks[i] == rank) continue;
if (!resortOthers && ranks[i].RatingAverage < rank.RatingAverage) {
rank.Rank = i+1;
resortOthers = true;
}
if (resortOthers) ranks[i].Rank++;
Console.WriteLine(ranks[i].Id+" "+resortOthers);
}
}
rating.Value = value;
rating.Date = DateTime.UtcNow;
rank.UpdateDate = rating.Date;
ctx.SaveChanges();
RatingInfo info = new() {
Id = rating.Id,
OwnerUid = viking.Uid,
CategoryID = category,
RatedEntityID = eID,
Value = value,
Date = rating.Date
};
return info;
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/SetRating")]
[VikingSession]
public IActionResult SubmitRating(Viking viking, [FromForm] int categoryID, [FromForm] int ratedEntityID, [FromForm] int ratedValue) {
return Ok(SubmitRating(viking, categoryID, ratedEntityID, null, ratedValue));
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/SetUserRating")]
[VikingSession]
public IActionResult SubmitUserRating(Viking viking, [FromForm] int categoryID, [FromForm] string ratedUserID, [FromForm] int ratedValue) {
return Ok(SubmitRating(viking, categoryID, null, ratedUserID, ratedValue));
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetRatingByRatedEntity")]
public RatingInfo[] GetAllRatings([FromForm] int categoryID, [FromForm] int ratedEntityID) {
return ctx.Ratings
.Where(r => r.CategoryID == categoryID && r.RatedEntityID == ratedEntityID && r.RatedUserID == null)
.Select(r => new RatingInfo {
Id = r.Id,
OwnerUid = r.Owner.Uid,
CategoryID = r.CategoryID,
RatedEntityID = r.RatedEntityID,
Value = r.Value,
Date = r.Date
}
).ToArray();
}
[HttpPost]
[Route("RatingWebService.asmx/DeleteEntityRating")]
[VikingSession]
public IActionResult DeleteRating(Viking viking, [FromForm] int categoryID, [FromForm] int ratedEntityID) {
Rating? rating = viking.Ratings.FirstOrDefault(
r => categoryID == r.CategoryID && r.RatedEntityID == ratedEntityID && r.RatedUserID == null
);
if (rating != null) ctx.Ratings.Remove(rating);
return Ok();
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetTopRatedByCategoryID")]
public RatingRankInfo[] GetRanks([FromForm] int categoryID, [FromForm] int numberOfRecord) {
return ctx.RatingRanks
.Where(rr => categoryID == rr.CategoryID && rr.RatedUserID == null)
.Take(numberOfRecord)
.Select(rr => new RatingRankInfo(rr))
.ToArray();
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetTopRatedUserByCategoryID")]
public IActionResult GetUserRanks([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 by 5.
))
.Take(numberOfRecord)
.Select(rr => new UserRatingRankInfo { RankInfo = new RatingRankInfo(rr) })
.ToArray()
});
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetEntityRatedRank")]
public IActionResult GetRank([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")]
public IActionResult GetUserRating([FromForm] int categoryID, [FromForm] string ratedUserID) {
Rating? rating = ctx.Ratings.FirstOrDefault(
r => categoryID == r.CategoryID && r.RatedEntityID == null && r.RatedUserID == ratedUserID
);
return Ok(rating?.Value ?? 0);
}
[HttpPost]
[Produces("application/xml")]
[Route("RatingWebService.asmx/GetRatingForRatedEntity")]
public IActionResult GetRating([FromForm] int categoryID, [FromForm] int ratedEntityID) {
Rating? rating = ctx.Ratings.FirstOrDefault(
r => categoryID == r.CategoryID && r.RatedEntityID == ratedEntityID && r.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.Owner);
// 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.Owner)
.WithMany(v => v.Ratings)
.HasForeignKey(r => r.OwnerId);
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);
}
}

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

@ -0,0 +1,28 @@
using sodoff.Schema;
using System.ComponentModel.DataAnnotations;
namespace sodoff.Model;
public class Rating {
[Key]
public int Id { get; set; }
/// <summary>Viking that controls this data.</summary>
public virtual Viking? Owner { get; set; }
public virtual RatingRank? Rank { get; set; }
/// <summary>VikingId</summary>
public int OwnerId { get; set; }
public int RankId { get; set; } // Done this to prevent it from generating an unnecessary pairs table.
public int CategoryID { get; set; }
public int? RatedEntityID { get; set; }
public string? RatedUserID { get; set; }
public int Value { get; set; }
public DateTime Date { get; set; }
}

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

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
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; }
/// <summary>On a scale of 1-5</summary>
public float RatingAverage { get; set; }
public int TotalVotes { get; set; }
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;
}

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

@ -0,0 +1,26 @@
using sodoff.Model;
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;
Rank = rank.Rank;
RatingAverage = rank.RatingAverage;
TotalVotes = rank.TotalVotes;
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,14 @@
using sodoff.Model;
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;
}