From e79898910916d71bd00f6fce77aeb67b9baa15a7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Thu, 9 May 2024 13:01:16 +0800 Subject: [PATCH] Add support for deleting answers --- .../App/DeleteAnswersCommand.cs | 22 +++++++++++++ ...tePostCommand.cs => DeletePostsCommand.cs} | 4 +-- .../Data/QuestionsProvider.cs | 9 ++++- MyApp.ServiceInterface/Data/Tasks.cs | 17 +++++++--- MyApp.ServiceInterface/QuestionServices.cs | 27 +++++++++++++-- MyApp.ServiceInterface/SearchServices.cs | 19 ++++++++--- MyApp.ServiceModel/Posts.cs | 9 +++++ MyApp/Components/Shared/AnswerPost.razor | 3 +- MyApp/Configure.Db.cs | 4 --- MyApp/wwwroot/mjs/dtos.mjs | 22 ++++++++++++- MyApp/wwwroot/mjs/question.mjs | 33 +++++++++++++++++-- 11 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 MyApp.ServiceInterface/App/DeleteAnswersCommand.cs rename MyApp.ServiceInterface/App/{DeletePostCommand.cs => DeletePostsCommand.cs} (82%) diff --git a/MyApp.ServiceInterface/App/DeleteAnswersCommand.cs b/MyApp.ServiceInterface/App/DeleteAnswersCommand.cs new file mode 100644 index 0000000..a0e0807 --- /dev/null +++ b/MyApp.ServiceInterface/App/DeleteAnswersCommand.cs @@ -0,0 +1,22 @@ +using System.Data; +using ServiceStack; +using ServiceStack.OrmLite; +using MyApp.Data; +using MyApp.ServiceModel; + +namespace MyApp.ServiceInterface.App; + +public class DeleteAnswersCommand(IDbConnection db) : IAsyncCommand +{ + public async Task ExecuteAsync(DeleteAnswers request) + { + foreach (var refId in request.Ids) + { + await db.DeleteAsync(x => x.RefId == refId); + await db.DeleteByIdAsync(refId); + await db.DeleteAsync(x => x.Id == refId); + await db.DeleteAsync(x => x.RefId == refId); + await db.DeleteAsync(x => x.RefId == refId); + } + } +} diff --git a/MyApp.ServiceInterface/App/DeletePostCommand.cs b/MyApp.ServiceInterface/App/DeletePostsCommand.cs similarity index 82% rename from MyApp.ServiceInterface/App/DeletePostCommand.cs rename to MyApp.ServiceInterface/App/DeletePostsCommand.cs index e99ec23..f899c7f 100644 --- a/MyApp.ServiceInterface/App/DeletePostCommand.cs +++ b/MyApp.ServiceInterface/App/DeletePostsCommand.cs @@ -6,9 +6,9 @@ namespace MyApp.ServiceInterface.App; -public class DeletePostCommand(AppConfig appConfig, IDbConnection db) : IAsyncCommand +public class DeletePostsCommand(AppConfig appConfig, IDbConnection db) : IAsyncCommand { - public async Task ExecuteAsync(DeletePost request) + public async Task ExecuteAsync(DeletePosts request) { foreach (var postId in request.Ids) { diff --git a/MyApp.ServiceInterface/Data/QuestionsProvider.cs b/MyApp.ServiceInterface/Data/QuestionsProvider.cs index 6c5da9a..ca4ef2a 100644 --- a/MyApp.ServiceInterface/Data/QuestionsProvider.cs +++ b/MyApp.ServiceInterface/Data/QuestionsProvider.cs @@ -142,7 +142,7 @@ public async Task GetQuestionAsync(int id) var answerPath = GetAnswerPath(postId, userName); var file = fs.GetFile(answerPath) - ?? await r2.GetFileAsync(answerPath); + ?? await r2.GetFileAsync(answerPath); return file; } @@ -317,6 +317,13 @@ public async Task DeleteQuestionFilesAsync(int id) await r2.DeleteFilesAsync(remoteQuestionFiles.Files.Select(x => x.VirtualPath)); } + public async Task DeleteAnswerFileAsync(string answerId) + { + var answerFile = await GetAnswerFileAsync(answerId); + if (answerFile != null) + await DeleteFileAsync(answerFile.VirtualPath); + } + public async Task GetAnswerBodyAsync(string answerId) { var answerFile = await GetAnswerFileAsync(answerId); diff --git a/MyApp.ServiceInterface/Data/Tasks.cs b/MyApp.ServiceInterface/Data/Tasks.cs index 73a1bb5..38a7758 100644 --- a/MyApp.ServiceInterface/Data/Tasks.cs +++ b/MyApp.ServiceInterface/Data/Tasks.cs @@ -58,11 +58,16 @@ public class NewComment public DateTime LastUpdated { get; set; } } -public class DeletePost +public class DeletePosts { public required List Ids { get; set; } } +public class DeleteAnswers +{ + public required List Ids { get; set; } +} + public class CreatePostJobs { public required List PostJobs { get; set; } @@ -101,8 +106,11 @@ public class DbWrites : IGet, IReturn [Command] public Post? UpdatePost { get; set; } - [Command] - public DeletePost? DeletePost { get; set; } + [Command] + public DeletePosts? DeletePosts { get; set; } + + [Command] + public DeleteAnswers? DeleteAnswers { get; set; } [Command] public StartJob? StartJob { get; set; } @@ -192,5 +200,6 @@ public class SearchTasks { public int? AddPostToIndex { get; set; } public string? AddAnswerToIndex { get; set; } - public int? DeletePost { get; set; } + public List? DeletePosts { get; set; } + public List? DeleteAnswers { get; set; } } diff --git a/MyApp.ServiceInterface/QuestionServices.cs b/MyApp.ServiceInterface/QuestionServices.cs index 599225d..f99bf7f 100644 --- a/MyApp.ServiceInterface/QuestionServices.cs +++ b/MyApp.ServiceInterface/QuestionServices.cs @@ -141,11 +141,34 @@ public async Task Any(DeleteQuestion request) rendererCache.DeleteCachedQuestionPostHtml(request.Id); MessageProducer.Publish(new DbWrites { - DeletePost = new() { Ids = [request.Id] }, + DeletePosts = new() { Ids = [request.Id] }, }); MessageProducer.Publish(new SearchTasks { - DeletePost = request.Id, + DeletePosts = [request.Id], + }); + + if (request.ReturnUrl != null && request.ReturnUrl.StartsWith('/') && request.ReturnUrl.IndexOf(':') < 0) + return HttpResult.Redirect(request.ReturnUrl, HttpStatusCode.TemporaryRedirect); + + return new EmptyResponse(); + } + + public async Task Any(DeleteAnswer request) + { + if (!request.Id.Contains('-')) + throw new ArgumentException("Invalid Answer Id", nameof(request.Id)); + var postId = request.Id.LeftPart('-').ToInt(); + + await questions.DeleteAnswerFileAsync(request.Id); + rendererCache.DeleteCachedQuestionPostHtml(postId); + MessageProducer.Publish(new DbWrites + { + DeleteAnswers = new() { Ids = [request.Id] }, + }); + MessageProducer.Publish(new SearchTasks + { + DeleteAnswers = [request.Id], }); if (request.ReturnUrl != null && request.ReturnUrl.StartsWith('/') && request.ReturnUrl.IndexOf(':') < 0) diff --git a/MyApp.ServiceInterface/SearchServices.cs b/MyApp.ServiceInterface/SearchServices.cs index 5b5ab5a..2bd2beb 100644 --- a/MyApp.ServiceInterface/SearchServices.cs +++ b/MyApp.ServiceInterface/SearchServices.cs @@ -78,11 +78,22 @@ await db.ExecuteNonQueryAsync($@"INSERT INTO {nameof(PostFts)} ( )"); } - if (request.DeletePost != null) + if (request.DeletePosts != null) { - var id = request.DeletePost.Value; - log.LogInformation("[SEARCH] Deleting Post '{PostId}' from Search Index...", id); - await db.ExecuteNonQueryAsync($"DELETE FROM PostFts where RefId = '{id}' or RefId LIKE '{id}-%'"); + foreach (var id in request.DeletePosts) + { + log.LogInformation("[SEARCH] Deleting Post '{PostId}' from Search Index...", id); + await db.ExecuteNonQueryAsync($"DELETE FROM PostFts where RefId = '{id}' or RefId LIKE '{id}-%'"); + } + } + + if (request.DeleteAnswers != null) + { + foreach (var refId in request.DeleteAnswers) + { + log.LogInformation("[SEARCH] Deleting Answer '{PostId}' from Search Index...", refId); + await db.ExecuteNonQueryAsync($"DELETE FROM PostFts where RefId = @refId or RefId = @refId", new { refId }); + } } } } diff --git a/MyApp.ServiceModel/Posts.cs b/MyApp.ServiceModel/Posts.cs index 7d659d5..18e752c 100644 --- a/MyApp.ServiceModel/Posts.cs +++ b/MyApp.ServiceModel/Posts.cs @@ -465,6 +465,15 @@ public class DeleteQuestion : IGet, IReturn public string? ReturnUrl { get; set; } } +[ValidateHasRole(Roles.Moderator)] +public class DeleteAnswer : IGet, IReturn +{ + [ValidateNotEmpty] + public string Id { get; set; } + + public string? ReturnUrl { get; set; } +} + public class GetRequestInfo : IGet, IReturn {} public class GetUserReputations : IGet, IReturn diff --git a/MyApp/Components/Shared/AnswerPost.razor b/MyApp/Components/Shared/AnswerPost.razor index 2cdc4df..71c7fc8 100644 --- a/MyApp/Components/Shared/AnswerPost.razor +++ b/MyApp/Components/Shared/AnswerPost.razor @@ -25,6 +25,7 @@ @AppConfig.GetReputation(UserName) +
@if (Question.Meta?.ModelVotes?.TryGetValue(Answer.CreatedBy!, out var votes) == true && @@ -73,7 +74,7 @@ @if (Question.Post.LockedDate == null) { -
+
edit diff --git a/MyApp/Configure.Db.cs b/MyApp/Configure.Db.cs index f4bdf20..271e13f 100644 --- a/MyApp/Configure.Db.cs +++ b/MyApp/Configure.Db.cs @@ -13,11 +13,7 @@ public class ConfigureDb : IHostingStartup public const string AnalyticsDbPath = "App_Data/analytics.db"; public const string CreatorKitDbPath = "App_Data/creatorkit.db"; public const string ArchiveDbPath = "App_Data/archive.db"; -#if DEBUG - public const string SearchDbPath = "../../pvq/dist/search.db"; -#else public const string SearchDbPath = "App_Data/search.db"; -#endif public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { diff --git a/MyApp/wwwroot/mjs/dtos.mjs b/MyApp/wwwroot/mjs/dtos.mjs index 78fd1e3..7e61aa3 100644 --- a/MyApp/wwwroot/mjs/dtos.mjs +++ b/MyApp/wwwroot/mjs/dtos.mjs @@ -1,5 +1,5 @@ /* Options: -Date: 2024-05-05 17:08:25 +Date: 2024-05-09 12:12:17 Version: 8.23 Tip: To override a DTO option, remove "//" prefix before updating BaseUrl: https://localhost:5001 @@ -1541,6 +1541,15 @@ export class CalculateLeaderBoard { getMethod() { return 'GET' } createResponse() { return new CalculateLeaderboardResponse() } } +export class CalculateTop1KLeaderboard { + /** @param {{modelsToExclude?:string}} [init] */ + constructor(init) { Object.assign(this, init) } + /** @type {?string} */ + modelsToExclude; + getTypeName() { return 'CalculateTop1KLeaderboard' } + getMethod() { return 'GET' } + createResponse() { return new CalculateLeaderboardResponse() } +} export class GetLeaderboardStatsByTag { /** @param {{tag?:string,modelsToExclude?:string}} [init] */ constructor(init) { Object.assign(this, init) } @@ -1581,6 +1590,17 @@ export class DeleteQuestion { getMethod() { return 'GET' } createResponse() { return new EmptyResponse() } } +export class DeleteAnswer { + /** @param {{id?:string,returnUrl?:string}} [init] */ + constructor(init) { Object.assign(this, init) } + /** @type {string} */ + id; + /** @type {?string} */ + returnUrl; + getTypeName() { return 'DeleteAnswer' } + getMethod() { return 'GET' } + createResponse() { return new EmptyResponse() } +} export class AnswerQuestion { /** @param {{postId?:number,body?:string,refId?:string,refUrn?:string}} [init] */ constructor(init) { Object.assign(this, init) } diff --git a/MyApp/wwwroot/mjs/question.mjs b/MyApp/wwwroot/mjs/question.mjs index 1ce7f74..1d1ff96 100644 --- a/MyApp/wwwroot/mjs/question.mjs +++ b/MyApp/wwwroot/mjs/question.mjs @@ -7,7 +7,7 @@ import { renderMarkdown } from "markdown" import { UserPostData, PostVote, GetQuestionFile, AnswerQuestion, UpdateQuestion, PreviewMarkdown, GetAnswerBody, CreateComment, GetMeta, - DeleteQuestion, DeleteComment, GetUserReputations, CommentVote, + DeleteQuestion, DeleteAnswer, DeleteComment, GetUserReputations, CommentVote, ShareContent, FlagContent, GetAnswer, WaitForUpdate, GetLastUpdated, } from "dtos.mjs" @@ -436,6 +436,29 @@ const QuestionAside = { } } +const AnswerAside = { + template:` +
+ Delete question +
+ `, + props:['id'], + setup(props) { + const { hasRole } = useAuth() + const isModerator = hasRole('Moderator') + const client = useClient() + async function deleteAnswer() { + if (confirm('Are you sure?')) { + const api = await client.api(new DeleteAnswer({ id:props.id })) + if (api.succeeded) { + location.href = location.href + } + } + } + return { deleteAnswer, isModerator } + } +} + const Comments = { template:`
@@ -1067,6 +1090,7 @@ async function loadEditAnswers(ctx) { edit = el.querySelector('.edit'), preview = el.querySelector('.preview'), previewHtml = preview?.innerHTML, + answerAside = el.querySelector('.answer-aside'), footer = el.querySelector('.answer-footer') const bus = new EventBus() @@ -1080,7 +1104,12 @@ async function loadEditAnswers(ctx) { footer.innerHTML = '' } } else { - console.warn(`could not find .edit'`) + console.warn(`could not find .edit'`, el) + } + if (answerAside) { + mount(answerAside, AnswerAside, { id }) + } else { + console.warn(`could not find .answer-aside'`, el) } }) }