#495 rating comments

This commit is contained in:
Hamid Reza Mohammadi 2026-08-04 15:47:05 +03:30
parent 430370a6b9
commit ca2f708add
6 changed files with 241 additions and 30 deletions

View File

@ -835,6 +835,7 @@ namespace RMuseum.Controllers
/// </summary>
/// <param name="url"></param>
/// <param name="catPoems"></param>
/// <param name="commentsSortByRanking">true: order comments by rating then date, false: order by date. Only applies to poem pages.</param>
/// <returns></returns>
[HttpGet]
[Route("page")]
@ -842,10 +843,10 @@ namespace RMuseum.Controllers
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPageCompleteViewModel))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetPageByUrl(string url, bool catPoems = false)
public async Task<IActionResult> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true)
{
RServiceResult<GanjoorPageCompleteViewModel> res =
await _ganjoorService.GetPageByUrl(url, catPoems);
await _ganjoorService.GetPageByUrl(url, catPoems, commentsSortByRanking);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
if (res.Result == null)
@ -1225,18 +1226,39 @@ namespace RMuseum.Controllers
[HttpPost]
[Route("poem/comment/{id}/rate")]
[Authorize]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorCommentRatingResultViewModel))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> RateComment(int id, short value)
{
Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
RServiceResult<bool> res =
RServiceResult<GanjoorCommentRatingResultViewModel> res =
await _ganjoorService.RateCommentAsync(userId, id, value);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// get the logged on user's own rating values for a poem's comments
/// (meant to be merged client-side into an already fetched, anonymously cacheable comment list)
/// </summary>
/// <param name="id">poem id</param>
/// <returns></returns>
[HttpGet]
[Route("poem/{id}/comments/myratings")]
[Authorize]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorCommentUserRatingViewModel[]))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetUserCommentRatings(int id)
{
var loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value);
RServiceResult<GanjoorCommentUserRatingViewModel[]> res =
await _ganjoorService.GetUserCommentRatings(id, loggedOnUserId);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// Get Section Related ones
/// </summary>

View File

@ -0,0 +1,46 @@
namespace RMuseum.Models.Ganjoor.ViewModels
{
/// <summary>
/// a single user's rating value for a single comment
/// (used to merge "my rating" state into an already fetched, anonymously cacheable comment list)
/// </summary>
public class GanjoorCommentUserRatingViewModel
{
/// <summary>
/// GanjoorComment Id
/// </summary>
public int CommentId { get; set; }
/// <summary>
/// +1 = Like, -1 = Dislike
/// </summary>
public short Value { get; set; }
}
/// <summary>
/// result of rating/unrating a comment, returned so the client can update counts in place
/// without re-fetching the whole comment list
/// </summary>
public class GanjoorCommentRatingResultViewModel
{
/// <summary>
/// comment id
/// </summary>
public int CommentId { get; set; }
/// <summary>
/// updated total likes
/// </summary>
public int LikeCount { get; set; }
/// <summary>
/// updated total dislikes
/// </summary>
public int DislikeCount { get; set; }
/// <summary>
/// the rating value the requesting user now has for this comment (0 if cleared)
/// </summary>
public short CurrentUserRatingValue { get; set; }
}
}

View File

@ -1292,12 +1292,13 @@
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetPageByUrl(System.String,System.Boolean)">
<member name="M:RMuseum.Controllers.GanjoorController.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary>
page by url
</summary>
<param name="url"></param>
<param name="catPoems"></param>
<param name="commentsSortByRanking">true: order comments by rating then date, false: order by date. Only applies to poem pages.</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.ModifyPage(System.Int32,RMuseum.Models.Ganjoor.ViewModels.GanjoorModifyPageViewModel)">
@ -1447,6 +1448,14 @@
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetUserCommentRatings(System.Int32)">
<summary>
get the logged on user's own rating values for a poem's comments
(meant to be merged client-side into an already fetched, anonymously cacheable comment list)
</summary>
<param name="id">poem id</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)">
<summary>
Get Section Related ones
@ -4595,6 +4604,18 @@
<member name="M:RMuseum.Migrations.RChFix.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Migrations.CommentsRatings">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary>
donation expenditures
@ -11695,6 +11716,48 @@
-1 = Dislike
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel">
<summary>
a single user's rating value for a single comment
(used to merge "my rating" state into an already fetched, anonymously cacheable comment list)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel.CommentId">
<summary>
GanjoorComment Id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel.Value">
<summary>
+1 = Like, -1 = Dislike
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel">
<summary>
result of rating/unrating a comment, returned so the client can update counts in place
without re-fetching the whole comment list
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.CommentId">
<summary>
comment id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.LikeCount">
<summary>
updated total likes
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.DislikeCount">
<summary>
updated total dislikes
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.CurrentUserRatingValue">
<summary>
the rating value the requesting user now has for this comment (0 if cleared)
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCoupletNumberViewModel">
<summary>
Ganjoor Couplet Number View Model
@ -16738,7 +16801,7 @@
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.GetPageByUrl(System.String,System.Boolean)">
<member name="M:RMuseum.Services.IGanjoorService.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary>
get page by url
</summary>
@ -16882,6 +16945,16 @@
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.GetUserCommentRatings(System.Int32,System.Guid)">
<summary>
get the requesting user's own rating values for all comments of a poem
(mirrors GetUserPoemRecitationsUpVotes - meant to be merged client-side into an
already fetched, anonymously cacheable GetPoemComments/GetPoemById result)
</summary>
<param name="poemId"></param>
<param name="userId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)">
<summary>
get a section related sections
@ -20347,13 +20420,22 @@
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.RateCommentAsync(System.Guid,System.Int32,System.Int16)">
<summary>
like / dislike / clear rating for a comment
</summary>
<param name="userId"></param>
<param name="commentId"></param>
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetUserCommentRatings(System.Int32,System.Guid)">
<summary>
get the requesting user's own rating values for all comments of a poem
(meant to be merged client-side into an already fetched, anonymously cacheable comment list)
</summary>
<param name="poemId"></param>
<param name="userId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetDigitalSourceFromTagAsync(System.String)">
<summary>
get digital source from tag
@ -21345,7 +21427,7 @@
<param name="url"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetPageByUrl(System.String,System.Boolean)">
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary>
get page by url
</summary>

View File

@ -94,7 +94,7 @@ namespace RMuseum.Services
/// <param name="url"></param>
/// <param name="catPoems"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false);
Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true);
/// <summary>
/// get page url by id
@ -230,7 +230,17 @@ namespace RMuseum.Services
/// <param name="commentId"></param>
/// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
/// <returns></returns>
Task<RServiceResult<bool>> RateCommentAsync(Guid userId, int commentId, short value);
Task<RServiceResult<GanjoorCommentRatingResultViewModel>> RateCommentAsync(Guid userId, int commentId, short value);
/// <summary>
/// get the requesting user's own rating values for all comments of a poem
/// (mirrors GetUserPoemRecitationsUpVotes - meant to be merged client-side into an
/// already fetched, anonymously cacheable GetPoemComments/GetPoemById result)
/// </summary>
/// <param name="poemId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorCommentUserRatingViewModel[]>> GetUserCommentRatings(int poemId, Guid userId);
/// <summary>
/// get a section related sections

View File

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
using RMuseum.Utils;
using RSecurityBackend.Models.Generic;
using System;
@ -14,38 +15,38 @@ namespace RMuseum.Services.Implementation
public partial class GanjoorService : IGanjoorService
{
/// <summary>
///
/// like / dislike / clear rating for a comment
/// </summary>
/// <param name="userId"></param>
/// <param name="commentId"></param>
/// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
/// <returns></returns>
public async Task<RServiceResult<bool>> RateCommentAsync(Guid userId, int commentId, short value)
public async Task<RServiceResult<GanjoorCommentRatingResultViewModel>> RateCommentAsync(Guid userId, int commentId, short value)
{
if(value != 1 && value != -1 && value != 0)
if (value != 1 && value != -1 && value != 0)
{
return new RServiceResult<bool>(false, "value != 1 && value != -1 && value != 0");
return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, "value != 1 && value != -1 && value != 0");
}
try
{
var comment = await _context.GanjoorComments.Where(c => c.Id == commentId).SingleOrDefaultAsync();
if (comment == null)
{
return new RServiceResult<bool>(false, "comment == null");
return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, "comment == null");
}
var oldRating = await _context.GanjoorCommentReactions.Where(c => c.GanjoorCommentId == commentId && c.UserId == userId).SingleOrDefaultAsync();
if(value == 0 && oldRating == null)
if (value == 0 && oldRating == null)
{
return new RServiceResult<bool>(true);
return await _CurrentCommentRatingResultAsync(comment, 0);
}
if (oldRating != null)
{
if (oldRating.Value == value)
{
return new RServiceResult<bool>(true);
return await _CurrentCommentRatingResultAsync(comment, oldRating.Value);
}
if(value == 0)
if (value == 0)
{
_context.Remove(oldRating);
}
@ -57,9 +58,9 @@ namespace RMuseum.Services.Implementation
}
await _context.SaveChangesAsync();
return await ReCalculateCommentSortKeyAsync(comment);
return await ReCalculateCommentSortKeyAsync(comment, value);
}
else
else
{
var rating = new GanjoorCommentReaction()
{
@ -71,17 +72,57 @@ namespace RMuseum.Services.Implementation
};
_context.Add(rating);
await _context.SaveChangesAsync();
return await ReCalculateCommentSortKeyAsync(comment);
return await ReCalculateCommentSortKeyAsync(comment, value);
}
}
catch (Exception exp)
{
return new RServiceResult<bool>(false, exp.ToString());
return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, exp.ToString());
}
}
private async Task<RServiceResult<bool>> ReCalculateCommentSortKeyAsync(GanjoorComment comment)
/// <summary>
/// get the requesting user's own rating values for all comments of a poem
/// (meant to be merged client-side into an already fetched, anonymously cacheable comment list)
/// </summary>
/// <param name="poemId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorCommentUserRatingViewModel[]>> GetUserCommentRatings(int poemId, Guid userId)
{
try
{
var ratings =
await _context.GanjoorCommentReactions.AsNoTracking()
.Where(r => r.PoemId == poemId && r.UserId == userId)
.Select(r => new GanjoorCommentUserRatingViewModel()
{
CommentId = r.GanjoorCommentId,
Value = r.Value,
})
.ToArrayAsync();
return new RServiceResult<GanjoorCommentUserRatingViewModel[]>(ratings);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorCommentUserRatingViewModel[]>(null, exp.ToString());
}
}
private Task<RServiceResult<GanjoorCommentRatingResultViewModel>> _CurrentCommentRatingResultAsync(GanjoorComment comment, short currentUserRatingValue)
{
return Task.FromResult(new RServiceResult<GanjoorCommentRatingResultViewModel>(
new GanjoorCommentRatingResultViewModel()
{
CommentId = comment.Id,
LikeCount = comment.LikeCount,
DislikeCount = comment.DislikeCount,
CurrentUserRatingValue = currentUserRatingValue,
}));
}
private async Task<RServiceResult<GanjoorCommentRatingResultViewModel>> ReCalculateCommentSortKeyAsync(GanjoorComment comment, short currentUserRatingValue)
{
try
{
@ -92,11 +133,19 @@ namespace RMuseum.Services.Implementation
comment.SortKey = GanjoorCommentRankingScoreCalculator.ComputeRankingScore(likes, dislikes);
_context.Update(comment);
await _context.SaveChangesAsync();
return new RServiceResult<bool>(true);
await CacheCleanForComment(comment.Id);
return new RServiceResult<GanjoorCommentRatingResultViewModel>(
new GanjoorCommentRatingResultViewModel()
{
CommentId = comment.Id,
LikeCount = comment.LikeCount,
DislikeCount = comment.DislikeCount,
CurrentUserRatingValue = currentUserRatingValue,
});
}
catch (Exception exp)
{
return new RServiceResult<bool>(false, exp.ToString());
return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, exp.ToString());
}
}
}

View File

@ -583,7 +583,7 @@ namespace RMuseum.Services.Implementation
/// <param name="url"></param>
/// <param name="catPoems"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false)
public async Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true)
{
if (url.IndexOf('?') != -1)
{
@ -599,7 +599,9 @@ namespace RMuseum.Services.Implementation
url = url.Replace("//", "/"); //duplicated slashes would be merged
var cachKey = $"GanjoorService::GetPageByUrl::{url}";
if (!_memoryCache.TryGetValue(cachKey, out GanjoorPageCompleteViewModel page))
// only the default (rating) sort order is cached, so a caller asking for date-sorted
// comments never reads or overwrites the shared cache entry used by everyone else
if (!commentsSortByRanking || !_memoryCache.TryGetValue(cachKey, out GanjoorPageCompleteViewModel page))
{
var dbPage = await _context.GanjoorPages.Where(p => p.FullUrl == url).AsNoTracking().SingleOrDefaultAsync();
if (dbPage == null)
@ -646,7 +648,7 @@ namespace RMuseum.Services.Implementation
{
case GanjoorPageType.PoemPage:
{
var poemRes = await GetPoemById((int)dbPage.PoemId);
var poemRes = await GetPoemById((int)dbPage.PoemId, sortByRanking: commentsSortByRanking);
if (!string.IsNullOrEmpty(poemRes.ExceptionString))
{
return new RServiceResult<GanjoorPageCompleteViewModel>(null, poemRes.ExceptionString);
@ -880,7 +882,7 @@ namespace RMuseum.Services.Implementation
query = sortByRanking
? query.OrderByDescending(c => c.SortKey)
.ThenByDescending(c => c.CommentDate)
.ThenBy(c => c.CommentDate)
: query.OrderBy(c => c.CommentDate);
var source =