def delete_solve():
    """ Deletes the specified solve. """

    target_solve_data, err_msg, http_status_code = __retrieve_target_solve(
        request.data, current_user)
    if not target_solve_data:
        return err_msg, http_status_code

    # Extract the target solve, user's event results, and the associated comp event
    target_solve, user_event_results, comp_event = target_solve_data

    # 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

    # Delete the target solve
    delete_user_solve(target_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, comp_event.Event.id)

    return timer_page(comp_event.id, gather_info_for_live_refresh=True)
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, comp_event.Event.id)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
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, comp_event.Event.id)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
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, comp_event.Event.id)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)
def set_time():
    """ Applies the specified time to the specified solve. """

    target_solve_data, err_msg, http_status_code = __retrieve_target_solve(
        request.data, current_user)
    if not target_solve_data:
        return err_msg, http_status_code

    # Extract the target solve, user's event results, and the associated competition event
    target_solve, user_event_results, comp_event = target_solve_data

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

    # Extract JSON solve data, deserialize to dict, and verify that all expected fields are present
    # This is slightly redundant given the call to __retrieve_target_solve, but we also need to pull
    # the centiseconds value which that doesn't do.
    solve_data = json.loads(request.data)
    if not all(key in solve_data
               for key in (SOLVE_ID, COMP_EVENT_ID, CENTISECONDS)):
        return (ERR_MSG_MISSING_INFO, HTTPStatus.BAD_REQUEST)

    # Extract all the specific fields out of the solve data dictionary
    target_solve.time = solve_data[CENTISECONDS]

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

    # No penalties on the solve after adjusting time
    target_solve.is_plus_two = False
    target_solve.is_dnf = False

    process_event_results(user_event_results, comp_event, current_user)
    save_event_results(user_event_results, comp_event.Event.id)

    return timer_page(comp_event.id, gather_info_for_live_refresh=True)
def clear_penalty():
    """ Clears penalties from the specified solve. """

    target_solve_data, err_msg, http_status_code = __retrieve_target_solve(
        request.data, current_user)
    if not target_solve_data:
        return err_msg, http_status_code

    # Extract the target solve, user's event results, and the associated competition event
    target_solve, user_event_results, comp_event = target_solve_data

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

    # Clear penalties
    target_solve.is_plus_two = False
    target_solve.is_dnf = False

    process_event_results(user_event_results, comp_event, current_user)
    save_event_results(user_event_results, comp_event.Event.id)

    return timer_page(comp_event.id, gather_info_for_live_refresh=True)
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":
        if MbldSolve(centiseconds).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, comp_event.Event.id)

    return timer_page(comp_event_id, gather_info_for_live_refresh=True)