def get_default_questions(language, student_id): """Read and return questions from json files. For a language, read questions from a json file and add their ids to the question queues in the student's embedded document. Do this for two separate queues: "front" and "back". Randomly pick between the "front" and "back" queues, and return the first question in one of those queues, as well the question side: "front" or "back". Argument: language: str student_id: str -- argument for ObjectId() in MongoDB Return: Tuple: (Question object, question side) """ f_question_ids, f_question = _read_question_from_json(language, "front") b_question_ids, b_question = _read_question_from_json(language, "back") # since we're using default questions, student has never studied # this langauge, and so there should be no embedded document # in Student document for this language; if that's the case, create one # check this EmbeddedDocumentList field exists before adding to it; # if all list items get deleted, then MongoDB will delete the list field # so guard against that assert not Student.objects(id=student_id, __raw__={"language_progress": None}).first() # get embedded document for this student for this language language_embed_doc = Student.objects( id=student_id).first().language_progress.filter( language=language) # only add a document for this language if it doesn't exist if not language_embed_doc: # create embedded document for this language student = Student.objects(id=student_id).first() lang_prog = LanguageProgress( language=language, f_question_queue=f_question_ids, b_question_queue=b_question_ids ) # add this new embedded document to list of all studied languages student.update(push__language_progress=lang_prog) student.save() # pick randomly between the front and back question and return if random.choices([0, 1], cum_weights=[40, 100])[0]: return f_question, "front" else: return b_question, "back"
def get_queue_question(language, student_id): """Read and return first question stored in student's question queue in db. Read question queue in Student document, and use the id to retrieve a question from Question collection in db. Randomly pick between the "front" and "back" question queues. Return the question as a Question object, or None if no questions in queue. Argument: language: str student_id = str -- argument for ObjectId() in MongoDB Return: Tuple: (Question object or None, question side or None) """ mongoengine.connect("lalang_db", host="localhost", port=27017) # check this EmbeddedDocumentList field exists before adding to it; # if all list items get deleted, then MongoDB will delete the list field # so guard against that assert not Student.objects(id=student_id, __raw__={"language_progress": None}).first() # get embedded document for this student for this language language_embed_doc = Student.objects( id=student_id).first().language_progress.filter( language=language) # in case the list or queue doesn't exist try: # check if the embedded document exists and question queues # are not empty if language_embed_doc and language_embed_doc[0].f_question_queue: # if there are questions in queue, query and add them to list # pick randomly between the front and back question queues if random.choices([0, 1], cum_weights=[40, 100])[0]: side = "front" question_id = language_embed_doc[0].f_question_queue[0] else: side = "back" question_id = language_embed_doc[0].b_question_queue[0] return Question.objects(id=question_id).first(), side else: return None, None except IndexError: return None, None
def create_student(email, username, first_name, last_name, password, temp): """Create and return new Student object; also, save to database.""" student = Student( email=email, username=username, alt_id=ObjectId(), first_name=first_name, last_name=last_name, password=password, temp=temp ) student.save() return student.id
def login(): if current_user.is_authenticated and not current_user.temp: return redirect(url_for("home")) form = StudentLogin() if form.validate_on_submit(): student = Student.objects(email=form.email.data.lower()).first() if student and bcrypt.check_password_hash(student.password, form.password.data): if current_user.is_authenticated and current_user.temp: # user has been using a temp account during session # but now wants to log in, so log out from the temp account logout_user() login_user(student, remember=form.remember.data) next_page = request.args.get("next") if next_page: if is_safe_url(next_page): return redirect(next_page) else: return abort(400) else: return redirect(url_for("home")) else: flash("We could not log you in. \ Please check your email and password.", "danger") return render_template("login.html", form=form)
def validate_email(self, email): if email.data and (email.data.lower() != current_user.email): # check that email is unique student = Student.objects(email=email.data.lower()).first() if student: raise ValidationError("An account with this email \ address already exists.")
def request_reset(): if current_user.is_authenticated and not current_user.temp: return redirect(url_for("home")) form = RequestResetForm() if form.validate_on_submit(): student = Student.objects(email=form.email.data).first() send_reset_email(student) flash("Please check your email for a password reset link.", "info") return redirect(url_for("login")) return render_template("request-reset.html", form=form)
def validate_username(self, username): if len(username.data) == 1: raise ValidationError("User name has to be between 2 and 20\ characters long.") elif username.data and (username.data.lower() != current_user.username): # check that username is unique student = Student.objects(username=username.data.lower()).first() if student: raise ValidationError("An account with this username \ already exists.")
def dict_to_student_obj(student_as_dict): """Take student represented as dictionary and return Student object.""" # making a copy in order to not lose email key in dict passed as argument student_as_dict_copy = copy.deepcopy(student_as_dict) email = student_as_dict_copy.pop("email") s_obj = Student(email=email) # student_as_dict no longer has key "email" for k, v in student_as_dict_copy.items(): setattr(s_obj, k, v) return s_obj
def register(): if current_user.is_authenticated and not current_user.temp: return redirect(url_for("home")) form = StudentRegister() if form.validate_on_submit(): hash_password = bcrypt.generate_password_hash(form.password.data)\ .decode("utf-8") if not current_user.is_authenticated: # user has never registered or answered a question # (ie no temp student document), so create a new Student document student = Student(email=form.email.data.lower(), username=form.username.data, alt_id=ObjectId(), first_name=form.first_name.data, last_name=form.last_name.data, password=hash_password, temp=False) student.save() else: # user has answered a question before, but never registered; # so update the temp document user has been using and # flag it as not temporary current_user.email = form.email.data.lower() current_user.username = form.username.data current_user.first_name = form.first_name.data current_user.last_name = form.last_name.data current_user.password = hash_password current_user.temp = False current_user.save() flash(f"An account for {form.email.data} \ has been created successfully.", "success") logout_user() return redirect(url_for("login")) return render_template("register.html", form=form)
def reset_password(token): if current_user.is_authenticated and not current_user.temp: return redirect(url_for("home")) student = Student.verify_reset_token(token) if student: form = ResetPasswordForm() if form.validate_on_submit(): if current_user.is_authenticated and current_user.temp: logout_user() student.password = bcrypt.generate_password_hash( form.password.data).decode("utf-8") student.save() flash("Password reset successfully. Please log in.", "success") return redirect(url_for("login")) return render_template("reset-password.html", form=form) flash("Invalid or expired token.", "warning") return redirect(url_for("request_reset"))
def create_temp_student(): """Create and return a temporary Student object; also save to database. Until a student registers, his/her information is saved using a temporary Student document. This document is generated the first time the student answers a question. Once the student registers, the Student document is updated. """ random_string = str(uuid.uuid4())[7:27] student = Student( # username must be no more than 20 characters username=random_string, email=random_string + "@fantasy.com", alt_id=ObjectId(), temp=True ) # save the document; if the username not unique, generate new one and retry try: student.save() except mongoengine.NotUniqueError: while True: random_string = str(uuid.uuid4())[7:27] student.username = random_string student.email = random_string + "@fantasy.com" try: student.save() break except mongoengine.NotUniqueError: continue logging.info(f"Just created temp student with id: {student.id}") logging.info(f"about to add questions to the queue of student {student.id}") # add default questions to the queues in Student document for lang in SUPPORTED_LANGUAGES: get_default_questions(lang, student.id) return student
You will need to add the "language_progress" field back - currently not implemented. All parameters are hard-coded right now. For this to work, you need to comment out "from lalang import routes" in the __init__.py file for lalang package. Otherwise, new embedded documents will be created upon the execution of this module. """ import mongoengine import sys sys.path.append("C:\\Users\\Lukasz\\Python\\ErroresBuenos") from lalang.db_model import Student # db_in = input("Which database should the questions be deleted from? ") db_in = "lalang_db" student_id = "5bb6b5bffde08a535c580608" mongoengine.connect(db_in, host="localhost", port=27017) language_embed_docs = Student.objects(id=student_id).first().language_progress num_del_records = language_embed_docs.delete() language_embed_docs.save() print(f"{num_del_records} embedded documents deleted for \ Student id: {student_id}")
def save_answer(*, student_id, language, question_id, question_side, user_answer, answer_correct=None, audio_answer_correct=None): """ Save user's answer in StudentHistory and Student documents. Add new questions to the queue in db, if the queue is short. Return: None """ mongoengine.connect("lalang_db", host="localhost", port=27017) # if no Student History collection in database, create one if not StudentHistory.objects: create_stud_hist_coll() if question_side[0] == "back": if len(user_answer[0]) > 0: user_answer = sanitize_input(user_answer[0]) else: user_answer = "" answer_correct = json.loads(answer_correct[0]) audio_answer_correct = json.loads(audio_answer_correct[0]) queue = "b_question_queue" if question_side[0] == "front": user_answer = user_answer[0] queue = "f_question_queue" # check if student has seen this question before stud_hist = StudentHistory.objects(student_id=str(current_user.id), question_id=question_id[0], question_side=question_side[0]).first() if stud_hist: logging.info(f"student_id passed: {str(current_user.id)}") logging.info(f"student_id received: {stud_hist.student_id}") logging.info(f"question_id passed: {question_id[0]}") logging.info(f"question_id received: {stud_hist.question_id}") logging.info(f"StudentHistory doc id returned by db query: {str(stud_hist.id)}") logging.info("student has seen this question before,\ so we'll update the document") # student answered this question before, so update the document. stud_hist.update(inc__attempts_count=1) stud_hist.update(set__last_attempted=datetime.datetime.now (tz=pytz.UTC)) if question_side[0] == "front": stud_hist.update(push__answer=user_answer) # For "back" sided questions, add the answer string only if # it's not already stored (exact match), and if it's not an empty string if (question_side[0] == "back"): if user_answer and (not user_answer in stud_hist.answer): stud_hist.update(push__answer=user_answer) if not stud_hist.answer_correct and answer_correct: stud_hist.update(set__answer_correct=True) if stud_hist.answer_correct and not answer_correct: stud_hist.update(set__answer_correct=False) if (not stud_hist.audio_answer_correct and audio_answer_correct): stud_hist.update(set__audio_answer_correct=True) if (stud_hist.audio_answer_correct and not audio_answer_correct): stud_hist.update(set__audio_answer_correct=False) try: stud_hist.save() except BaseException as err: logging.info(f"failed to update a doc in StudentHistory. \ Error: {err}") else: logging.info("first time the student saw the question,\ so create a document for it") # first time the student saw the question, so create a document for it stud_hist = StudentHistory( student_id=current_user.id, language=language[0], question_id=ObjectId(question_id[0]), question_side=question_side[0], attempts_count=1, last_attempted=datetime.datetime.now(tz=pytz.UTC) ) # if answer not an empty string, save it if user_answer: stud_hist.answer = [user_answer] if question_side[0] == "back": stud_hist.answer_correct = answer_correct stud_hist.audio_answer_correct = audio_answer_correct try: stud_hist.save() except BaseException as err: logging.info(f"failed to save to StudentHistory. Error: {err}") # now update the Student document student = Student.objects(id=str(current_user.id)).first() if question_side[0] == "back": if answer_correct: student.update(inc__num_correct_answers=1) if question_side[0] == "front": if user_answer == "2": student.update(inc__num_correct_answers=1) # update the question queue and answer stacks in Student document # get embedded document for this student for this language language_embed_doc = student.language_progress.filter( language=stud_hist.language) # update the embedded document for this language language_embed_doc[0].last_studied = datetime.datetime.now(tz=pytz.UTC) if question_side[0] == "back": if stud_hist.answer_correct: language_embed_doc[0].b_answered_corr_stack.append( stud_hist.question_id) else: # if the wrong answer stack is too big, reduce it # before adding the latest wrong answer _reduce_wrong_stack(language_embed_doc[0], "back") language_embed_doc[0].b_answered_wrong_stack.append( stud_hist.question_id) if question_side[0] == "front": if user_answer == "2": language_embed_doc[0].f_answered_corr_stack.append( stud_hist.question_id) if user_answer == "1": language_embed_doc[0].f_answered_review_stack.append( stud_hist.question_id) if user_answer == "0": # if the wrong answer stack is too big, reduce it # before adding the latest wrong answer _reduce_wrong_stack(language_embed_doc[0], "front") language_embed_doc[0].f_answered_wrong_stack.append( stud_hist.question_id) # remove the answered question from the queue assert getattr(language_embed_doc[0], queue)[0] == stud_hist.question_id getattr(language_embed_doc[0], queue).pop(0) logging.info(f"Removed question from queue") logging.info( f"Number of questions in queue after pop: {len(getattr(language_embed_doc[0], queue))}") language_embed_doc.save() logging.info( f"Number of questions in queue after save: {len(getattr(language_embed_doc[0], queue))}") # if queue doesn't have enough questions, add more questions if len(getattr(language_embed_doc[0], queue)) < MIN_QUESTIONS_IN_QUEUE: prep_questions(language[0], question_side[0], current_user.id, NUM_QUESTIONS_TO_LOAD) logging.info(f"added new questions to queue") # query database again to check the size of the queue language_embed_doc = student.language_progress.filter( language=stud_hist.language) logging.info( f"Number of questions in queue after new query: {len(getattr(language_embed_doc[0], queue))}")
def validate_email(self, email): # check that email exists student = Student.objects(email=email.data.lower()).first() if not student: raise ValidationError("No such account exists. Please register.")
def validate_username(self, username): # check that username is unique student = Student.objects(username=username.data.lower()).first() if student: raise ValidationError("An account with this username \ already exists.")
def prep_questions(language, side, student_id, num_questions_needed): """Pick new questions and save them in db. Randomly query Question collection for more questions in the database, add them to student's queue in Student document - i.e. save them in db. Argument: language: str side: "front" or "back" - which side of the question card student_id = str or ObjectId() from MongoDB num_questions_needed: integer Precondition: num_questions_needed > 0 Return: None """ # set the flags for the queues and stacks, either "f" or "b" s = side[0] # set the flags for the queues and stacks from the opposite question side if s == "f": op = "b" else: op = "f" num_questions_added = 0 # force to string if ObjectId passed student_id = str(student_id) mongoengine.connect("lalang_db", host="localhost", port=27017) query_args = {"language": f"{language}", "description": "word flashcard"} questions_iter = Question.objects(__raw__=query_args) # get embedded document for this student for this language language_embed_doc = Student.objects( id=student_id).first().language_progress.filter( language=language) logging.info("Num of questions in queue at beginning of prep_questions: ") logging.info(len(getattr(language_embed_doc[0], f"{s}_question_queue"))) # if answered_corr_stack is "big", reduce it, i.e. release questions # for review size_corr_stack = len(getattr(language_embed_doc[0], f"{s}_answered_corr_stack")) logging.info(f"size_corr_stack: {size_corr_stack}") if size_corr_stack > START_REVIEW: logging.info("We need to release questions for review") questions_to_release = (size_corr_stack - BASE_ANSWERED_CORRECTLY) logging.info(f"Number of questions to release: {questions_to_release}") del getattr(language_embed_doc[0], f"{s}_answered_corr_stack")[:questions_to_release] logging.info("Questions released for review.") # draw random question from all possible questions # keep drawing questions until you get {num_questions_needed} questions while num_questions_added < num_questions_needed: logging.info("In the while loop in prep_questions") q_used = False duplicate_check_passed = False # for side="back", draw from three sources: # 25% of the time from f_answered_wrong_stack, # i.e. the opposite - "front" - side of the question # 25% of the time from b_answered_wrong_stack # 50% of the time from all questions in the Question collection # for side="front", draw from four sources: # 20% of the time from b_answered_wrong_stack, # i.e. the opposite - "back" - side of the question # 15% of the time from f_answered_wrong_stack # 50% of the time from all questions in the Question collection # 15% of the time from f_answered_review_stack choices = [0, 1, 2, 3] if side == "back": weights = [25, 50, 100, 100] draw_bin = random.choices(choices, cum_weights=weights)[0] else: weights = [20, 35, 85, 100] draw_bin = random.choices(choices, cum_weights=weights)[0] # if picking from all questions in the Question collection if draw_bin == 2: drawn_id = _draw_random_question(questions_iter) # if picking from wrong_stack from the opposite side if draw_bin == 0: # check if the opposite answered_wrong_stack is big enough # to draw from; if so, draw from the first 10 questions if (len(getattr(language_embed_doc[0], f"{op}_answered_wrong_stack")) >= MIN_WRONG_STACK_SIZE_FOR_DRAW): pick = random.choice(range(10)) drawn_id = getattr(language_embed_doc[0], f"{op}_answered_wrong_stack")[pick] else: # it didn't work, so just draw a random question # from all questions in the collection drawn_id = _draw_random_question(questions_iter) # if picking from wrong_stack from the same side if draw_bin == 1: # check if the answered_wrong_stack for the same side is big enough # to draw from; if so, use the first question in the stack if (len(getattr(language_embed_doc[0], f"{op}_answered_wrong_stack")) >= MIN_WRONG_STACK_SIZE_FOR_DRAW): drawn_id = getattr(language_embed_doc[0], f"{op}_answered_wrong_stack").pop(0) # we already know that this drawn question is not present # in any relevant queue or stack, so it can be used. # So, set a flag to stop further verification. duplicate_check_passed = True else: # it didn't work, so just draw a random question # from all questions in the collection drawn_id = _draw_random_question(questions_iter) # if picking from f_answered_review_stack for "front" sided question if draw_bin == 3: # check if f_answered_review_stack is big enough to draw from; # if so, use the first question in the stack if (len(language_embed_doc[0].f_answered_review_stack) >= MIN_REVIEW_STACK_SIZE_FOR_DRAW): drawn_id = language_embed_doc[0].f_answered_review_stack.pop(0) # we already know that this drawn question is not present # in any relevant queue or stack, so it can be used. # So, set a flag to stop further verification. duplicate_check_passed = True else: # it didn't work, so just draw a random question # from all questions in the collection drawn_id = _draw_random_question(questions_iter) # check that the drawn question is not in the queue, or the two stacks # of answered questions; if not, then add it to the queue if not duplicate_check_passed: for q_id in getattr(language_embed_doc[0], f"{s}_question_queue"): if drawn_id == q_id: q_used = True break if not duplicate_check_passed and not q_used: for q_id in getattr(language_embed_doc[0], f"{s}_answered_wrong_stack"): if drawn_id == q_id: q_used = True break if not q_used: for q_id in getattr(language_embed_doc[0], f"{s}_answered_corr_stack"): if drawn_id == q_id: q_used = True break if duplicate_check_passed or not q_used: # question not used, so add its id to the Student queue of questions getattr(language_embed_doc[0], f"{s}_question_queue").append(drawn_id) language_embed_doc.save() num_questions_added += 1 logging.info(f"Num of questions added to queue: {num_questions_added}") # query again to check the size of the question queue language_embed_doc = Student.objects( id=student_id).first().language_progress.filter( language=language) logging.info("Num of questions in queue at the end of prep_questions: ") logging.info(len(getattr(language_embed_doc[0], f"{s}_question_queue"))) return None