Пример #1
0
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"
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
 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.")
Пример #6
0
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)
Пример #7
0
 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.")
Пример #8
0
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
Пример #9
0
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)
Пример #10
0
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"))
Пример #11
0
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
Пример #12
0
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}")
Пример #13
0
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))}")
Пример #14
0
 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.")
Пример #15
0
 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.")
Пример #16
0
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