/// <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);
            }
        }
Exemple #3
0
        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);
        }
Exemple #6
0
 /// <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));
 }
Exemple #7
0
        /// <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);
        }
Exemple #8
0
        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
                   ));
        }
Exemple #10
0
        /// <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();
        }
Exemple #14
0
        /// <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);
        }
Exemple #15
0
        /// <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);
        }
Exemple #16
0
        /// <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);
        }
Exemple #17
0
 /// <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>());
        }
Exemple #22
0
        /// <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);
        }
Exemple #23
0
 /// <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));
        }
Exemple #26
0
        /// <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;
        }
Exemple #30
0
        /// <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);
        }