/// <summary> /// Adds or updates a users answer. Creates new user if they're not in the cache, /// otherwise update answer. returns the number of questions answered. /// </summary> public int AddOrUpdate(Answer updateAnswer) { List<CacheAnswer> listAnswers; // Check for new user if(!_answerCache.TryGetValue(updateAnswer.ProfileId, out listAnswers)) { listAnswers = new List<CacheAnswer>(); _answerCache.Add(updateAnswer.ProfileId, listAnswers); } //Check if updating or adding new answer for(int i=0; i < listAnswers.Count; i++) { var a = listAnswers[i]; if(a.QuestionId==updateAnswer.QuestionId) { a.ChoiceBit = updateAnswer.ChoiceBit(); a.ChoiceAccept = updateAnswer.ChoiceAccept; a.ChoiceWeight = updateAnswer.ChoiceWeight; a.LastAnswered = DateTime.Now; //copy back to list since structs are passed by value listAnswers[i] = a; //found and updated answer, done return listAnswers.Count; } } //Got here: New answer listAnswers.Add(new CacheAnswer { QuestionId = updateAnswer.QuestionId, ChoiceBit = updateAnswer.ChoiceBit(), ChoiceAccept = updateAnswer.ChoiceAccept, ChoiceWeight = updateAnswer.ChoiceWeight, LastAnswered = DateTime.Now }); return listAnswers.Count; }
/// <summary> /// Makes sure an answer is valid by meeting the following requirements. Returns true /// if answer is valid, false if not. Has side effect of modifiying passed in Answer. /// /// - Answer isn't skipped (ChoiceIndex is not 0 or null) /// - Set Irrelevant values: If ChoiceWeight = 0 then all bits set to 1 on ChoiceAccept /// - ChoiceWeight must be 0, 1, 2, or 3 /// - ChoiceAccept can't be 0 /// /// </summary> public bool ValidateAnswer(Answer ans) { if (ans.ChoiceIndex == null || ans.ChoiceIndex == 0) return false; //Irrelevant if (ans.ChoiceWeight == 0) { // user chose irrelevant, mark all answers as acceptable ans.ChoiceAccept = 0xFF; } if (ans.ChoiceWeight != 0 && ans.ChoiceWeight != 1 && ans.ChoiceWeight != 2 && ans.ChoiceWeight != 3) { return false; } if (ans.ChoiceAccept == 0) return false; return true; }
/// <summary> /// /// Updates/Adds a users answer in the database. Called by AnswerQuestion which already /// validated the answer so we don't have to here. /// /// </summary> protected async Task AnswerDbAsync(Answer ans) { var db = new OkbDbContext(); var dbAns = await db.Answers.FindAsync(ans.ProfileId, ans.QuestionId); //Are we updating a question or adding a new one? if (dbAns == null) { //new answer ans.LastAnswered = DateTime.Now; db.Answers.Add(ans); } else { //update answer dbAns.ChoiceIndex = ans.ChoiceIndex; dbAns.ChoiceWeight = ans.ChoiceWeight; dbAns.ChoiceAccept = ans.ChoiceAccept; dbAns.LastAnswered = DateTime.Now; } await db.SaveChangesAsync(); }
/// <summary> /// Simulations answering one question to see how fast we can insert answers into the DB. /// Should be called in a loop and given a Random object. /// </summary> public void SimulateAnsweringQuestion(Random rand) { var db = new OkbDbContext(); var ans = new Answer { ProfileId = rand.Next(2, 200000), QuestionId = (short)rand.Next(201, 1243), ChoiceIndex = 1, ChoiceWeight = 1, ChoiceAccept = 1, LastAnswered = DateTime.Now }; try { db.Answers.Add(ans); db.SaveChanges(); } catch (Exception) { throw; } }
/// <summary> /// Returns true if the other answer matches my requirements /// </summary> public bool IsMatch(Answer otherAnswer) { return (otherAnswer.ChoiceBit() & ChoiceAccept) != 0; }
private static void ChoicePart2(TagBuilder tag, Answer answer, Answer compareAnswer, int index) { AcceptableChoice(tag, index, answer.ChoiceAccept); if (compareAnswer != null) { //we both answered this question - add comparison classes if (compareAnswer.ChoiceIndex == index) MatchChoice(tag, index, answer.ChoiceAccept); } }
private static void ChosenChoice(TagBuilder tag, int index, Answer answer) { if (index != answer.ChoiceIndex) { tag.AddCssClass("question-choice-fade"); } else { tag.AddCssClass("question-choice-bold"); } }
public static HtmlString ShowAnswersMe(this HtmlHelper htmlHelper, Answer answer, IList<string> choices) { var html = ""; for (int i = 1; i <= choices.Count; i++) { var tag = new TagBuilder("li"); tag.AddCssClass(answer.ChoiceIndex == i ? "question-choice-check" : "question-choice-bullet"); tag.AddCssClass(answer.IsMatch(i) ? "question-choice-green" : "question-choice-strike"); tag.InnerHtml = choices[i - 1]; html += tag.ToString(); } return new HtmlString(html); }
/////////////////////////////////////////////////////////////////////////////////////// public static HtmlString ShowAnswer(this HtmlHelper htmlHelper, Answer answer, Answer otherAnswer, IList<string> choices) { if (answer.ChoiceIndex == null || otherAnswer.ChoiceIndex == null) { return new HtmlString("INVALID ANSWER"); } var tag = new TagBuilder("span"); tag.AddCssClass(otherAnswer.IsMatch(answer) ? "question-choice-green" : "question-choice-red"); tag.InnerHtml = choices[((int)answer.ChoiceIndex - 1) % choices.Count]; return new HtmlString(tag.ToString()); }
/// <summary> /// Answers a question by adding it to the db. Will update if answer exists, otherwise adds a new /// answer. The controller will take care of updating answer in cache. Validation peformed by the caller. /// </summary> public async Task AnswerAsync(Answer ans) { await AnswerDbAsync(ans); }
/// <summary> /// Skip the question. Returns the next 2 questions like Answer but they aren't shown /// asynchronously - make the user wait for skipping questions. Doesn't update the /// answer cache. /// </summary> public async Task<JsonResult> Skip(AnswerViewModel input) { var profileId = GetMyProfileId(); var answer = new Answer { ProfileId = profileId, QuestionId = (short)input.QuestionId, ChoiceIndex = null, ChoiceAccept = 0, ChoiceWeight = 0, }; await _quesRepo.AnswerAsync(answer); var nextQuestions = _quesRepo.Next2Questions(profileId); return Json(nextQuestions); }
/// <summary> /// Add/Updates a user's answer in the database, and then calls MatchApiClient to /// update the cache. Responsible for keeping DB and cache in sync so wraps these /// operations in a transaction. /// /// - If getNextFlag = true get the next 2 questions and return them as JSON /// - Otherwise return the user's validated answer /// - Sets forceRecalculateMatches = true if user answered less than 10 questions (so new users can see results immediately) /// /// More info about TransactionScope (limitations): /// https://msdn.microsoft.com/en-us/data/dn456843.aspx /// /// </summary> public async Task<JsonResult> Answer(AnswerViewModel input, bool getNextFlag = true) { var profileId = GetMyProfileId(); _matchApiClient = GetMatchApiClient(); var answer = new Answer { ProfileId = profileId, QuestionId = (short)input.QuestionId }; //Convert checkboxes to bit array for Acceptable choices byte acceptBits = 0; foreach (var index in input.ChoiceAccept) { acceptBits |= (byte)(1 << (index - 1)); } answer.ChoiceIndex = (byte)input.ChoiceIndex; answer.ChoiceAccept = acceptBits; answer.ChoiceWeight = (byte)input.ChoiceImportance; //We should do some user input validation if(!_quesRepo.ValidateAnswer(answer)) { //invalid answer - return not OK Response.StatusCode = (int)HttpStatusCode.BadRequest; return Json(null); } // Isolation Level: Read Committed // Timeout : 5 minutes var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromMinutes(5) }; int count; //Update DB and Cache in a transaction //By default if scope.Complete() isn't called the transaction is rolled back. using (var scope = new TransactionScope(TransactionScopeOption.Required, options, TransactionScopeAsyncFlowOption.Enabled)) { var t1 = _quesRepo.AnswerAsync(answer); var t2 = _matchApiClient.AnswerAsync(answer); //await Task.WhenAll(t1, t2); await t1; count = await t2; scope.Complete(); } //Update the Activity feed if(IsOkToAddActivity(OkbConstants.ActivityCategories.AnsweredQuestion)) { //Get the question text var ques = _quesRepo.GetQuestion(input.QuestionId); var choiceText = ""; choiceText = ques.Choices[((int)answer.ChoiceIndex - 1) % ques.Choices.Count]; _feedRepo.AnsweredQuestionActivity(profileId, ques.Text, choiceText); UpdateActivityLastAdded(OkbConstants.ActivityCategories.AnsweredQuestion); } //set forceRecalculateMatches = true if answered less than 10 questions if (count < 10) { Session[OkbConstants.FORCE_RECALCULATE_MATCHES] = true; } IList<QuestionModel> nextQuestions = null; //Get the next questions if (getNextFlag) { nextQuestions = _quesRepo.Next2Questions(profileId); return Json(nextQuestions); } else { return Json(answer); } }