/// <summary> /// Resolves the question corresponding to the UserQuestionData object. /// </summary> private async Task <Question> ResolveUnsolvedQuestionAsync( UserQuestionData userQuestionData) { return(await _questionResolverFactory .CreateQuestionResolver(userQuestionData) .ResolveUnsolvedQuestionAsync()); }
/// <summary> /// Returns the next question ID to use. /// </summary> public int GetNextQuestionId( UserQuestionData userQuestionData, IList <int> availableQuestionIds) { var seenQuestionIds = userQuestionData ?.Submissions ?.Where(uqs => uqs.Seed.HasValue) ?.GroupBy(uqs => uqs.Seed.Value) ?.ToDictionary(g => g.Key, g => g.Count()) ?? new Dictionary <int, int>(); var validQuestionIds = new HashSet <int>(availableQuestionIds); var unseenQuestionIds = validQuestionIds .Except(seenQuestionIds.Keys) .ToList(); if (unseenQuestionIds.Any()) { var randomIndex = _randomNumberProvider.NextInt() % unseenQuestionIds.Count; return(unseenQuestionIds[randomIndex]); } else { return(seenQuestionIds .Where(kvp => validQuestionIds.Contains(kvp.Key)) .MinBy(kvp => kvp.Value) .Key); } }
public void CreateQuestionResolver_NewAttemptAllowed_ReturnsCorrectType( Type questionType, Type expectedResolverType) { var userQuestionData = new UserQuestionData() { AssignmentQuestion = new AssignmentQuestion() { Question = (Question)Activator.CreateInstance(questionType) } }; var questionStatusCalculator = GetMockQuestionStatusCalculator ( userQuestionData, allowNewAttempt: true ); var questionResolverFactory = new QuestionResolverFactory ( dbContext: null, jsonSerializer: null, questionLoaderFactory: null, questionStatusCalculator: questionStatusCalculator ); var result = questionResolverFactory.CreateQuestionResolver ( userQuestionData ); Assert.Equal(expectedResolverType, result.GetType()); }
/// <summary> /// Constructor. /// </summary> public GeneratedQuestionTemplateResolver( UserQuestionData userQuestionData, IJsonSerializer jsonSerializer) { _jsonSerializer = jsonSerializer; _userQuestionData = userQuestionData; }
/// <summary> /// Generates a new seed. /// </summary> public int GenerateSeed(UserQuestionData userQuestionData, int numSeeds) { var existingSeeds = userQuestionData.Submissions ?.Where(uqs => uqs.Seed.HasValue && uqs.Seed < numSeeds) ?.GroupBy(uqs => uqs.Seed.Value) ?.ToDictionary(g => g.Key, g => g.Count()) ?? new Dictionary <int, int>(); int newSeed; if (existingSeeds.Count >= numSeeds) { newSeed = existingSeeds .MinBy(kvp => kvp.Value) .Key; } else { do { newSeed = _randomNumberProvider.NextInt() % numSeeds; } while (existingSeeds.ContainsKey(newSeed)); } return(newSeed); }
/// <summary> /// Returns whether or not a new question is needed because the existing question /// is no longer valid. This can happen if a choice is removed. /// </summary> private bool ExistingQuestionNoLongerValid( UserQuestionData userQuestionData, IList <int> availableQuestionIds) { return(userQuestionData.Seed.HasValue && !availableQuestionIds.Contains(userQuestionData.Seed.Value)); }
/// <summary> /// Returns whether or not a new question is needed for the user's next attempt. /// </summary> private bool NewQuestionNeededForNextAttempt(UserQuestionData userQuestionData) { var status = _questionStatusCalculator .GetQuestionStatus(userQuestionData); return(userQuestionData.Seed == null && status.AllowNewAttempt); }
public void CreateQuestionResolver_NoNewAttemptAllowed_ReturnsCorrectType() { var userQuestionData = new UserQuestionData() { AssignmentQuestion = new AssignmentQuestion() { Question = new MultipleChoiceQuestion() } }; var questionStatusCalculator = GetMockQuestionStatusCalculator ( userQuestionData, allowNewAttempt: false ); var questionResolverFactory = new QuestionResolverFactory ( dbContext: null, jsonSerializer: null, questionLoaderFactory: null, questionStatusCalculator: questionStatusCalculator ); var result = questionResolverFactory.CreateQuestionResolver ( userQuestionData ); Assert.Equal(typeof(NoMoreAttemptsQuestionResolver), result.GetType()); }
/// <summary> /// Returns the question to solve. /// </summary> private async Task <QuestionToSolve> GetQuestionToSolveAsync( UserQuestionData userQuestionData, Question resolvedQuestion, QuestionSubmission lastQuestionSubmission) { var userQuestionStatus = GetUserQuestionStatus(userQuestionData); var assignmentProgress = await GetAssignmentProgressAsync(userQuestionData); var seed = GetSeedToDisplay(userQuestionData); var pastSubmissions = GetPastSubmissions(userQuestionData); return(new QuestionToSolve ( userQuestionData.AssignmentQuestionId, userQuestionData.AssignmentQuestion.Name, resolvedQuestion, seed, userQuestionData.User, lastQuestionSubmission, userQuestionData.AssignmentQuestion.IsInteractive(), pastSubmissions, userQuestionStatus, assignmentProgress )); }
/// <summary> /// Creates a mock question selector. /// </summary> private IRandomlySelectedQuestionSelector CreateMockQuestionSelector( UserQuestionData userQuestionData, IList <int> availableQuestionIds, int newSeed) { var mockQuestionSelector = new Mock <IRandomlySelectedQuestionSelector>(); mockQuestionSelector .Setup ( m => m.GetNextQuestionId ( userQuestionData, It.Is <IList <int> > ( ids => ids .OrderBy(id => id) .SequenceEqual ( availableQuestionIds .OrderBy(id => id) ) ) ) ).Returns(newSeed); return(mockQuestionSelector.Object); }
/// <summary> /// Returns whether or not the generated question template was modified since /// the question was generated. /// </summary> private bool WasQuestionModified(UserQuestionData userQuestionData) { var generatedQuestion = (GeneratedQuestionTemplate)userQuestionData .AssignmentQuestion .Question; return(generatedQuestion.DateModified > userQuestionData.CachedQuestionDataTime); }
/// <summary> /// Resolves the question corresponding to the given UserQuestionSubmission. /// </summary> private async Task <Question> ResolveSolvedQuestionAsync( UserQuestionData userQuestionData, UserQuestionSubmission userQuestionSubmission) { return(await _questionResolverFactory .CreateQuestionResolver(userQuestionData) .ResolveSolvedQuestionAsync(userQuestionSubmission)); }
public async Task UpdateAllAsync_UpdatersInvoked() { var userQuestionData = new UserQuestionData(); var defaultUpdater = new DefaultUserQuestionDataUpdater(); defaultUpdater.AddToBatch(userQuestionData); await defaultUpdater.UpdateAllAsync(); }
/// <summary> /// Adds a job to the batch that updates the given UserQuestionData object. /// </summary> public void AddToBatch(UserQuestionData userQuestionData) { var userQuestionDataUpdater = _userQuestionDataUpdaterImplFactory .GetUserQuestionDataUpdater(userQuestionData.AssignmentQuestion.Question); _userQuestionDataUpdaters.Add(userQuestionDataUpdater); userQuestionDataUpdater.AddToBatch(userQuestionData); }
/// <summary> /// Adds a job to the batch that updates the given UserQuestionData object. /// </summary> public void AddToBatch(UserQuestionData userQuestionData) { if (!(userQuestionData.AssignmentQuestion.Question is RandomlySelectedQuestion)) { throw new InvalidOperationException("Invalid question type."); } _userQuestionDatas.Add(userQuestionData); }
/// <summary> /// Returns whether or not the user is an admin for the class. /// </summary> private bool IsClassAdmin(UserQuestionData uqd) { var classroom = uqd.AssignmentQuestion.Assignment.Classroom; return(uqd.User .ClassroomMemberships .SingleOrDefault(cm => cm.Classroom == classroom) ?.Role >= ClassroomRole.Admin); }
/// <summary> /// Returns information about the user's attempts for a given question, /// and whether future attempts are permitted. /// </summary> public UserQuestionStatus GetQuestionStatus(UserQuestionData uqd) { return(new UserQuestionStatus ( GetNumAttempts(uqd), AnyCorrectSubmissions(uqd), GetAttemptsRemaining(uqd) )); }
/// <summary> /// Constructor. /// </summary> public RandomlySelectedQuestionResolver( UserQuestionData userQuestionData, DatabaseContext dbContext, IQuestionLoaderFactory questionLoaderFactory) { _userQuestionData = userQuestionData; _dbContext = dbContext; _questionLoaderFactory = questionLoaderFactory; }
/// <summary> /// Returns a UserQuestionDataStore with a single question. /// </summary> private UserQuestionDataStore GetUserQuestionDataStore( int assignmentQuestionId = 1, string assignmentQuestionName = "Question", User user = null, string lastSubmission = null, bool interactive = false, bool generated = false, int numAttempts = 0, int numAttemptsRemaining = 0, string cachedQuestionData = null, int seed = 0, double questionPoints = 1.0, IList <UserQuestionSubmission> pastSubmissions = null) { var userQuestionData = new UserQuestionData() { User = user, AssignmentQuestionId = assignmentQuestionId, AssignmentQuestion = new AssignmentQuestion() { Id = assignmentQuestionId, Name = assignmentQuestionName, Assignment = new Assignment() { CombinedSubmissions = !interactive, MaxAttempts = numAttempts + numAttemptsRemaining, }, Points = questionPoints, Question = generated ? (Question) new GeneratedQuestionTemplate() : interactive ? (Question) new MethodQuestion() : (Question) new MultipleChoiceQuestion() }, LastQuestionSubmission = lastSubmission, NumAttempts = numAttempts, CachedQuestionData = cachedQuestionData, Seed = seed, Submissions = pastSubmissions }; if (pastSubmissions != null) { foreach (var submission in pastSubmissions) { submission.UserQuestionData = userQuestionData; } } return(new UserQuestionDataStore ( new Dictionary <int, UserQuestionData>() { [assignmentQuestionId] = userQuestionData } )); }
/// <summary> /// Returns whether or not the given question must be regenerated with a new seed. /// </summary> private bool IsNewSeedRequired(UserQuestionData userQuestionData) { var allowNewAttempt = _questionStatusCalculator .GetQuestionStatus(userQuestionData) .AllowNewAttempt; var generationRequired = userQuestionData.Seed == null; return(allowNewAttempt && generationRequired); }
/// <summary> /// Returns a list of past submissions for a given question. /// </summary> private IList <DateTime> GetPastSubmissions(UserQuestionData userQuestionData) { if (userQuestionData.AssignmentQuestion.IsInteractive()) { return(new List <DateTime>()); } return(userQuestionData.Submissions ?.GroupBy(s => s.DateSubmitted) ?.Select(g => g.Key) ?.OrderBy(d => d) ?.ToList() ?? new List <DateTime>()); }
/// <summary> /// Creates a mock seed generator. /// </summary> private IGeneratedQuestionSeedGenerator CreateMockSeedGenerator( UserQuestionData userQuestionData, int maxSeed, int newSeed) { var mockSeedGenerator = new Mock <IGeneratedQuestionSeedGenerator>(); mockSeedGenerator .Setup(m => m.GenerateSeed(userQuestionData, maxSeed)) .Returns(newSeed); return(mockSeedGenerator.Object); }
/// <summary> /// Sets up the mock factory to return a given updater for a given /// UserQuestionData object. /// </summary> private static void SetupMockImplFactory( Mock <IUserQuestionDataUpdaterImplFactory> mockImplFactory, UserQuestionData userQuestionData, MockUserQuestionDataUpdater impl) { mockImplFactory .Setup ( m => m.GetUserQuestionDataUpdater ( userQuestionData.AssignmentQuestion.Question ) ).Returns(impl); }
/// <summary> /// Returns the progress for the assignment, for assignments that do /// not support combined submissions. (For assignments with combined /// submissions, progress information is redundant.) /// </summary> private async Task <AssignmentProgress> GetAssignmentProgressAsync( UserQuestionData userQuestionData) { if (userQuestionData.AssignmentQuestion.Assignment.CombinedSubmissions) { return(null); } return(await _assignmentProgressRetriever.GetAssignmentProgressAsync ( userQuestionData.AssignmentQuestion.AssignmentId, userQuestionData.AssignmentQuestionId, userQuestionData.UserId )); }
/// <summary> /// Regenerates the question. /// </summary> private RegenerateQuestionJob GetRegenerateJob( UserQuestionData userQuestionData, int?seed) { var generatedQuestionTemplate = (GeneratedQuestionTemplate) userQuestionData.AssignmentQuestion.Question; var maxSeed = generatedQuestionTemplate.NumSeeds ?? int.MaxValue; if (!seed.HasValue || seed.Value >= maxSeed) { seed = _seedGenerator.GenerateSeed(userQuestionData, maxSeed); } return(new RegenerateQuestionJob(userQuestionData, seed.Value)); }
/// <summary> /// Returns the number of attempts remaining for the given question, /// or null if the user has an unlimited number of attempts remaining. /// </summary> private int?GetAttemptsRemaining(UserQuestionData uqd) { if (IsClassAdmin(uqd)) { return(null); } if (!AllowAttemptsAfterCorrectSubmission(uqd) && AnyCorrectSubmissions(uqd)) { return(0); } return(uqd.AssignmentQuestion .LimitedAttempts() ? GetMaxAttempts(uqd) - GetNumAttempts(uqd) : null); }
/// <summary> /// Returns the contents of the submission for the most recent /// attempt at solving the question. /// </summary> public QuestionSubmission GetLastQuestionAttempt( UserQuestionData userQuestionData) { if (userQuestionData.LastQuestionSubmission == null) { return(null); } try { return(_jsonSerializer .Deserialize <QuestionSubmission>(userQuestionData.LastQuestionSubmission)); } catch { return(null); } }
/// <summary> /// Grades the submission. /// </summary> private async Task <ScoredQuestionResult> GradeSubmission( QuestionSubmission submission, Question resolvedQuestion, UserQuestionData userQuestionData, UserQuestionStatus userQuestionStatus, DateTime submissionDate) { if (!userQuestionStatus.AllowNewAttempt) { throw new InvalidOperationException("No attempts remaining."); } var scoredQuestionResult = await GradeQuestionAsync ( resolvedQuestion, submission ); if (userQuestionData.Submissions == null) { userQuestionData.Submissions = new List <UserQuestionSubmission>(); } var submissionContents = _jsonSerializer.Serialize(submission); var savedSubmission = new UserQuestionSubmission() { Score = scoredQuestionResult.Score, DateSubmitted = submissionDate, Seed = userQuestionData.Seed, CachedQuestionData = userQuestionData.CachedQuestionData, SubmissionContents = submissionContents }; userQuestionData.Submissions.Add(savedSubmission); userQuestionData.NumAttempts++; userQuestionData.Seed = null; if (userQuestionData.AssignmentQuestion.IsInteractive()) { userQuestionData.LastQuestionSubmission = submissionContents; } return(scoredQuestionResult); }
/// <summary> /// Applies a new generated question instance to a given question. /// </summary> private void ApplyQuestionGenerationResult( UserQuestionData userQuestionData, QuestionGenerationResult result, DateTime generationTime) { if (result.Error != null) { throw new InvalidOperationException("Failed to regenerate question."); } if (userQuestionData.Seed != result.Seed) { userQuestionData.LastQuestionSubmission = null; } userQuestionData.Seed = result.Seed; userQuestionData.CachedQuestionData = result.SerializedQuestion; userQuestionData.CachedQuestionDataTime = generationTime; }
/// <summary> /// Returns a mock QuestionStatusCalculator. /// </summary> private IQuestionStatusCalculator GetMockQuestionStatusCalculator( UserQuestionData userQuestionData, bool attemptsRemaining = true) { var statusCalculator = new Mock <IQuestionStatusCalculator>(); statusCalculator .Setup(m => m.GetQuestionStatus(userQuestionData)) .Returns ( new UserQuestionStatus ( numAttempts: 0, answeredCorrectly: false, numAttemptsRemaining: attemptsRemaining ? (int?)null : 0 ) ); return(statusCalculator.Object); }