def apply_comment():
    """ Applies the supplied comment to the desired competition event for this user. """

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present
    solve_data = json.loads(request.data)
    if not all(key in solve_data for key in (COMP_EVENT_ID, COMMENT)):
        return (ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST)

    # Extract all the specific fields out of the solve data dictionary
    comp_event_id = solve_data[COMP_EVENT_ID]
    comment = solve_data[COMMENT]

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return (ERR_MSG_NO_SUCH_EVENT.format(comp_event_id),
                HTTPStatus.NOT_FOUND)

    # Verify that the competition event belongs to the active competition.
    comp = comp_event.Competition
    if not comp.active:
        return (ERR_MSG_INACTIVE_COMP, HTTPStatus.BAD_REQUEST)

    # Retrieve the user's results record for this event
    user_event_results = get_event_results_for_user(comp_event_id,
                                                    current_user)
    if (not user_event_results) or (not user_event_results.solves):
        return (ERR_MSG_NO_RESULTS.format(comp_event_id), HTTPStatus.NOT_FOUND)

    # Apply the new comment and save the results
    user_event_results.comment = comment
    save_event_results(user_event_results)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
Example #2
0
def comp_event_results(comp_event_id):
    """ A route for obtaining results for a specific competition event and rendering them
    for the leaderboards pages. """

    if 'overall' in comp_event_id:
        return get_overall_performance_data(
            int(comp_event_id.replace('overall_', '')))

    comp_event_id = int(comp_event_id)

    comp_event = get_comp_event_by_id(comp_event_id)

    # If the page is being viewed by an admin, render the controls for toggling blacklist status
    # and also apply additional styling on blacklisted results to make them easier to see
    show_admin = current_user.is_admin

    # Store the scrambles so we can show those too
    scrambles = [s.scramble for s in comp_event.scrambles]

    results = get_all_complete_user_results_for_comp_event(
        comp_event_id, omit_blacklisted=False)
    results = list(
        results
    )  # take out of the SQLAlchemy BaseQuery and put into a simple list

    if not results:
        return "Nobody has participated in this event yet. Maybe you'll be the first!"

    results = filter_blacklisted_results(results, show_admin, current_user)

    # Split the times string into components, add to a list called `"solves_helper` which
    # is used in the UI to show individual solves, and make sure the length == 5, filled
    # with empty strings if necessary
    # TODO put this in business logic somewhere
    for result in results:
        if comp_event.Event.name == 'FMC':
            solves_helper = list()
            for i, solve in enumerate(result.solves):
                scramble = solve.Scramble.scramble
                solution = solve.fmc_explanation
                moves = solve.get_friendly_time()
                solves_helper.append((scramble, solution, moves))
            while len(solves_helper) < 5:
                solves_helper.append((None, None, None))
        else:
            solves_helper = result.times_string.split(', ')
            while len(solves_helper) < 5:
                solves_helper.append('')
        setattr(result, 'solves_helper', solves_helper)

    # Sort the results
    results_with_ranks = sort_user_results_with_rankings(
        results, comp_event.Event.eventFormat)

    return render_template("results/comp_event_table.html",
                           results=results_with_ranks,
                           comp_event=comp_event,
                           show_admin=show_admin,
                           scrambles=scrambles)
def toggle_prev_penalty():
    """ Toggles the either the DNF or +2 status of the last solve for the specified user and
    competition event. """

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present
    solve_data = json.loads(request.data)
    if not all(key in solve_data for key in (COMP_EVENT_ID, )):
        return (ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST)

    # Extract all the specific fields out of the solve data dictionary
    comp_event_id = solve_data[COMP_EVENT_ID]
    penalty_to_toggle = solve_data[PENALTY_TO_TOGGLE]

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return (ERR_MSG_NO_SUCH_EVENT.format(comp_event_id),
                HTTPStatus.NOT_FOUND)

    # If this is FMC, this isn't valid
    if comp_event.Event.name == 'FMC':
        return (ERR_MSG_NOT_VALID_FOR_FMC, HTTPStatus.BAD_REQUEST)

    # Verify that the competition event belongs to the active competition.
    comp = comp_event.Competition
    if not comp.active:
        return (ERR_MSG_INACTIVE_COMP, HTTPStatus.BAD_REQUEST)

    # Retrieve the user's results record for this event
    user_event_results = get_event_results_for_user(comp_event_id,
                                                    current_user)
    if (not user_event_results) or (not user_event_results.solves):
        return (ERR_MSG_NO_RESULTS.format(comp_event_id), HTTPStatus.NOT_FOUND)

    # Grab the last completed solve
    previous_solve = user_event_results.solves[-1]

    if penalty_to_toggle == PENALTY_DNF:
        # Toggle DNF
        # If the solve now has DNF, ensure it doesn't also have +2
        previous_solve.is_dnf = not previous_solve.is_dnf
        if previous_solve.is_dnf:
            previous_solve.is_plus_two = False

    else:
        # Toggle +2
        # If the solve now has +2, ensure it doesn't also have DNF
        previous_solve.is_plus_two = not previous_solve.is_plus_two
        if previous_solve.is_plus_two:
            previous_solve.is_dnf = False

    # Process through the user's event results, ensuring PB flags, best single, average, overall
    # event result, etc are all up-to-date.
    process_event_results(user_event_results, comp_event, current_user)
    save_event_results(user_event_results)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
Example #4
0
def reprocess_results_for_user_and_comp_event(username, comp_event_id):
    """ Reprocesses the event results for the specified user and competition event. """

    user = get_user_by_username(username)
    if not user:
        print("Oops, that user doesn't exist.")

    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        print("Oops, that comp event doesn't exist.")

    results = get_event_results_for_user(comp_event_id, user)
    results = process_event_results(results, comp_event, user)
    save_event_results(results)
def __retrieve_target_solve(solve_data, user):
    """ Utility method to retrieve the specified solve (by solve id and competition event id).
    Validates the solve belongs to an active competition, and belongs to the specified user.
    Returns the following shapes:
        ((target_solve, user_event_results, comp_event), None, None)
            if the solve exists, in the active comp, for the specified user.
        (None, err_msg, http_status_code)
            if something goes wrong during retrieval.
    """

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present

    if not all(key in solve_data for key in (SOLVE_ID, COMP_EVENT_ID)):
        return None, ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST

    # Extract all the specific fields out of the solve data dictionary
    solve_id = solve_data[SOLVE_ID]
    comp_event_id = solve_data[COMP_EVENT_ID]

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return None, ERR_MSG_NO_SUCH_EVENT.format(
            comp_event_id), HTTPStatus.NOT_FOUND

    # Verify that the competition event belongs to the active competition.
    comp = comp_event.Competition
    if not comp.active:
        return ERR_MSG_INACTIVE_COMP, HTTPStatus.BAD_REQUEST

    # Retrieve the user's results record for this event
    user_event_results = get_event_results_for_user(comp_event_id, user)
    if (not user_event_results) or (not user_event_results.solves):
        return None, ERR_MSG_NO_RESULTS.format(
            comp_event_id), HTTPStatus.NOT_FOUND

    # Find the target solve in the user's results
    target_solve = None
    for solve in user_event_results.solves:
        if solve.id == solve_id:
            target_solve = solve
            break

    # If we didn't find the target solve, let the user know
    if not target_solve:
        return None, ERR_MSG_NO_SOLVE.format(
            solve_id, user.username), HTTPStatus.NOT_FOUND

    return (target_solve, user_event_results, comp_event), None, None
def delete_prev_solve():
    """ Deletes the last completed solve of the specified competition event for this user. """

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present
    solve_data = json.loads(request.data)
    if not all(key in solve_data for key in (COMP_EVENT_ID, )):
        return (ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST)

    # Extract all the specific fields out of the solve data dictionary
    comp_event_id = solve_data[COMP_EVENT_ID]

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return (ERR_MSG_NO_SUCH_EVENT.format(comp_event_id),
                HTTPStatus.NOT_FOUND)

    # Verify that the competition event belongs to the active competition.
    comp = comp_event.Competition
    if not comp.active:
        return (ERR_MSG_INACTIVE_COMP, HTTPStatus.BAD_REQUEST)

    # Retrieve the user's results record for this event
    user_event_results = get_event_results_for_user(comp_event_id,
                                                    current_user)
    if (not user_event_results) or (not user_event_results.solves):
        return (ERR_MSG_NO_RESULTS.format(comp_event_id), HTTPStatus.NOT_FOUND)

    # If the results only have one solve (which we're about to delete), we need to delete the
    # results entirely
    do_delete_user_results_after_solve = len(user_event_results.solves) == 1

    # Grab the last completed solve and delete it
    previous_solve = user_event_results.solves[-1]
    delete_user_solve(previous_solve)

    # If no more solves left, just delete the whole results record
    if do_delete_user_results_after_solve:
        delete_event_results(user_event_results)

    # Otherwise process through the user's event results, ensuring PB flags, best single, average,
    # overall event result, etc are all up-to-date.
    else:
        process_event_results(user_event_results, comp_event, current_user)
        save_event_results(user_event_results)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
Example #7
0
def comp_event_results(comp_event_id):
    """ A route for obtaining results for a specific competition event and rendering them
    for the leaderboards pages. """

    if 'overall' in comp_event_id:
        return get_overall_performance_data(
            int(comp_event_id.replace('overall_', '')))

    comp_event_id = int(comp_event_id)

    comp_event = get_comp_event_by_id(comp_event_id)

    # If the page is being viewed by an admin, render the controls for toggling blacklist status
    # and also apply additional styling on blacklisted results to make them easier to see
    show_admin = current_user.is_admin

    log_msg = LOG_USER_VIEWING_RESULTS.format(current_user.username,
                                              comp_event.Event.name,
                                              comp_event.Competition.title)
    app.logger.info(log_msg, extra={'is_admin': show_admin})

    # Store the scrambles so we can show those too
    scrambles = [s.scramble for s in comp_event.scrambles]

    results = get_all_complete_user_results_for_comp_event(
        comp_event_id, omit_blacklisted=False)
    results = list(
        results
    )  # take out of the SQLAlchemy BaseQuery and put into a simple list

    # return jsonify(results)

    # if not results:
    #     return "Nobody has participated in this event yet. Maybe you'll be the first!"

    results = filter_blacklisted_results(results, show_admin, current_user)

    # Split the times string into components, add to a list called `"solves_helper` which
    # is used in the UI to show individual solves, and make sure the length == 5, filled
    # with empty strings if necessary
    # TODO put this in business logic somewhere
    for result in results:
        if comp_event.Event.name == 'FMC':
            solves_helper = list()
            for i, solve in enumerate(result.solves):
                scramble = solve.Scramble.scramble
                solution = solve.fmc_explanation
                moves = solve.get_friendly_time()
                solves_helper.append((scramble, solution, moves))
            while len(solves_helper) < 5:
                solves_helper.append((None, None, None))
        else:
            solves_helper = result.times_string.split(', ')
            while len(solves_helper) < 5:
                solves_helper.append('')
        setattr(result, 'solves_helper', solves_helper)

    # Sort the results
    results_with_ranks = sort_user_results_with_rankings(
        results, comp_event.Event.eventFormat)

    good_results = list(
        map(
            lambda result: {
                "rank": result[0],
                "visibleRank": result[1],
                "solve": {
                    "id": result[2].__dict__['id'],
                    "times": result[2].__dict__['times_string'].split(', '),
                    "best_single": result[2].__dict__['single'],
                    "average": result[2].__dict__['average'],
                    "comment": result[2].__dict__['comment'],
                    "blacklisted": result[2].__dict__['is_blacklisted'],
                    "user": get_user(result[2].__dict__['User'])
                }
            }, results_with_ranks))

    return jsonify({'results': good_results, 'scrambles': scrambles})
def post_solve():
    """ Saves a solve. Ensures the user has UserEventResults for this event, associated this solve
    with those results, and processes the results to make sure all relevant data is up-to-date. """

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present
    solve_data = json.loads(request.data)
    if not all(key in solve_data for key in EXPECTED_FIELDS):
        return (ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST)

    # Extract all the specific fields out of the solve data dictionary
    is_dnf = solve_data[IS_DNF]
    is_plus_two = solve_data[IS_PLUS_TWO]
    scramble_id = solve_data[SCRAMBLE_ID]
    comp_event_id = solve_data[COMP_EVENT_ID]
    centiseconds = solve_data[CENTISECONDS]
    is_inspection_dnf = solve_data.get(IS_INSPECTION_DNF, False)
    fmc_comment = solve_data.get(FMC_COMMENT, '')

    # If the solve time isn't positive, don't save the solve. Let the user know that a negative
    # time isn't allowed.
    if centiseconds <= 0:
        return (ERR_MSG_NON_POSITIVE_TIME, HTTPStatus.BAD_REQUEST)

    # If the submitted solve is for a scramble the user already has a solve for,
    # don't take any further action to persist a solve, just return. User probably
    # is user manual time entry and pressed enter twice accidentally in quick succession
    if get_user_solve_for_scramble_id(current_user.id, scramble_id):
        return timer_page(comp_event_id, gather_info_for_live_refresh=True)

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return (ERR_MSG_NO_SUCH_EVENT.format(comp_event_id),
                HTTPStatus.NOT_FOUND)

    # Verify that the competition event belongs to the active competition.
    comp = comp_event.Competition
    if not comp.active:
        return (ERR_MSG_INACTIVE_COMP, HTTPStatus.BAD_REQUEST)

    # Double-check that if the solve is MBLD, the number of attempted cubes is > 1
    if comp_event.Event.name == "MBLD":
        _, num_attempted = get_mbld_successful_and_attempted(centiseconds)
        if num_attempted < 2:
            return (ERR_MSG_MBLD_TOO_FEW_ATTEMPTED, HTTPStatus.BAD_REQUEST)

    # Retrieve the user's results record for this event if they exist, or else create a new record
    user_event_results = get_event_results_for_user(comp_event_id,
                                                    current_user)
    if not user_event_results:
        user_event_results = UserEventResults(comp_event_id=comp_event_id,
                                              user_id=current_user.id,
                                              comment='')

    # Create the record for this solve and associate it with the user's event results
    solve = UserSolve(time=centiseconds,
                      is_dnf=is_dnf,
                      is_plus_two=is_plus_two,
                      scramble_id=scramble_id,
                      is_inspection_dnf=is_inspection_dnf,
                      fmc_explanation=fmc_comment)
    user_event_results.solves.append(solve)

    # Process through the user's event results, ensuring PB flags, best single, average, overall
    # event result, etc are all up-to-date.
    process_event_results(user_event_results, comp_event, current_user)
    save_event_results(user_event_results)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
Example #9
0
def timer_page(comp_event_id, gather_info_for_live_refresh=False):
    """ It's the freakin' timer page. """

    # Retrieve the specified competition event
    comp_event = get_comp_event_by_id(comp_event_id)
    if not comp_event:
        return (ERR_MSG_NO_SUCH_EVENT.format(comp_event_id=comp_event_id), 404)

    # Verify it's for the actve competition
    comp = comp_event.Competition
    if not comp.active:
        return (ERR_MSG_INACTIVE_COMP, 400)

    event_name = comp_event.Event.name
    event_format = comp_event.Event.eventFormat
    event_description = comp_event.Event.description

    # Get the user's settings, and specifically pull the setting to determine
    # whether or not to hide the scramble preview
    settings = __get_user_settings(current_user)
    hide_scramble_preview = settings[SettingCode.HIDE_SCRAMBLE_PREVIEW]
    show_shapes_background = settings[SettingCode.ENABLE_MOVING_SHAPES_BG]

    # Grab the user's event results (if any)
    user_results = get_event_results_for_user(comp_event_id, current_user)

    # Get a list of user-readable user solve times
    user_solves, last_solve = __build_user_solves_list(
        user_results, comp_event.Event.totalSolves, comp_event.scrambles)

    # Split last_solve into min/seconds and centiseconds
    # TODO: comment this more thoroughly, and put into its own function
    last_seconds = DEFAULT_SECONDS
    last_centis = DEFAULT_CENTIS
    hide_timer_dot = False
    if last_solve:
        if event_name == EVENT_FMC.name:
            hide_timer_dot = True
            last_seconds = last_solve
            last_centis = ''
        elif event_name == EVENT_MBLD.name:
            pass
        else:
            if last_solve == DNF:
                hide_timer_dot = True
                last_seconds = DNF
                last_centis = ''
            else:
                last_seconds = last_solve.split('.')[0]
                last_centis = last_solve.split('.')[1]

    # Determine the scramble ID, scramble text, and index for the next unsolved scramble.
    # If all solves are done, substitute in some sort of message in place of the scramble text
    scramble_info = __determine_scramble_id_text_index(user_results,
                                                       user_solves,
                                                       comp_event.scrambles,
                                                       event_name,
                                                       event_format)
    scramble_id, scramble_text, scramble_index = scramble_info

    # Build up the page title, consisting of the event name and competition title
    alternative_title = PAGE_TITLE_TEMPLATE.format(event_name=event_name,
                                                   comp_title=comp.title)

    # Determine if we should display a scramble preview for this event
    show_scramble_preview = event_name not in NO_SCRAMBLE_PREVIEW_EVENTS

    # Determine button states
    button_state_info = __determine_button_states(user_results, scramble_index)

    # Grab the comment (if any) from the user results (if any), otherwise default to an empty string
    comment = user_results.comment if user_results else ''

    # Determine if the event has been completed by this user
    is_complete_flag = user_results.is_complete if user_results else False
    num_solves_done = len(user_results.solves) if user_results else 0
    is_complete = __determine_is_complete(is_complete_flag, event_format,
                                          num_solves_done)

    # Determine the timer page subtype (timer, manual time entry, FMC manual entry, or MBLD)
    page_subtype = __determine_page_subtype(event_name, settings)

    if gather_info_for_live_refresh:
        # Only a caller coming from one of the persistence routes should go through this path.
        # Build up a dictionary of relevant information so the timer page can be re-rendered
        # with up-to-date information about the state of the timer page
        timer_page_render_info = {
            'button_state_info': button_state_info,
            'scramble_text': scramble_text,
            'scramble_id': scramble_id,
            'user_solves': user_solves,
            'last_seconds': last_seconds,
            'last_centis': last_centis,
            'hide_timer_dot': hide_timer_dot,
            'is_complete': is_complete,
            'comment': comment,
            'last_solve': last_solve
        }
        return json.dumps(timer_page_render_info)

    return render_template(TIMER_TEMPLATE_MOBILE_MAP[request.MOBILE],
                           scramble_text=scramble_text,
                           scramble_id=scramble_id,
                           comp_event_id=comp_event_id,
                           event_name=event_name,
                           alternative_title=alternative_title,
                           user_solves=user_solves,
                           button_states=button_state_info,
                           show_scramble_preview=show_scramble_preview,
                           last_solve=last_solve,
                           last_seconds=last_seconds,
                           last_centis=last_centis,
                           hide_timer_dot=hide_timer_dot,
                           comment=comment,
                           is_complete=is_complete,
                           settings=settings,
                           page_subtype=page_subtype,
                           hide_scramble_preview=hide_scramble_preview,
                           show_shapes_background=show_shapes_background,
                           event_description=event_description)