From c97f4abd071589f3ea785befb501087995e570b6 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 26 Mar 2024 15:37:50 +0800 Subject: [PATCH] Allow human answers --- MyApp.ServiceInterface/Data/AppConfig.cs | 6 + .../Data/QuestionsProvider.cs | 33 +++- MyApp.ServiceInterface/Data/RendererCache.cs | 91 +++++++++++ MyApp.ServiceInterface/QuestionServices.cs | 35 ++++- MyApp.ServiceModel/Posts.cs | 22 ++- MyApp.ServiceModel/Stats.cs | 1 + .../Components/Pages/Questions/Question.razor | 5 + MyApp/Components/Shared/Footer.razor | 2 +- .../Shared/LiteYoutubeIncludes.razor | 2 + MyApp/Components/Shared/QuestionPost.razor | 19 ++- MyApp/Configure.Renderer.cs | 86 +---------- MyApp/wwwroot/css/app.css | 16 +- MyApp/wwwroot/mjs/dtos.mjs | 143 ++++++++++++++---- MyApp/wwwroot/pages/Questions/Answer.mjs | 101 +++++++++++++ 14 files changed, 431 insertions(+), 131 deletions(-) create mode 100644 MyApp.ServiceInterface/Data/RendererCache.cs create mode 100644 MyApp/Components/Shared/LiteYoutubeIncludes.razor create mode 100644 MyApp/wwwroot/pages/Questions/Answer.mjs diff --git a/MyApp.ServiceInterface/Data/AppConfig.cs b/MyApp.ServiceInterface/Data/AppConfig.cs index 8126ba4..590a0b1 100644 --- a/MyApp.ServiceInterface/Data/AppConfig.cs +++ b/MyApp.ServiceInterface/Data/AppConfig.cs @@ -23,6 +23,12 @@ public ApplicationUser GetApplicationUser(string model) return user ?? DefaultUser; } + public string GetUserName(string model) + { + var user = ModelUsers.FirstOrDefault(x => x.Model == model || x.UserName == model); + return user?.UserName ?? model; + } + private long nextPostId = -1; public void SetInitialPostId(long initialValue) => this.nextPostId = initialValue; public long LastPostId => Interlocked.Read(ref nextPostId); diff --git a/MyApp.ServiceInterface/Data/QuestionsProvider.cs b/MyApp.ServiceInterface/Data/QuestionsProvider.cs index be4eab3..2bbaeec 100644 --- a/MyApp.ServiceInterface/Data/QuestionsProvider.cs +++ b/MyApp.ServiceInterface/Data/QuestionsProvider.cs @@ -75,6 +75,13 @@ public static string GetModelAnswerPath(int id, string model) return path; } + public static string GetHumanAnswerPath(int id, string userName) + { + var (dir1, dir2, fileId) = id.ToFileParts(); + var path = $"{dir1}/{dir2}/{fileId}.h.{userName}.json"; + return path; + } + public static string GetMetaPath(int id) { var (dir1, dir2, fileId) = id.ToFileParts(); @@ -82,11 +89,11 @@ public static string GetMetaPath(int id) return path; } - public async Task SaveFileAsync(string file, string contents) + public async Task SaveFileAsync(string virtualPath, string contents) { await Task.WhenAll( - fs.WriteFileAsync(file, contents), - r2.WriteFileAsync(file, contents)); + r2.WriteFileAsync(virtualPath, contents), + fs.WriteFileAsync(virtualPath, contents)); } public async Task WriteMetaAsync(Meta meta) @@ -130,13 +137,31 @@ public async Task SaveQuestionAsync(Post post) await SaveFileAsync(GetQuestionPath(post.Id), ToJson(post)); } - public async Task SaveAnswerAsync(int postId, string model, string json) + public async Task SaveModelAnswerAsync(int postId, string model, string json) { await SaveFileAsync(GetModelAnswerPath(postId, model), json); } + public async Task SaveHumanAnswerAsync(Post post) + { + await SaveFileAsync(GetHumanAnswerPath( + post.ParentId ?? throw new ArgumentNullException(nameof(Post.ParentId)), + post.CreatedBy ?? throw new ArgumentNullException(nameof(Post.CreatedBy))), + ToJson(post)); + } + + public async Task SaveHumanAnswerAsync(int postId, string userName, string json) + { + await SaveFileAsync(GetHumanAnswerPath(postId, userName), json); + } + public async Task SaveLocalFileAsync(string virtualPath, string contents) { await fs.WriteFileAsync(virtualPath, contents); } + + public async Task SaveRemoteFileAsync(string virtualPath, string contents) + { + await r2.WriteFileAsync(virtualPath, contents); + } } diff --git a/MyApp.ServiceInterface/Data/RendererCache.cs b/MyApp.ServiceInterface/Data/RendererCache.cs new file mode 100644 index 0000000..623d5c0 --- /dev/null +++ b/MyApp.ServiceInterface/Data/RendererCache.cs @@ -0,0 +1,91 @@ +using ServiceStack; +using ServiceStack.IO; + +namespace MyApp.Data; + +public class RendererCache(AppConfig appConfig, R2VirtualFiles r2) +{ + private static bool DisableCache = true; + + public string GetCachedQuestionPostPath(int id) => appConfig.CacheDir.CombineWith(GetQuestionPostVirtualPath(id)); + + public string GetQuestionPostVirtualPath(int id) + { + var idParts = id.ToFileParts(); + var fileName = $"{idParts.fileId}.QuestionPost.html"; + var dirPath = $"{idParts.dir1}/{idParts.dir2}"; + var filePath = $"{dirPath}/{fileName}"; + return filePath; + } + + public async Task GetQuestionPostHtmlAsync(int id) + { + if (DisableCache) + return null; + var filePath = GetCachedQuestionPostPath(id); + if (File.Exists(filePath)) + return await File.ReadAllTextAsync(filePath); + return null; + } + + public void DeleteCachedQuestionPostHtml(int id) + { + try + { + File.Delete(GetCachedQuestionPostPath(id)); + } + catch {} + } + + public async Task SetQuestionPostHtmlAsync(int id, string? html) + { + if (DisableCache) + return; + if (!string.IsNullOrEmpty(html)) + throw new ArgumentNullException(html); + + var (dir1, dir2, fileId) = id.ToFileParts(); + appConfig.CacheDir.CombineWith($"{dir1}/{dir2}").AssertDir(); + var filePath = GetCachedQuestionPostPath(id); + await File.WriteAllTextAsync(filePath, html); + } + + private string GetHtmlTabFilePath(string? tab) + { + var partialName = string.IsNullOrEmpty(tab) + ? "" + : $".{tab}"; + var filePath = appConfig.CacheDir.CombineWith($"HomeTab{partialName}.html"); + return filePath; + } + + static TimeSpan HomeTabValidDuration = TimeSpan.FromMinutes(5); + + public async Task SetHomeTabHtmlAsync(string? tab, string html) + { + if (DisableCache) + return; + appConfig.CacheDir.AssertDir(); + var filePath = GetHtmlTabFilePath(tab); + await File.WriteAllTextAsync(filePath, html); + } + + public async Task GetHomeTabHtmlAsync(string? tab) + { + if (DisableCache) + return null; + var filePath = GetHtmlTabFilePath(tab); + var fileInfo = new FileInfo(filePath); + if (fileInfo.Exists) + { + if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc > HomeTabValidDuration) + return null; + + var html = await fileInfo.ReadAllTextAsync(); + if (!string.IsNullOrEmpty(html)) + return html; + } + + return null; + } +} \ No newline at end of file diff --git a/MyApp.ServiceInterface/QuestionServices.cs b/MyApp.ServiceInterface/QuestionServices.cs index cfaa800..4aea256 100644 --- a/MyApp.ServiceInterface/QuestionServices.cs +++ b/MyApp.ServiceInterface/QuestionServices.cs @@ -4,7 +4,7 @@ namespace MyApp.ServiceInterface; -public class QuestionServices(AppConfig appConfig, QuestionsProvider questions) : Service +public class QuestionServices(AppConfig appConfig, QuestionsProvider questions, RendererCache rendererCache) : Service { public async Task Any(AskQuestion request) { @@ -16,9 +16,7 @@ public async Task Any(AskQuestion request) if (tags.Count > 5) throw new ArgumentException("Maximum of 5 tags allowed", nameof(request.Tags)); - var userName = Request.GetClaimsPrincipal().GetUserName() - ?? throw new ArgumentNullException(nameof(ApplicationUser.UserName)); - + var userName = GetUserName(); var now = DateTime.UtcNow; var post = new Post { @@ -59,6 +57,26 @@ public async Task Any(AskQuestion request) }; } + public async Task Any(AnswerQuestion request) + { + var userName = GetUserName(); + var now = DateTime.UtcNow; + var post = new Post + { + ParentId = request.PostId, + Summary = request.Body.StripHtml().SubstringWithEllipsis(0,200), + CreationDate = now, + CreatedBy = userName, + LastActivityDate = now, + Body = request.Body, + RefId = request.RefId, + }; + + await questions.SaveHumanAnswerAsync(post); + rendererCache.DeleteCachedQuestionPostHtml(post.Id); + return new AnswerQuestionResponse(); + } + public async Task Any(GetQuestionFile request) { var questionFiles = await questions.GetQuestionFilesAsync(request.Id); @@ -95,6 +113,13 @@ public async Task Any(CreateWorkerAnswer request) }); } - await questions.SaveAnswerAsync(request.PostId, request.Model, json); + await questions.SaveModelAnswerAsync(request.PostId, request.Model, json); + } + + private string GetUserName() + { + var userName = Request.GetClaimsPrincipal().GetUserName() + ?? throw new ArgumentNullException(nameof(ApplicationUser.UserName)); + return userName; } } diff --git a/MyApp.ServiceModel/Posts.cs b/MyApp.ServiceModel/Posts.cs index 1c900d3..abd485d 100644 --- a/MyApp.ServiceModel/Posts.cs +++ b/MyApp.ServiceModel/Posts.cs @@ -251,7 +251,7 @@ public class AskQuestion : IPost, IReturn public required string Title { get; set; } [ValidateNotEmpty, ValidateMinimumLength(30), ValidateMaximumLength(32768)] - [Input(Type="MarkdownInput", Help = "Include all information required for someone to identity and resolve your exact question"), FieldCss(Field="col-span-12", Input="h-56")] + [Input(Type="MarkdownInput", Help = "Include all information required for someone to identity and resolve your exact question"), FieldCss(Field="col-span-12", Input="h-60")] public required string Body { get; set; } [ValidateNotEmpty, ValidateMinimumLength(2, Message = "At least 1 tag required"), ValidateMaximumLength(120)] @@ -270,6 +270,26 @@ public class AskQuestionResponse public ResponseStatus? ResponseStatus { get; set; } } +[ValidateIsAuthenticated] +[Description("Your Answer")] +public class AnswerQuestion : IPost, IReturn +{ + [Input(Type="hidden")] + [ValidateGreaterThan(0)] + public int PostId { get; set; } + + [ValidateNotEmpty, ValidateMinimumLength(30), ValidateMaximumLength(32768)] + [Input(Type="MarkdownInput", Label=""), FieldCss(Field="col-span-12", Input="h-60")] + public required string Body { get; set; } + + [Input(Type="hidden")] + public string? RefId { get; set; } +} +public class AnswerQuestionResponse +{ + public ResponseStatus? ResponseStatus { get; set; } +} + public class PreviewMarkdown : IPost, IReturn { public string Markdown { get; set; } diff --git a/MyApp.ServiceModel/Stats.cs b/MyApp.ServiceModel/Stats.cs index 5db0481..1789105 100644 --- a/MyApp.ServiceModel/Stats.cs +++ b/MyApp.ServiceModel/Stats.cs @@ -114,6 +114,7 @@ public class DbWrites { public Vote? RecordPostVote { get; set; } public Post? CreatePost { get; set; } + public Post? UpdatePost { get; set; } public List? CreatePostJobs { get; set; } public StartJob? StartJob { get; set; } public int? AnswerAddedToPost { get; set; } diff --git a/MyApp/Components/Pages/Questions/Question.razor b/MyApp/Components/Pages/Questions/Question.razor index 4e1d975..abefe56 100644 --- a/MyApp/Components/Pages/Questions/Question.razor +++ b/MyApp/Components/Pages/Questions/Question.razor @@ -18,6 +18,10 @@ else if (question?.Post?.Title != null) { + @if (question.Answers.All(x => x.Model != HttpContext.GetUserName())) + { +
+ } } else { @@ -35,6 +39,7 @@