Skip to content

Commit

Permalink
Allow human answers
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Mar 26, 2024
1 parent 30fb22d commit c97f4ab
Show file tree
Hide file tree
Showing 14 changed files with 431 additions and 131 deletions.
6 changes: 6 additions & 0 deletions MyApp.ServiceInterface/Data/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 29 additions & 4 deletions MyApp.ServiceInterface/Data/QuestionsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,25 @@ 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();
var path = $"{dir1}/{dir2}/{fileId}.meta.json";
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)
Expand Down Expand Up @@ -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);
}
}
91 changes: 91 additions & 0 deletions MyApp.ServiceInterface/Data/RendererCache.cs
Original file line number Diff line number Diff line change
@@ -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<string?> 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<string?> 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;
}
}
35 changes: 30 additions & 5 deletions MyApp.ServiceInterface/QuestionServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> Any(AskQuestion request)
{
Expand All @@ -16,9 +16,7 @@ public async Task<object> 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
{
Expand Down Expand Up @@ -59,6 +57,26 @@ public async Task<object> Any(AskQuestion request)
};
}

public async Task<object> 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<object> Any(GetQuestionFile request)
{
var questionFiles = await questions.GetQuestionFilesAsync(request.Id);
Expand Down Expand Up @@ -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;
}
}
22 changes: 21 additions & 1 deletion MyApp.ServiceModel/Posts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public class AskQuestion : IPost, IReturn<AskQuestionResponse>
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)]
Expand All @@ -270,6 +270,26 @@ public class AskQuestionResponse
public ResponseStatus? ResponseStatus { get; set; }
}

[ValidateIsAuthenticated]
[Description("Your Answer")]
public class AnswerQuestion : IPost, IReturn<AnswerQuestionResponse>
{
[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<string>
{
public string Markdown { get; set; }

Check warning on line 295 in MyApp.ServiceModel/Posts.cs

View workflow job for this annotation

GitHub Actions / push_to_registry

Non-nullable property 'Markdown' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Expand Down
1 change: 1 addition & 0 deletions MyApp.ServiceModel/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public class DbWrites
{
public Vote? RecordPostVote { get; set; }
public Post? CreatePost { get; set; }
public Post? UpdatePost { get; set; }
public List<PostJob>? CreatePostJobs { get; set; }
public StartJob? StartJob { get; set; }
public int? AnswerAddedToPost { get; set; }
Expand Down
5 changes: 5 additions & 0 deletions MyApp/Components/Pages/Questions/Question.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
else if (question?.Post?.Title != null)
{
<QuestionPost Question="question"/>
@if (question.Answers.All(x => x.Model != HttpContext.GetUserName()))
{
<div class="mb-20" data-component="pages/Questions/Answer.mjs" data-props="{id:@question.Id}"></div>
}
}
else
{
Expand All @@ -35,6 +39,7 @@
</div>
<Aside/>
</div>
<LiteYoutubeIncludes />

@code {
[CascadingParameter]
Expand Down
2 changes: 1 addition & 1 deletion MyApp/Components/Shared/Footer.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<footer class="bg-accent-1 dark:bg-black border-t border-accent-2 dark:border-gray-600">
<footer id="footer" class="bg-accent-1 dark:bg-black border-t border-accent-2 dark:border-gray-600">

<nav class="pt-8 columns-2 sm:flex sm:justify-center sm:space-x-12 text-center sm:text-left" aria-label="Footer">
<div class="pb-6">
Expand Down
2 changes: 2 additions & 0 deletions MyApp/Components/Shared/LiteYoutubeIncludes.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<link rel="stylesheet" href="css/lite-yt-embed.css">
<script src="lib/js/lite-yt-embed.js"></script>
19 changes: 11 additions & 8 deletions MyApp/Components/Shared/QuestionPost.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
@if (Question.Post.CreatedBy != null)
{
<div class="flex mr-4">
<img id="user-avatar" class="h-6 w-6 sm:h-8 sm:w-8 rounded-full bg-contain" src="/avatar/@Question.Post.CreatedBy" alt=@($"{Question.Post.CreatedBy} avatar")>
<img class="h-6 w-6 sm:h-8 sm:w-8 rounded-full bg-contain" src="/avatar/@Question.Post.CreatedBy" alt=@($"{Question.Post.CreatedBy} avatar")>
<div>
<b class="ml-2">@Question.Post.CreatedBy</b>
</div>
Expand Down Expand Up @@ -89,10 +89,13 @@
}
</article>

<div class="mt-16">
<h3 class="text-2xl font-semibold">
@Question.Answers.Count Answers
</h3>
<div id="answers" class="mt-16">
@if (@Question.Answers.Count > 0)
{
<h3 class="text-2xl font-semibold">
@Question.Answers.Count Answers
</h3>
}
<div>
@foreach (var answer in Question.Answers)
{
Expand All @@ -105,11 +108,11 @@
<svg class="down w-6 h-6 sm:w-10 sm:h-10 cursor-pointer select-none hover:text-green-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M21.886 5.536A1.002 1.002 0 0 0 21 5H3a1.002 1.002 0 0 0-.822 1.569l9 13a.998.998 0 0 0 1.644 0l9-13a.998.998 0 0 0 .064-1.033M12 17.243L4.908 7h14.184z"/></svg>
</div>
@{
var user = AppConfig.GetApplicationUser(answer.Model);
var userName = AppConfig.GetUserName(answer.Model);
}
<div class="mt-8 flex flex-col items-center">
<img class="w-10 h-10 xl:w-20 xl:h-20 bg-cover inline-block" src="@user.UserName.GetAvatarUrl()">
<div class="hidden md:block text-center whitespace-nowrap text-xs xl:text-sm font-semibold">@user.UserName</div>
<img class="w-10 h-10 xl:w-20 xl:h-20 bg-cover inline-block" src="@userName.GetAvatarUrl()">
<div class="hidden md:block text-center whitespace-nowrap text-xs xl:text-sm font-semibold">@userName</div>
</div>
</div>
<div class="xl:flex-grow prose">
Expand Down
Loading

0 comments on commit c97f4ab

Please sign in to comment.