feat(learning): harden access and practice sessions
This commit is contained in:
@@ -5,6 +5,7 @@ using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -13,6 +14,7 @@ namespace Tiku.Infrastructure.QuestionBanks;
|
||||
|
||||
public sealed class QuestionBankQueryService(
|
||||
IQuestionBankPersistence questionBankPersistence,
|
||||
ILearningPersistence learningPersistence,
|
||||
IPublicQuestionAccessPolicy accessPolicy,
|
||||
ITenantExecutionScope tenantExecutionScope) : IQuestionBankQueryService
|
||||
{
|
||||
@@ -26,6 +28,7 @@ public sealed class QuestionBankQueryService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var limit = ResolveLimit(filter.Limit, DefaultBankLimit, MaxBankLimit);
|
||||
var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? [];
|
||||
var tenantItems = filter.Source == QuestionSource.Platform
|
||||
? []
|
||||
: await questionBankPersistence.QuestionBanks
|
||||
@@ -33,6 +36,12 @@ public sealed class QuestionBankQueryService(
|
||||
.Where(bank =>
|
||||
bank.TenantId == filter.TenantId &&
|
||||
bank.Status == QuestionBankStatus.Active &&
|
||||
learningPersistence.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
allowedSliceIds.Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.QuestionBank &&
|
||||
slice.ResourceId == bank.Id) &&
|
||||
(!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value || bank.RegionId == null) &&
|
||||
(string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim())))
|
||||
.OrderBy(bank => bank.Name)
|
||||
@@ -57,6 +66,7 @@ public sealed class QuestionBankQueryService(
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemQuestionBank = provider.GetRequiredService<IQuestionBankPersistence>();
|
||||
var systemLearning = provider.GetRequiredService<ILearningPersistence>();
|
||||
var systemTenancy = provider.GetRequiredService<ITenancyPersistence>();
|
||||
return await systemQuestionBank.QuestionBanks.AsNoTracking()
|
||||
.Join(
|
||||
@@ -67,6 +77,12 @@ public sealed class QuestionBankQueryService(
|
||||
(bank, tenant) => bank)
|
||||
.Where(bank =>
|
||||
bank.Status == QuestionBankStatus.Active &&
|
||||
systemLearning.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
allowedSliceIds.Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.QuestionBank &&
|
||||
slice.ResourceId == bank.Id) &&
|
||||
(!filter.RegionId.HasValue || bank.RegionId == filter.RegionId.Value ||
|
||||
bank.RegionId == null) &&
|
||||
(string.IsNullOrWhiteSpace(filter.Keyword) || bank.Name.Contains(filter.Keyword.Trim())))
|
||||
@@ -122,56 +138,6 @@ public sealed class QuestionBankQueryService(
|
||||
?? throw new QuestionBankNotFoundException("Question was not found.");
|
||||
}
|
||||
|
||||
public async Task<CatalogList<QuestionVersionCatalogItem>> GetQuestionVersionsAsync(
|
||||
QuestionBankFilter filter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!filter.QuestionId.HasValue) throw new QuestionBankRequiredFieldException("questionId is required.");
|
||||
|
||||
if (filter.Source == QuestionSource.Platform)
|
||||
{
|
||||
await accessPolicy.EnsureCanStartAsync(filter.TenantId, cancellationToken);
|
||||
var platformItems =
|
||||
await GetPlatformVersionsAsync(filter.QuestionId.Value, filter.TenantId, cancellationToken);
|
||||
return new CatalogList<QuestionVersionCatalogItem>(platformItems);
|
||||
}
|
||||
|
||||
var questionExists = await questionBankPersistence.Questions
|
||||
.AsNoTracking()
|
||||
.AnyAsync(
|
||||
question =>
|
||||
question.TenantId == filter.TenantId &&
|
||||
question.Id == filter.QuestionId.Value &&
|
||||
question.Status == QuestionStatus.Published,
|
||||
cancellationToken);
|
||||
|
||||
if (!questionExists) throw new QuestionBankNotFoundException("Question was not found.");
|
||||
|
||||
var items = await questionBankPersistence.QuestionVersions
|
||||
.AsNoTracking()
|
||||
.Where(version =>
|
||||
version.TenantId == filter.TenantId &&
|
||||
version.QuestionId == filter.QuestionId.Value)
|
||||
.OrderByDescending(version => version.VersionNo)
|
||||
.Select(version => new QuestionVersionCatalogItem(
|
||||
version.Id,
|
||||
version.QuestionId,
|
||||
version.VersionNo,
|
||||
version.Content,
|
||||
version.Options,
|
||||
version.CorrectOptionIndex,
|
||||
version.CorrectOptionIndices,
|
||||
version.AnswerText,
|
||||
version.Explanation,
|
||||
version.SubQuestions,
|
||||
version.CodeLang,
|
||||
version.CodeTemplate,
|
||||
version.CreatedAt))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new CatalogList<QuestionVersionCatalogItem>(items);
|
||||
}
|
||||
|
||||
private IQueryable<Question> BaseQuestionQuery()
|
||||
{
|
||||
return questionBankPersistence.Questions
|
||||
@@ -184,6 +150,23 @@ public sealed class QuestionBankQueryService(
|
||||
QuestionBankFilter filter)
|
||||
{
|
||||
query = query.Where(question => question.TenantId == filter.TenantId);
|
||||
var allowedSliceIds = filter.AllowedContentSliceIds?.ToArray() ?? [];
|
||||
query = query.Where(question =>
|
||||
(question.QuestionBankId.HasValue && learningPersistence.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
allowedSliceIds.Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.QuestionBank &&
|
||||
slice.ResourceId == question.QuestionBankId.Value)) ||
|
||||
questionBankPersistence.QuestionCollectionItems.Any(collectionItem =>
|
||||
collectionItem.TenantId == filter.TenantId &&
|
||||
collectionItem.QuestionId == question.Id &&
|
||||
learningPersistence.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
allowedSliceIds.Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.Collection &&
|
||||
slice.ResourceId == collectionItem.CollectionId)));
|
||||
|
||||
if (filter.QuestionBankId.HasValue)
|
||||
query = query.Where(question => question.QuestionBankId == filter.QuestionBankId.Value);
|
||||
@@ -247,8 +230,6 @@ public sealed class QuestionBankQueryService(
|
||||
QuestionSource source)
|
||||
{
|
||||
var emptyOptions = JsonDefaults.Array();
|
||||
var emptyCorrectOptionIndices = JsonDefaults.Array();
|
||||
var emptySubQuestions = JsonDefaults.Array();
|
||||
return
|
||||
from question in questions
|
||||
join version in context.QuestionVersions.AsNoTracking()
|
||||
@@ -281,11 +262,6 @@ public sealed class QuestionBankQueryService(
|
||||
version == null ? null : version.VersionNo,
|
||||
version == null ? null : version.Content,
|
||||
version == null ? emptyOptions : version.Options,
|
||||
version == null ? null : version.CorrectOptionIndex,
|
||||
version == null ? emptyCorrectOptionIndices : version.CorrectOptionIndices,
|
||||
version == null ? null : version.AnswerText,
|
||||
version == null ? null : version.Explanation,
|
||||
version == null ? emptySubQuestions : version.SubQuestions,
|
||||
version == null ? null : version.CodeLang,
|
||||
version == null ? null : version.CodeTemplate,
|
||||
new QuestionLocator(source, question.Id));
|
||||
@@ -316,6 +292,7 @@ public sealed class QuestionBankQueryService(
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemQuestionBank = provider.GetRequiredService<IQuestionBankPersistence>();
|
||||
var systemLearning = provider.GetRequiredService<ILearningPersistence>();
|
||||
var systemTenancy = provider.GetRequiredService<ITenancyPersistence>();
|
||||
var platformTenantId = await systemTenancy.Tenants.AsNoTracking()
|
||||
.Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
|
||||
@@ -324,6 +301,21 @@ public sealed class QuestionBankQueryService(
|
||||
var query = systemQuestionBank.Questions.AsNoTracking().Where(question =>
|
||||
question.TenantId == platformTenantId &&
|
||||
question.Status == QuestionStatus.Published &&
|
||||
((question.QuestionBankId.HasValue && systemLearning.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
(filter.AllowedContentSliceIds ?? Array.Empty<Guid>()).Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.QuestionBank &&
|
||||
slice.ResourceId == question.QuestionBankId.Value)) ||
|
||||
systemQuestionBank.QuestionCollectionItems.Any(collectionItem =>
|
||||
collectionItem.TenantId == filter.TenantId &&
|
||||
collectionItem.QuestionId == question.Id &&
|
||||
systemLearning.ContentSlices.Any(slice =>
|
||||
slice.TenantId == filter.TenantId &&
|
||||
(filter.AllowedContentSliceIds ?? Array.Empty<Guid>()).Contains(slice.Id) &&
|
||||
slice.Status == ContentSliceStatus.Active &&
|
||||
slice.ResourceType == LearningContentResourceType.Collection &&
|
||||
slice.ResourceId == collectionItem.CollectionId))) &&
|
||||
(!filter.QuestionId.HasValue || question.Id == filter.QuestionId.Value) &&
|
||||
(!filter.QuestionBankId.HasValue || question.QuestionBankId == filter.QuestionBankId.Value) &&
|
||||
(!filter.SubjectId.HasValue || question.SubjectId == filter.SubjectId.Value) &&
|
||||
@@ -351,53 +343,6 @@ public sealed class QuestionBankQueryService(
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private Task<QuestionVersionCatalogItem[]> GetPlatformVersionsAsync(
|
||||
Guid questionId,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return tenantExecutionScope.ExecuteAsync(
|
||||
new SystemScopeRequest(
|
||||
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(QuestionBankQueryService),
|
||||
"Read platform question versions for an entitled tenant", Guid.NewGuid().ToString("N")),
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemQuestionBank = provider.GetRequiredService<IQuestionBankPersistence>();
|
||||
var systemTenancy = provider.GetRequiredService<ITenancyPersistence>();
|
||||
var platformQuestion = await systemQuestionBank.Questions.AsNoTracking()
|
||||
.Where(question => question.Id == questionId && question.Status == QuestionStatus.Published)
|
||||
.Join(
|
||||
systemTenancy.Tenants.AsNoTracking().Where(tenant => tenant.Mode == TenantMode.PlatformOwned),
|
||||
question => question.TenantId,
|
||||
tenant => tenant.Id,
|
||||
(question, tenant) => new { question.TenantId, question.Id })
|
||||
.SingleOrDefaultAsync(token);
|
||||
if (platformQuestion is null) throw new QuestionBankNotFoundException("Question was not found.");
|
||||
|
||||
return await systemQuestionBank.QuestionVersions.AsNoTracking()
|
||||
.Where(version =>
|
||||
version.TenantId == platformQuestion.TenantId &&
|
||||
version.QuestionId == platformQuestion.Id)
|
||||
.OrderByDescending(version => version.VersionNo)
|
||||
.Select(version => new QuestionVersionCatalogItem(
|
||||
version.Id,
|
||||
version.QuestionId,
|
||||
version.VersionNo,
|
||||
version.Content,
|
||||
version.Options,
|
||||
version.CorrectOptionIndex,
|
||||
version.CorrectOptionIndices,
|
||||
version.AnswerText,
|
||||
version.Explanation,
|
||||
version.SubQuestions,
|
||||
version.CodeLang,
|
||||
version.CodeTemplate,
|
||||
version.CreatedAt))
|
||||
.ToArrayAsync(token);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static int ResolveLimit(int? limit, int defaultLimit, int maxLimit)
|
||||
{
|
||||
return Math.Clamp(limit ?? defaultLimit, 1, maxLimit);
|
||||
|
||||
Reference in New Issue
Block a user