Beispiel #1
0
    def test_authorize_anonymous(self):
        random_uuid = str(uuid.uuid4())
        new_password = self.faker.password()
        anonymous_user = User.create_anonymous(random_uuid, new_password)
        self.db.session.add(anonymous_user)
        self.db.session.commit()

        result = User.authorize_anonymous(random_uuid, new_password)

        assert result is not None and result == anonymous_user
Beispiel #2
0
def reset_password(email):
    """
    This endpoint can be used to rest a users password.
    To do this a uniquecode is required.
    """
    last_code = UniqueCode.last_code(email)
    code = request.form.get("code", None)
    if not (last_code == code):
        return make_error(400, "Invalid code")

    password = request.form.get("password", None)
    if len(password) < 4:
        return make_error(400, "Password should be at least 4 characters long")

    user = User.find(email)
    if user is None:
        return make_error(400, "Email unknown")
    user.update_password(password)
    db_session.commit()

    # Delete all the codes for this user
    for x in UniqueCode.all_codes_for(email):
        db_session.delete(x)
    db_session.commit()

    return "OK"
Beispiel #3
0
def add_anon_user():
    """

    Creates anonymous user, then redirects to the get_session
    endpoint. Returns a session

    """

    # These two are post parameters required by the method
    uuid = request.form.get("uuid", None)
    password = request.form.get("password", None)

    # These two are optional
    language_code = request.form.get("learned_language_code", None)
    native_code = request.form.get("native_language_code", None)

    try:
        new_user = User.create_anonymous(uuid, password, language_code,
                                         native_code)
        db_session.add(new_user)
        db_session.commit()
    except ValueError as e:
        return bad_request("Could not create anon user.")
    except sqlalchemy.exc.IntegrityError as e:
        return bad_request(
            "Could not create anon user. Maybe uuid already exists?")
    return get_anon_session(uuid)
Beispiel #4
0
def reset_password(email):
    """
    This endpoint can be used to rest a users password.
    To do this a uniquecode is required.
    """
    last_code = UniqueCode.last_code(email)
    code = request.form.get("code", None)
    if not (last_code == code):
        return make_error(400, "Invalid code")

    password = request.form.get("password", None)
    if len(password) < 4:
        return make_error(400, "Password should be at least 4 characters long")

    user = User.find(email)
    if user is None:
        return make_error(400, "Email unknown")
    user.update_password(password)
    db_session.commit()

    # Delete all the codes for this user
    for x in UniqueCode.all_codes_for(email):
        db_session.delete(x)
    db_session.commit()

    return "OK"
Beispiel #5
0
def add_anon_user():
    """

        Creates anonymous user, then redirects to the get_session
        endpoint. Returns a session

    """

    # These two are post parameters required by the method
    uuid = request.form.get("uuid", None)
    password = request.form.get("password", None)

    # These two are optional
    language_code = request.form.get("learned_language_code", None)
    native_code = request.form.get("native_language_code", None)

    try:
        new_user = User.create_anonymous(uuid, password, language_code, native_code)
        db_session.add(new_user)
        db_session.commit()
    except ValueError as e:
        flask.abort(flask.make_response("Could not create anon user.", 400))
    except sqlalchemy.exc.IntegrityError as e:
        flask.abort(flask.make_response("Could not create anon user. Maybe uuid already exists?", 400))
    return get_anon_session(uuid)
        def _upgrade_to_teacher(email):
            from zeeguu_core.model import User, Teacher
            db = zeeguu_core.db

            u = User.find(email)
            db.session.add(Teacher(u))
            db.session.commit()
def user_article_history(user_id):
    user = User.find_by_id(user_id)

    sessions = UserReadingSession.find_by_user(user.id)

    dates = {}
    for each in sessions[:-650:-1]:
        if each.article and each.duration > 1000:
            if not dates.get(each.human_readable_date()):
                dates[each.human_readable_date()] = []

            # user_article = UserArticle.find(user, each.article)
            events_in_this_session = each.events_in_this_session()

            has_like = False
            feedback = ""
            difficulty = ""
            for event in events_in_this_session:
                if event.is_like():
                    has_like = True
                if event.is_feedback():
                    feedback = event.value
                    difficulty = "fk: " + str(each.article.fk_difficulty)

            dates[each.human_readable_date()].append({
                'date':
                each.human_readable_date(),
                'duration':
                each.human_readable_duration(),
                'start':
                each.start_time.strftime("%H:%M:%S"),
                'article':
                each.article.title,
                'liked':
                has_like,
                'feedback':
                feedback,
                'difficulty':
                difficulty
            })

    text_result = f"<title>{user.name}</title>"
    text_result += f"<h1>{user.name} ({user.id})</h1><br/>"
    previous_title = ""
    for date in dates:
        text_result += date + "<br/>"
        for session in dates[date]:
            if previous_title != session['article']:
                text_result += f"<br/>&nbsp;&nbsp;<b> {session['article']} </b><br/>"
            text_result += f"&nbsp;&nbsp;&nbsp;&nbsp; {session['duration']} ({session['start']})"
            if session['liked']:
                text_result += ' (LIKED) '
            text_result += session['difficulty'] + " " + session[
                'feedback'] + " <br/>"
            previous_title = session['article']

        text_result += "<br/><br/>"

    return text_result
def recompute_for_users():
    """

        recomputes only those caches that are already in the table
        and belong to a user. if multiple users have the same preferences
        the computation is donne only for the first because this is how
        recompute_recommender_cache_if_needed does.

        To think about:
        - what happens when this script is triggered simultaneously
        with triggering recompute_recommender_cache_if_needed from
        the UI? will there end up be duplicated recommendations?
        should we add a uninque constraint on (hash x article)?

        Note:

        in theory, the recomputing should be doable independent of users
        in practice, the recompute_recommender_cache takes the user as input.
        for that function to become independent of the user we need to be
        able to recover the ids of the languages, topics, searchers, etc. from the
        content_hash
        to do this their ids would need to be comma separated

        OTOH, in the future we might still want to have a per-user cache
        because the recommendations might be different for each user
        since every user has different language levels!!!

    :param existing_hashes:
    :return:
    """
    already_done = []
    for user_id in User.all_recent_user_ids():
        try:
            user = User.find_by_id(user_id)
            reading_pref_hash = reading_preferences_hash(user)
            if reading_pref_hash not in already_done:
                recompute_recommender_cache_if_needed(user, session)
                zeeguu_core.log_n_print(
                    f"Success for {reading_pref_hash} and {user}")
                already_done.append(reading_pref_hash)
            else:
                zeeguu_core.log_n_print(
                    f"nno need to do for {user}. hash {reading_pref_hash} already done"
                )
        except Exception as e:
            zeeguu_core.log_n_print(f"Failed for user {user}")
def _link_teacher_cohort(user_id, cohort_id):
    '''
        Takes user_id and cohort_id and links them together in teacher_cohort_map table.
    '''
    from zeeguu_core.model import TeacherCohortMap
    user = User.find_by_id(user_id)
    cohort = Cohort.find(cohort_id)
    db.session.add(TeacherCohortMap(user, cohort))
    db.session.commit()
    return 'added teacher_cohort relationship'
def _link_teacher_cohort(user_id, cohort_id):
    '''
        Takes user_id and cohort_id and links them together in teacher_cohort_map table.
    '''
    from zeeguu_core.model import TeacherCohortMap
    user = User.find_by_id(user_id)
    cohort = Cohort.find(cohort_id)
    db.session.add(TeacherCohortMap(user, cohort))
    db.session.commit()
    return 'added teacher_cohort relationship'
Beispiel #11
0
def fix_bookmark_priorities(USER_ID):
    print(f"fixing for user {USER_ID}")
    user = User.find_by_id(USER_ID)

    all_users_bookmarks = user.all_bookmarks()
    for each in all_users_bookmarks:
        each.update_fit_for_study()
    db.session.commit()

    BookmarkPriorityUpdater.update_bookmark_priority(db, user)
    print(f"... OK for {len(all_users_bookmarks)} bookmarks")
Beispiel #12
0
def add_user(email):
    """

        Creates user, then redirects to the get_session
        endpoint. Returns a session

    """

    password = request.form.get("password")
    username = request.form.get("username")
    invite_code = request.form.get("invite_code")
    cohort_name = ''

    if password is None or len(password) < 4:
        return make_error(400, "Password should be at least 4 characters long")

    if not (_valid_invite_code(invite_code)):
        return make_error(
            400, "Invitation code is not recognized. Please contact us.")

    try:

        cohort = Cohort.query.filter_by(inv_code=invite_code).first()

        if cohort:
            # if the invite code is from a cohort, then there has to be capacity
            if not cohort.cohort_still_has_capacity():
                return make_error(
                    400, "No more places in this class. Please contact us.")

            cohort_name = cohort.name

        new_user = User(email,
                        username,
                        password,
                        invitation_code=invite_code,
                        cohort=cohort)
        db_session.add(new_user)

        if cohort:
            if cohort.is_cohort_of_teachers:
                teacher = Teacher(new_user)
                db_session.add(teacher)

        db_session.commit()

        send_new_user_account_email(username, invite_code, cohort_name)

    except sqlalchemy.exc.IntegrityError:
        return make_error(401, "There is already an account for this email.")
    except ValueError:
        return make_error(400, "Invalid value")

    return get_session(email)
Beispiel #13
0
def send_code(email):
    """
    This endpoint generates a unique code that will be used to allow
    the user to change his/her password. The unique code is send to
    the specified email address.
    """
    from zeeguu_core.emailer.password_reset import send_password_reset_email

    try:
        User.find(email)
    except sqlalchemy.orm.exc.NoResultFound:
        return bad_request("Email unknown")

    code = UniqueCode(email)
    db_session.add(code)
    db_session.commit()

    send_password_reset_email(email, code)

    return "OK"
Beispiel #14
0
def user_article_history(user_id):
    user = User.find_by_id(user_id)

    sessions = UserReadingSession.find_by_user(user.id)

    dates = {}
    for each in sessions[:-650:-1]:
        if each.article and each.duration > 1000:
            if not dates.get(each.human_readable_date()):
                dates[each.human_readable_date()] = []

            # user_article = UserArticle.find(user, each.article)
            events_in_this_session = each.events_in_this_session()

            has_like = False
            feedback = ""
            difficulty = ""
            for event in events_in_this_session:
                if event.is_like():
                    has_like = True
                if event.is_feedback():
                    feedback = event.value
                    difficulty = "fk: " + str(each.article.fk_difficulty)

            dates[each.human_readable_date()].append(
                {
                    'date': each.human_readable_date(),
                    'duration': each.human_readable_duration(),
                    'start': each.start_time.strftime("%H:%M:%S"),
                    'article': each.article.title,
                    'liked': has_like,
                    'feedback': feedback,
                    'difficulty': difficulty
                }
            )

    text_result = f"<title>{user.name}</title>"
    text_result += f"<h1>{user.name} ({user.id})</h1><br/>"
    previous_title = ""
    for date in dates:
        text_result += date + "<br/>"
        for session in dates[date]:
            if previous_title != session['article']:
                text_result += f"<br/>&nbsp;&nbsp;<b> {session['article']} </b><br/>"
            text_result += f"&nbsp;&nbsp;&nbsp;&nbsp; {session['duration']} ({session['start']})"
            if session['liked']:
                text_result += ' (LIKED) '
            text_result += session['difficulty'] + " " + session['feedback'] + " <br/>"
            previous_title = session['article']

        text_result += "<br/><br/>"

    return text_result
Beispiel #15
0
    def test_create_anonymous(self):
        self.user = UserRule().user
        new_password = self.faker.password()
        self.user.update_password(new_password)

        user_to_check = User.create_anonymous(str(self.user.id), new_password,
                                              self.user.learned_language.code, self.user.native_language.code)

        assert user_to_check.email == str(self.user.id) + User.ANONYMOUS_EMAIL_DOMAIN
        assert user_to_check.name == str(self.user.id)
        assert user_to_check.learned_language == self.user.learned_language
        assert user_to_check.native_language == self.user.native_language
    def setUp(self, mock_invite_code):
        # idea from here:
        # https: // docs.pytest.org / en / latest / example / simple.html  # detect-if-running-from-within-a-pytest-run
        # allows the api translate_and_Bookmark to know that it's being called from the unit test
        # and use the reverse translator instead of the real translators

        app.testing = True
        self.app = app.test_client()
        zeeguu_core.db.create_all()

        response = self.app.post(f"/add_user/{TEST_EMAIL}", data=test_user_data)

        self.session = str(int(response.data))
        self.user = User.find(TEST_EMAIL)
def test_performance():
    me = User.find('*****@*****.**')

    a = datetime.now()

    res = find_articles_for_user(me)
    print(len(res))

    for each in res[0:20]:
        print(f"{each.topics} {each.published_time} {each.title}")

    b = datetime.now()

    print(b - a)
Beispiel #18
0
def get_session(email):
    """
    If the email and password match,
    a sessionId is returned as a string.
    This sessionId can to be passed
    along all the other requests that are annotated
    with @with_user in this file
    """

    password = request.form.get("password", None)
    if password == "":
        return make_error(401, "Password not given")

    if not User.email_exists(email):
        return make_error(401,
                          "There is no account associated with this email")

    user = User.authorize(email, password)
    if user is None:
        return make_error(401, "Invalid credentials")
    session = Session.for_user(user)
    db_session.add(session)
    db_session.commit()
    return str(session.id)
Beispiel #19
0
def get_session(email):
    """
        If the email and password match,
        a sessionId is returned as a string.
        This sessionId can to be passed
        along all the other requests that are annotated
        with @with_user in this file
    """
    password = request.form.get("password", None)
    if password is None:
        return make_error(400, "Password not given")
    user = User.authorize(email, password)
    if user is None:
        return make_error(401, "Invalid credentials")
    session = Session.for_user(user)
    db_session.add(session)
    db_session.commit()
    return str(session.id)
Beispiel #20
0
def reset_password(email):
    code = request.form.get("code", None)
    submitted_pass = request.form.get("password", None)

    user = User.find(email)
    last_code = UniqueCode.last_code(email)

    if submitted_code_is_wrong(last_code, code):
        return bad_request("Invalid code")
    if password_is_too_short(submitted_pass):
        return bad_request("Password is too short")
    if user is None:
        return bad_request("Email unknown")

    user.update_password(submitted_pass)
    delete_all_codes_for_email(email)

    db_session.commit()

    return "OK"
Beispiel #21
0
def get_anon_session(uuid):
    """
    
        If the uuid and password match, a  sessionId is
        returned as a string. This sessionId can to be passed
        along all the other requests that are annotated
        with @with_user in this file
        
    """
    password = request.form.get("password", None)

    if password is None:
        flask.abort(400)
    user = User.authorize_anonymous(uuid, password)
    if user is None:
        flask.abort(401)
    session = Session.for_user(user)
    db_session.add(session)
    db_session.commit()
    return str(session.id)
Beispiel #22
0
def get_anon_session(uuid):
    """
    
        If the uuid and password match, a  sessionId is
        returned as a string. This sessionId can to be passed
        along all the other requests that are annotated
        with @with_user in this file
        
    """
    password = request.form.get("password", None)

    if password is None:
        flask.abort(400)
    user = User.authorize_anonymous(uuid, password)
    if user is None:
        flask.abort(401)
    session = Session.for_user(user)
    db_session.add(session)
    db_session.commit()
    return str(session.id)
def bookmarks_for_article(article_id, user_id):
    """
    Returns the bookmarks of this user organized by date. Based on the
    POST arguments, it can return also the context of the bookmark as
    well as it can return only the bookmarks after a given date.

    :param (POST) with_context: If this parameter is "true", the endpoint
    also returns the text where the bookmark was found.

    :param (POST) after_date: the date after which to start retrieving
     the bookmarks. if no date is specified, all the bookmarks are returned.
     The date format is: %Y-%m-%dT%H:%M:%S. E.g. 2001-01-01T01:55:00

    """

    user = User.find_by_id(user_id)
    article = Article.query.filter_by(id=article_id).one()

    bookmarks = user.bookmarks_for_article(article_id,
                                           with_context=True,
                                           with_title=True)

    return json_result(dict(bookmarks=bookmarks, article_title=article.title))
def bookmarks_for_article(article_id, user_id):
    """
    Returns the bookmarks of this user organized by date. Based on the
    POST arguments, it can return also the context of the bookmark as
    well as it can return only the bookmarks after a given date.

    :param (POST) with_context: If this parameter is "true", the endpoint
    also returns the text where the bookmark was found.

    :param (POST) after_date: the date after which to start retrieving
     the bookmarks. if no date is specified, all the bookmarks are returned.
     The date format is: %Y-%m-%dT%H:%M:%S. E.g. 2001-01-01T01:55:00

    """

    user = User.find_by_id(user_id)
    article = Article.query.filter_by(id=article_id).one()

    bookmarks = user.bookmarks_for_article(article_id, with_context=True, with_title=True)

    return json_result(dict(
        bookmarks = bookmarks,
        article_title = article.title
    ))
def create_account(db_session, username, password, invite_code, email, learned_language=None, native_language=None):
    cohort_name = ""
    if password is None or len(password) < 4:
        raise Exception("Password should be at least 4 characters long")

    if not valid_invite_code(invite_code):
        raise Exception("Invitation code is not recognized. Please contact us.")

    cohort = Cohort.query.filter_by(inv_code=invite_code).first()
    if cohort:
        if cohort.cohort_still_has_capacity():
            cohort_name = cohort.name
        else:
            raise Exception("No more places in this class. Please contact us ([email protected]).")

    try:

        new_user = User(email, username, password, invitation_code=invite_code, cohort=cohort,
                        learned_language=learned_language, native_language=native_language)
        db_session.add(new_user)

        if cohort:
            if cohort.is_cohort_of_teachers:
                teacher = Teacher(new_user)
                db_session.add(teacher)

        db_session.commit()

        send_new_user_account_email(username, invite_code, cohort_name)

        return new_user

    except sqlalchemy.exc.IntegrityError:
        raise Exception("There is already an account for this email.")
    except Exception as e:
        raise Exception("Could not create the account")
Beispiel #26
0
#!/usr/bin/env python
"""

   Script that lists recent users

   To be called from a cron job.

"""

from zeeguu_core.model import User

for user_id in User.all_recent_user_ids():
    user = User.find_by_id(user_id)
    print(user.name)
    print(user.email)
Beispiel #27
0
 def test_validate_name(self):
     random_name = self.faker.name()
     assert User.validate_name('', random_name)
Beispiel #28
0
 def test_validate_password(self):
     random_password = self.faker.password()
     assert User.validate_password('', random_password)
Beispiel #29
0
 def test_validate_email(self):
     random_email = self.faker.email()
     assert User.validate_email('', random_email)
Beispiel #30
0
 def __get_bookmarks_for_user(self, user_id):
     user = User.find_by_id(user_id)
     print('Using user ' + user.name + ' with id ' + str(user.id))
     return user.all_bookmarks()
Beispiel #31
0
# - there was a bug in the bookmark quality and a
#   bookmark that was a subset of another would not
#   be correctly detected as such
#
# - there were too few examples with contexts of
#   20 words so we increased the context size to 42

from zeeguu_core.model import User
from zeeguu_core.word_scheduling.arts.bookmark_priority_updater import BookmarkPriorityUpdater
from zeeguu_core import db


def fix_bookmark_priorities(USER_ID):
    print(f"fixing for user {USER_ID}")
    user = User.find_by_id(USER_ID)

    all_users_bookmarks = user.all_bookmarks()
    for each in all_users_bookmarks:
        each.update_fit_for_study()
    db.session.commit()

    BookmarkPriorityUpdater.update_bookmark_priority(db, user)
    print(f"... OK for {len(all_users_bookmarks)} bookmarks")


for user in User.find_all()[700:]:
    try:
        fix_bookmark_priorities(user.id)
    except:
        print("... failed")
Beispiel #32
0
    def test_authorize(self):
        new_password = self.faker.password()
        self.user.update_password(new_password)
        result = User.authorize(self.user.email, new_password)

        assert result is not None and result == self.user
Beispiel #33
0
 def test_exists(self):
     assert User.exists(self.user)
Beispiel #34
0
            diff = math.fabs(a[i] - b[i])
            diffs.append(diff * factor[i])
        return diffs


if __name__ == "__main__":
    optimization_goals = OptimizationGoals(words_in_parallel=20,
                                           words_in_parallel_factor=3,
                                           repetition_correct_factor=0,
                                           repetition_incorrect_factor=0)

    # update exercise source stats
    BookmarkPriorityUpdater._update_exercise_source_stats()

    # optimize for algorithm for these users
    users = User.find_all()

    start = timer()

    user_ids = [user.id for user in users]
    results = []
    for user_id in user_ids:
        algorithm = ArtsRT()
        evaluator = AlgorithmEvaluator(user_id, algorithm, change_limit=1.0)
        variables_to_set = [['d', getattr(algorithm, 'd'), +5],
                            ['b', getattr(algorithm, 'b'), +10],
                            ['w', getattr(algorithm, 'w'), +10]]
        result = evaluator.fit_parameters(variables_to_set, optimization_goals)
        if result is not None:
            count_bookmarks = len(evaluator.fancy.bookmarks)
            count_exercises = sum(
Beispiel #35
0
def past_exercises_for(user_id):
    user = User.find_by_id(USER_ID)

    q = (db.session.query(Exercise).join(bookmark_exercise_mapping).join(
        Bookmark).join(User).filter(User.id == USER_ID).order_by(
            Exercise.time))

    for ex in q.all():
        bookmark = ex.get_bookmark()
        past = ""

        sorted_log = sorted(
            bookmark.exercise_log,
            key=lambda x: datetime.datetime.strftime(x.time, "%Y-%m-%d"),
            reverse=True)

        corrects_in_a_row = 0
        for each in sorted_log:
            if each.time < ex.time:
                if each.outcome.outcome == "Correct":
                    corrects_in_a_row += 1
                else:
                    corrects_in_a_row = 0

                past += f"{each.time.day}/{each.time.month} {each.outcome.outcome} < "

        if ex.outcome.outcome == "Correct":
            corrects_in_a_row += 1
        else:
            corrects_in_a_row = 0

        if corrects_in_a_row:
            print(
                f"{ex.time.day}/{ex.time.month} {bookmark.origin.word}({bookmark.id}) {ex.outcome.outcome}:{corrects_in_a_row} < ({past})"
            )
        else:
            print(
                f"{ex.time.day}/{ex.time.month} {bookmark.origin.word}({bookmark.id}) {ex.outcome.outcome} < ({past})"
            )

        if bookmark.learned and ex.time == bookmark.learned_time:
            print("Learned!")
            print(" ")

    print("All Bookmarks")
    for bookmark in user.all_bookmarks():
        btime = datetime.datetime.strftime(bookmark.time, "%Y-%m-%d")
        print(f"{btime} " +
              ("[fit_for_study] " if bookmark.fit_for_study else "") +
              ("[Learned] " if bookmark.learned else "") +
              f"Ctx: {bookmark.context_word_count()} " + f"{bookmark.id} " +
              f"{bookmark.origin.word} / {bookmark.translation.word}")

    print("")
    print("Bookmarks to Study")
    for bookmark in user.bookmarks_to_study():
        btime = datetime.datetime.strftime(bookmark.time, "%Y-%m-%d")
        print(f"{btime} " +
              ("[Quality] " if bookmark.quality_bookmark() else "") +
              ("[fit_for_study] " if bookmark.fit_for_study else "") +
              ("[Learned] " if bookmark.learned else "") +
              f"Ctx: {bookmark.context_word_count()} " + f"{bookmark.id} " +
              f"{bookmark.origin.word} / {bookmark.translation.word}")
Beispiel #36
0
#!/usr/bin/env python
"""

   Script that lists recent users

   To be called from a cron job.

"""
from sortedcontainers import SortedList
from zeeguu_core.model import User, Bookmark

from wordstats import Word

user = User.find_by_id(1890)
language = 'nl'

months_dict = dict()

for bookmark in Bookmark.query.filter_by(user=user):

    if not bookmark.origin.language.code == language:
        continue

    # if not bookmark.quality_bookmark():
    #     continue

    if len(bookmark.origin.word) < 4:
        continue

    date_key = bookmark.time.strftime("%y-%m")
Beispiel #37
0
import datetime

from zeeguu_core.model import User, Exercise, Bookmark
from zeeguu_core.model.bookmark import bookmark_exercise_mapping
from zeeguu_core import db

# USER_ID = 2162
# USER_ID = 2145  # Fe
# USER_ID = 2134 #Victor
USER_ID = 534

user = User.find_by_id(USER_ID)

q = (db.session.query(Exercise).join(bookmark_exercise_mapping).join(
    Bookmark).join(User).filter(User.id == USER_ID).order_by(Exercise.time))

for ex in q.all():
    bookmark = ex.get_bookmark()
    past = ""

    sorted_log = sorted(
        bookmark.exercise_log,
        key=lambda x: datetime.datetime.strftime(x.time, "%Y-%m-%d"),
        reverse=True)

    corrects_in_a_row = 0
    for each in sorted_log:
        if each.time < ex.time:
            if each.outcome.outcome == "Correct":
                corrects_in_a_row += 1
            else: