Source code for results.views.results_views

from django.contrib.auth.decorators import login_required
from django.contrib.humanize.templatetags.humanize import ordinal
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from organisations.models import Organisation
from results.models import ResultsFile
from results.views.core import (
    double_dummy_from_usebio,
    dealer_and_vulnerability_for_board,
)
from results.views.par_contract import par_score_and_contract
from results.views.usebio import parse_usebio_file
from utils.utils import cobalt_paginator


def _get_player_names_by_id(usebio):
    """Helper to get the player names, numbers and directions"""

    player_dict = {"system_numbers": {}, "names": {}}
    for item in usebio["PARTICIPANTS"]["PAIR"]:

        # Names
        player_1 = item["PLAYER"][0]["PLAYER_NAME"].title()
        player_2 = item["PLAYER"][1]["PLAYER_NAME"].title()
        this_pair_id = item["PAIR_NUMBER"]
        players_names = _format_pair_name(player_1, player_2)
        player_dict["names"][this_pair_id] = players_names

        # system numbers
        try:
            player_1_system_number = int(item["PLAYER"][0]["NATIONAL_ID_NUMBER"])
            player_2_system_number = int(item["PLAYER"][1]["NATIONAL_ID_NUMBER"])
        except TypeError:
            player_1_system_number = None
            player_2_system_number = None

        player_dict["system_numbers"][this_pair_id] = [
            player_1_system_number,
            player_2_system_number,
        ]

    return player_dict


def _get_pair_direction_for_board(usebio, board_number, pair):
    """Get which direction a pair was sitting for a particular board"""

    for board_item in usebio["EVENT"]["BOARD"]:
        if int(board_item["BOARD_NUMBER"]) == board_number:
            for score in board_item["TRAVELLER_LINE"]:
                if score["NS_PAIR_NUMBER"] == pair:
                    return True
                if score["EW_PAIR_NUMBER"] == pair:
                    return False

    # Default to NS if we don't know
    return True


def _format_pair_name(player_1, player_2):
    """helper function to nicely format a pair name"""

    players_names = f"{player_1} & {player_2}"

    # for couple show name as Mary & David Smith
    surname1 = player_1.split(" ")[-1]
    surname2 = player_2.split(" ")[-1]
    if surname1 == surname2:
        first_name1 = player_1.split(" ")[0]
        players_names = f"{first_name1} & {player_2}"

    return players_names


def _set_indicator_based_on_percentage(percentage):
    """set a value for indicator to be used as a class name in the template based upon the percentage"""

    # Set size of success circle
    indicator = ""
    if percentage > 20:
        indicator = "results-circle-quarter"
    if percentage >= 40:
        indicator = "results-circle-half"
    if percentage >= 60:
        indicator = "results-circle-three-quarter"
    if percentage >= 80:
        indicator = "results-circle-full"
    if percentage == 100:
        indicator = "results-circle-full-100"

    return indicator


def _set_icon_based_on_percentage(percentage):
    """set a value for material icons to be used in the template based upon the percentage"""

    indicator = "<i class='material-icons' style='color: black'>cancel</i>"
    if percentage >= 1:
        indicator = "<i class='material-icons' style='color: red'>cancel</i>"
    if percentage > 20:
        indicator = "<i class='material-icons' style='color: red'>expand_more</i>"
    if percentage >= 40:
        indicator = "<span class='' style='color: blue; font-size: larger;'>=</span>"
    if percentage >= 60:
        indicator = "<i class='material-icons' style='color: green'>star_half</i>"
    if percentage >= 80:
        indicator = "<i class='material-icons' style='color: blue'>check_circle</i>"
    if percentage == 100:
        indicator = "<i class='material-icons' style='color: orange'>star</i>"

    return indicator


def _percentage_from_match_points(ns_match_points, ew_match_points, ns_flag):
    """calculate the percentage using the matchpoints. Include the direction as well"""

    # Calculate percentage
    total_mps = ns_match_points + ew_match_points

    # Guard against passed-out boards where all tables score identically (total = 0)
    if total_mps == 0:
        return 50.0

    if ns_flag:
        percentage = ns_match_points / total_mps
    else:
        percentage = ew_match_points / total_mps

    return percentage * 100.0


[docs] @login_required() def usebio_mp_pairs_results_summary_view(request, results_file_id): """Show the summary results for a usebio format event. Dispatches on event type.""" results_file = get_object_or_404(ResultsFile, pk=results_file_id) usebio = parse_usebio_file(results_file)["EVENT"] masterpoint_type = usebio.get("MASTER_POINT_TYPE", "No").title() if results_file.event_type in [ ResultsFile.EventType.CROSS_IMP, ResultsFile.EventType.BUTLER_PAIRS, ]: return imp_results_summary_view(request, usebio, results_file, masterpoint_type) # MP_PAIRS (default) # TODO: Error checking, handle ties, one field or two # TODO: Masterpoints show type in title and change colours # TODO: Highlight team mates if usebio["WINNER_TYPE"] == "2": return usebio_mp_pairs_results_summary_view_two_field( request, usebio, results_file, masterpoint_type ) elif usebio["WINNER_TYPE"] == "1": return usebio_mp_pairs_results_summary_view_single_field( request, usebio, results_file, masterpoint_type ) else: return HttpResponse( f"usebio winner type of {usebio['WINNER_TYPE']} not currently supported." )
[docs] def usebio_mp_pairs_results_summary_view_two_field( request, usebio, results_file, masterpoint_type ): """Handle two field NS/EW""" ns_scores = [] ew_scores = [] for item in usebio["PARTICIPANTS"]["PAIR"]: player_1 = item["PLAYER"][0]["PLAYER_NAME"].title() player_2 = item["PLAYER"][1]["PLAYER_NAME"].title() try: player_1_system_number = int(item["PLAYER"][0]["NATIONAL_ID_NUMBER"]) player_2_system_number = int(item["PLAYER"][1]["NATIONAL_ID_NUMBER"]) except TypeError: player_1_system_number = None player_2_system_number = None # This may break for ties position = int(item["PLACE"]) masterpoints = int(item.get("MASTER_POINTS_AWARDED", 0)) / 100.0 pair_number = item["PAIR_NUMBER"] direction = item["DIRECTION"] percentage = item["PERCENTAGE"] players_names = _format_pair_name(player_1, player_2) # See if this user is in the data and highlight if request.user.system_number in [ player_1_system_number, player_2_system_number, ]: tr_highlight = "bg-warning" else: tr_highlight = "" row = { "player_1": player_1, "player_2": player_2, "players_names": players_names, "player_1_system_number": player_1_system_number, "player_2_system_number": player_2_system_number, "position": position, "masterpoints": masterpoints, "pair_number": pair_number, "percentage": percentage, "tr_highlight": tr_highlight, } if direction == "NS": ns_scores.append(row) else: ew_scores.append(row) # sort ns_scores = sorted(ns_scores, key=lambda d: d["position"]) ew_scores = sorted(ew_scores, key=lambda d: d["position"]) return render( request, "results/usebio/usebio_results_summary_two_field_view.html", { "results_file": results_file, "usebio": usebio, "ns_scores": ns_scores, "ew_scores": ew_scores, "masterpoint_type": masterpoint_type, }, )
[docs] def usebio_mp_pairs_results_summary_view_single_field( request, usebio, results_file, masterpoint_type ): """Handle single field e.g. Howell""" scores = [] for item in usebio["PARTICIPANTS"]["PAIR"]: player_1 = item["PLAYER"][0]["PLAYER_NAME"].title() player_2 = item["PLAYER"][1]["PLAYER_NAME"].title() try: player_1_system_number = int(item["PLAYER"][0]["NATIONAL_ID_NUMBER"]) player_2_system_number = int(item["PLAYER"][1]["NATIONAL_ID_NUMBER"]) except TypeError: player_1_system_number = None player_2_system_number = None # This may break for ties position = int(item["PLACE"]) try: masterpoints = int(item["MASTER_POINTS_AWARDED"]) / 100.0 except KeyError: masterpoints = 0 pair_number = item["PAIR_NUMBER"] percentage = item["PERCENTAGE"] players_names = _format_pair_name(player_1, player_2) # See if this user is in the data and highlight if request.user.system_number in [ player_1_system_number, player_2_system_number, ]: tr_highlight = "bg-warning" else: tr_highlight = "" row = { "player_1": player_1, "player_2": player_2, "players_names": players_names, "player_1_system_number": player_1_system_number, "player_2_system_number": player_2_system_number, "position": position, "masterpoints": masterpoints, "pair_number": pair_number, "percentage": percentage, "tr_highlight": tr_highlight, } scores.append(row) # sort scores = sorted(scores, key=lambda d: d["position"]) return render( request, "results/usebio/usebio_results_summary_single_field_view.html", { "results_file": results_file, "usebio": usebio, "scores": scores, "masterpoint_type": masterpoint_type, }, )
[docs] @login_required() def usebio_mp_pairs_details_view(request, results_file_id, pair_id): """Show the board by board results for a pair""" results_file = get_object_or_404(ResultsFile, pk=results_file_id) usebio = parse_usebio_file(results_file)["EVENT"] # Get position and percentage from usebio position = "" pair_percentage = "" for item in usebio["PARTICIPANTS"]["PAIR"]: pair = item["PAIR_NUMBER"] if pair == pair_id: position = int(item["PLACE"]) pair_percentage = item["PERCENTAGE"] break # First get all the players names and details player_dict = _get_player_names_by_id(usebio) pair_data = [] last_opponent = 0 bg_colour = False if "BOARD" not in usebio: return render( request, "results/usebio/usebio_no_boards_warning.html", { "usebio": usebio, "results_file": results_file, "pair_id": pair_id, "pair_name": player_dict["names"][pair_id], }, ) for board in usebio["BOARD"]: board_number = int(board.get("BOARD_NUMBER")) for traveller_line in board.get("TRAVELLER_LINE"): ns_pair = traveller_line.get("NS_PAIR_NUMBER") ew_pair = traveller_line.get("EW_PAIR_NUMBER") if pair_id in [ns_pair, ew_pair]: # Our pair played this board and this is the score if pair_id == ns_pair: opponents = player_dict["names"].get(ew_pair) opponents_pair_id = ew_pair ns_flag = True else: opponents = player_dict["names"].get(ns_pair) opponents_pair_id = ns_pair ns_flag = False contract = traveller_line.get("CONTRACT") played_by = traveller_line.get("PLAYED_BY") lead = traveller_line.get("LEAD") or "" tricks = traveller_line.get("TRICKS") or "" ns_match_points = float(traveller_line.get("NS_MATCH_POINTS")) ew_match_points = float(traveller_line.get("EW_MATCH_POINTS")) score = traveller_line.get("SCORE") if score and score[0] == "A": # Adjusted score, don't show user the code, just that it was adjusted score = "ADJ" percentage = _percentage_from_match_points( ns_match_points, ew_match_points, ns_flag ) indicator = _set_icon_based_on_percentage(percentage) # change background colour so boards played against same opponents are grouped if opponents_pair_id != last_opponent: # Has changed bg_colour = not bg_colour last_opponent = opponents_pair_id row = { "board_number": board_number, "contract": contract, "played_by": played_by, "lead": lead, "tricks": tricks, "indicator": indicator, "score": score, "opponents": opponents, "opponents_pair_id": opponents_pair_id, "percentage": percentage, "bg_colour": bg_colour, } pair_data.append(row) # sort pair_data = sorted(pair_data, key=lambda d: d["board_number"]) return render( request, "results/usebio/usebio_results_pairs_detail.html", { "usebio": usebio, "results_file": results_file, "pair_data": pair_data, "pair_id": pair_id, "pair_name": player_dict["names"][pair_id], "position": position, "pair_percentage": pair_percentage, }, )
[docs] @login_required() def usebio_mp_pairs_board_view(request, results_file_id, board_number, pair_id): """Show the traveller for a board. If pair_id is provided then we show it from the perspective of that pair. Pair id will be 0 if not provided""" results_file = get_object_or_404(ResultsFile, pk=results_file_id) usebio = parse_usebio_file(results_file) # First get all the players names and numbers player_dict = _get_player_names_by_id(usebio["EVENT"]) # get direction of pair on this board ns_flag = _get_pair_direction_for_board(usebio, board_number, pair_id) # extract data about this board board_data = get_traveller_info( usebio, board_number, player_dict, ns_flag, pair_id, request ) # Now get hand record hand = {} double_dummy = None if "HANDSET" not in usebio or "BOARD" not in usebio["HANDSET"]: return render( request, "utils/coblt_generic_error_page.html", { "title": "No Board Data Available", "message": "Sorry, we don't have any details available for this board.", }, ) for board in usebio["HANDSET"]["BOARD"]: if int(board["BOARD_NUMBER"]) == board_number: for compass in board["HAND"]: hand[compass["DIRECTION"]] = { "clubs": compass["CLUBS"], "diamonds": compass["DIAMONDS"], "hearts": compass["HEARTS"], "spades": compass["SPADES"], } double_dummy = double_dummy_from_usebio(board["HAND"]) break if not double_dummy: return HttpResponse(f"Board {board_number} not found for this result") # Sort data if ns_flag: board_data = sorted(board_data, key=lambda d: -d["ns_match_points"]) else: board_data = sorted(board_data, key=lambda d: -d["ew_match_points"]) # Get extra data and par score dealer, vulnerability = dealer_and_vulnerability_for_board(board_number) par_score, par_string = par_score_and_contract(double_dummy, vulnerability, dealer) # insert par_data into board_data board_data = _insert_par_data_into_list(board_data, par_score, par_string, ns_flag) # Add High card points and losing trick count high_card_points, losing_trick_count = calculate_hcp_and_ltc(hand) previous_board = board_number - 1 if board_number > 1 else None total_boards = len(usebio["HANDSET"]["BOARD"]) next_board = board_number + 1 if board_number < total_boards else None # Extract the pair's contract for this board so the DDS player can use it pair_contract_level = pair_contract_trump = pair_contract_declarer = None pair_contract_double = "" if pair_id and pair_id != "0": for row in board_data: if pair_id in (row.get("ns_pair_number"), row.get("ew_pair_number")): level, trump, double = _parse_usebio_contract(row.get("contract", "")) played_by = row.get("played_by", "") if level and trump and played_by in ("N", "E", "S", "W"): pair_contract_level = level pair_contract_trump = trump pair_contract_declarer = played_by pair_contract_double = double break return render( request, "results/usebio/usebio_results_board_detail.html", { "usebio": usebio.get("EVENT"), "results_file": results_file, "board_data": board_data, "board_number": board_number, "pair_id": pair_id, "hand": hand, "double_dummy": double_dummy, "dealer": dealer, "vulnerability": vulnerability, "par_score": par_score, "par_string": par_string, "high_card_points": high_card_points, "losing_trick_count": losing_trick_count, "next_board": next_board, "previous_board": previous_board, "total_boards": total_boards, "ns_flag": ns_flag, "total_boards_range": range(1, total_boards + 1), "pair_contract_level": pair_contract_level, "pair_contract_trump": pair_contract_trump, "pair_contract_declarer": pair_contract_declarer, "pair_contract_double": pair_contract_double, }, )
def _parse_usebio_contract(contract_str): """Parse a USEBIO contract string into (level, trump, double). Examples: "4S" -> (4, "S", ""), "3NT" -> (3, "N", ""), "5HX" -> (5, "H", "X"), "5HXX" -> (5, "H", "XX"). Returns (None, None, None) for passed-out boards or unparseable strings. """ if not contract_str: return None, None, None s = contract_str.upper().strip() if s in ("PASS", "PASSOUT", "P"): return None, None, None try: level = int(s[0]) rest = s[1:] if rest.startswith("NT"): trump = "N" suffix = rest[2:] elif rest and rest[0] in ("S", "H", "D", "C"): trump = rest[0] suffix = rest[1:] else: return None, None, None double = ( "XX" if suffix.startswith("XX") else "X" if suffix.startswith("X") else "" ) return level, trump, double except (IndexError, ValueError): return None, None, None def _insert_par_data_into_list(board_data, par_score, par_string, ns_flag): """sub of usebio_mp_pairs_board_view to add in the par data""" # For NS view, scores are descending, for EW ascending. Insert par score at first spot we find can_insert = True # Allow insert above first row job_done = False # Flag to see if we inserted or not, if not add at end for index, item in enumerate(board_data): # skip adjusted scores if type(item["score"]) is not int: continue if can_insert and ( (par_score >= item["score"] and ns_flag) or (par_score <= item["score"] and not ns_flag) ): row = { "score": par_score, "par_score": par_score, "par_string": par_string, } board_data.insert(index, row) job_done = True break # where we can insert yet or not depends on scores and direction can_insert = bool( (par_score <= item["score"] and ns_flag) or (par_score >= item["score"] and not ns_flag) ) # If we didn't find somewhere to put it, put at end if not job_done: row = { "score": par_score, "par_score": par_score, "par_string": par_string, } board_data.append(row) return board_data def _get_traveller_info_process_board( board, player_dict, pair_id, board_number, ns_flag, request ): """sub of get_traveller_info to process the record""" board_data = [] for traveller_line in board.get("TRAVELLER_LINE"): ns_pair_number = traveller_line.get("NS_PAIR_NUMBER") ns_pair = player_dict["names"].get(ns_pair_number) ew_pair_number = traveller_line.get("EW_PAIR_NUMBER") ew_pair = player_dict["names"].get(ew_pair_number) contract = traveller_line.get("CONTRACT") played_by = traveller_line.get("PLAYED_BY") lead = traveller_line.get("LEAD") or "" tricks = traveller_line.get("TRICKS") or "" score = traveller_line.get("SCORE") try: score = int(score) except (ValueError, TypeError): pass ew_match_points = float(traveller_line.get("EW_MATCH_POINTS")) ns_match_points = float(traveller_line.get("NS_MATCH_POINTS")) # Calculate percentage and score if type(score) is str: # Score is adjusted - USEBIO says it should be A5050 or similar - use 1:3 as percentage percentage = int(score[1:3]) score = "ADJ" else: # Normal numeric score percentage = _percentage_from_match_points( ns_match_points, ew_match_points, ns_flag ) # indicator = _set_indicator_based_on_percentage(percentage) indicator = _set_icon_based_on_percentage(percentage) # highlight row of interest if pair_id in [ns_pair_number, ew_pair_number]: if request.user.system_number in player_dict["system_numbers"][pair_id]: # This user tr_highlight = "bg-warning" else: # Another user tr_highlight = "bg-info" else: tr_highlight = "" row = { "board_number": board_number, "contract": contract, "played_by": played_by, "lead": lead, "tricks": tricks, "score": score, "percentage": percentage, "ns_pair_number": ns_pair_number, "ew_pair_number": ew_pair_number, "ns_pair": ns_pair, "ew_pair": ew_pair, "tr_highlight": tr_highlight, "indicator": indicator, "ns_match_points": ns_match_points, "ew_match_points": ew_match_points, } board_data.append(row) return board_data
[docs] def get_traveller_info(usebio, board_number, player_dict, ns_flag, pair_id, request): """extract traveller information about a board from a usebio record""" for board in usebio["EVENT"]["BOARD"]: this_board_number = int(board.get("BOARD_NUMBER")) if this_board_number == board_number: # Found our board - go through the traveller lines return _get_traveller_info_process_board( board, player_dict, pair_id, board_number, ns_flag, request )
[docs] def calculate_hcp_and_ltc(hand): """calculate the high card points and losing trick count for this board""" hcp = {} ltc = {} for compass in hand: hcp[compass] = 0 ltc[compass] = 0 for suit_name in hand[compass]: suit = hand[compass][suit_name] if suit: # HCP if suit.find("A") >= 0: hcp[compass] += 4 if suit.find("K") >= 0: hcp[compass] += 3 if suit.find("Q") >= 0: hcp[compass] += 2 if suit.find("J") >= 0: hcp[compass] += 1 # LTC if len(suit) == 1 and suit != "A": ltc[compass] += 1 elif len(suit) == 2: if suit == "AK": pass elif suit[0] in ["A", "K"]: ltc[compass] += 1 else: ltc[compass] += 2 elif suit[:3] == "AKQ": pass elif suit[:2] in ["AK", "AQ", "KQ"]: ltc[compass] += 1 elif suit[0] in ["A", "K", "Q"]: ltc[compass] += 2 else: ltc[compass] += 3 return hcp, ltc
def _set_icon_based_on_imp_points(imp_points): """Set a material icon based on IMP points for a single board.""" if imp_points > 5: return "<i class='material-icons' style='color: orange'>star</i>" if imp_points > 1.5: return "<i class='material-icons' style='color: blue'>check_circle</i>" if imp_points > -1.5: return "<span style='color: blue; font-size: larger;'>=</span>" if imp_points > -5: return "<i class='material-icons' style='color: red'>expand_more</i>" return "<i class='material-icons' style='color: red'>cancel</i>"
[docs] def imp_results_summary_view(request, usebio, results_file, masterpoint_type): """Show the summary results for a CROSS_IMP event (single-field or two-field).""" ns_scores = [] ew_scores = [] for item in usebio["PARTICIPANTS"]["PAIR"]: player_1 = item["PLAYER"][0]["PLAYER_NAME"].title() player_2 = item["PLAYER"][1]["PLAYER_NAME"].title() try: player_1_system_number = int(item["PLAYER"][0]["NATIONAL_ID_NUMBER"]) player_2_system_number = int(item["PLAYER"][1]["NATIONAL_ID_NUMBER"]) except TypeError: player_1_system_number = None player_2_system_number = None position = int(item["PLACE"]) masterpoints = int(item.get("MASTER_POINTS_AWARDED", 0)) / 100.0 pair_number = item["PAIR_NUMBER"] direction = item.get("DIRECTION") total_score = float(item["TOTAL_SCORE"]) total_score_display = ( f"+{total_score:.2f}" if total_score >= 0 else f"{total_score:.2f}" ) players_names = _format_pair_name(player_1, player_2) if request.user.system_number in [ player_1_system_number, player_2_system_number, ]: tr_highlight = "bg-warning" else: tr_highlight = "" row = { "player_1": player_1, "player_2": player_2, "players_names": players_names, "player_1_system_number": player_1_system_number, "player_2_system_number": player_2_system_number, "position": position, "masterpoints": masterpoints, "pair_number": pair_number, "total_score": total_score, "total_score_display": total_score_display, "tr_highlight": tr_highlight, } if direction == "NS": ns_scores.append(row) else: ew_scores.append(row) ns_scores = sorted(ns_scores, key=lambda d: d["position"]) ew_scores = sorted(ew_scores, key=lambda d: d["position"]) # Single-field events have no NS pairs — all pairs share one combined ranking single_field = not ns_scores return render( request, "results/usebio/usebio_results_summary_imp_view.html", { "results_file": results_file, "usebio": usebio, "ns_scores": ns_scores, "ew_scores": ew_scores, "single_field": single_field, "masterpoint_type": masterpoint_type, }, )
[docs] @login_required() def imp_pairs_details_view(request, results_file_id, pair_id): """Show board-by-board CROSS_IMP results for a pair.""" results_file = get_object_or_404(ResultsFile, pk=results_file_id) usebio = parse_usebio_file(results_file)["EVENT"] # Get position and total score from participants position = "" pair_total_score = "" pair_total_score_display = "" for item in usebio["PARTICIPANTS"]["PAIR"]: if item["PAIR_NUMBER"] == pair_id: position = int(item["PLACE"]) pair_total_score = float(item["TOTAL_SCORE"]) pair_total_score_display = ( f"+{pair_total_score:.2f}" if pair_total_score >= 0 else f"{pair_total_score:.2f}" ) break player_dict = _get_player_names_by_id(usebio) if results_file.event_type == ResultsFile.EventType.BUTLER_PAIRS: ns_imp_key = "NS_BUTLER_POINTS" ew_imp_key = "EW_BUTLER_POINTS" else: ns_imp_key = "NS_CROSS_IMP_POINTS" ew_imp_key = "EW_CROSS_IMP_POINTS" if "BOARD" not in usebio: return render( request, "results/usebio/usebio_no_boards_warning.html", { "usebio": usebio, "results_file": results_file, "pair_id": pair_id, "pair_name": player_dict["names"].get(pair_id, pair_id), }, ) pair_data = [] last_opponent = None bg_colour = False for board in usebio["BOARD"]: board_number = int(board.get("BOARD_NUMBER")) for traveller_line in board.get("TRAVELLER_LINE"): ns_pair = traveller_line.get("NS_PAIR_NUMBER") ew_pair = traveller_line.get("EW_PAIR_NUMBER") if pair_id not in [ns_pair, ew_pair]: continue ns_flag = pair_id == ns_pair opponents = player_dict["names"].get(ew_pair if ns_flag else ns_pair) opponents_pair_id = ew_pair if ns_flag else ns_pair contract = traveller_line.get("CONTRACT") played_by = traveller_line.get("PLAYED_BY") or "" lead = traveller_line.get("LEAD") or "" tricks = traveller_line.get("TRICKS") or "" score = traveller_line.get("SCORE", "0") ns_imp = float(traveller_line.get(ns_imp_key, 0)) ew_imp = float(traveller_line.get(ew_imp_key, 0)) imp_points = ns_imp if ns_flag else ew_imp imp_display = ( f"+{imp_points:.2f}" if imp_points >= 0 else f"{imp_points:.2f}" ) indicator = _set_icon_based_on_imp_points(imp_points) if opponents_pair_id != last_opponent: bg_colour = not bg_colour last_opponent = opponents_pair_id pair_data.append( { "board_number": board_number, "contract": contract, "played_by": played_by, "lead": lead, "tricks": tricks, "score": score, "opponents": opponents, "opponents_pair_id": opponents_pair_id, "imp_points": imp_points, "imp_display": imp_display, "indicator": indicator, "bg_colour": bg_colour, } ) pair_data = sorted(pair_data, key=lambda d: d["board_number"]) return render( request, "results/usebio/usebio_results_imp_pairs_detail.html", { "usebio": usebio, "results_file": results_file, "pair_data": pair_data, "pair_id": pair_id, "pair_name": player_dict["names"].get(pair_id, pair_id), "position": position, "pair_total_score_display": pair_total_score_display, }, )
def _get_imp_traveller_info_process_board( board, player_dict, pair_id, board_number, ns_flag, request, ns_imp_key, ew_imp_key ): """Process a single board's traveller lines for IMP-scored events (Cross IMP or Butler).""" board_data = [] for traveller_line in board.get("TRAVELLER_LINE"): ns_pair_number = traveller_line.get("NS_PAIR_NUMBER") ns_pair = player_dict["names"].get(ns_pair_number) ew_pair_number = traveller_line.get("EW_PAIR_NUMBER") ew_pair = player_dict["names"].get(ew_pair_number) contract = traveller_line.get("CONTRACT") played_by = traveller_line.get("PLAYED_BY") or "" lead = traveller_line.get("LEAD") or "" tricks = traveller_line.get("TRICKS") or "" score = traveller_line.get("SCORE") try: score = int(score) except (ValueError, TypeError): pass ns_imp_points = float(traveller_line.get(ns_imp_key, 0)) ew_imp_points = float(traveller_line.get(ew_imp_key, 0)) imp_points = ns_imp_points if ns_flag else ew_imp_points indicator = _set_icon_based_on_imp_points(imp_points) if pair_id in [ns_pair_number, ew_pair_number]: if request.user.system_number in player_dict["system_numbers"].get( pair_id, [] ): tr_highlight = "bg-warning" else: tr_highlight = "bg-info" else: tr_highlight = "" board_data.append( { "board_number": board_number, "contract": contract, "played_by": played_by, "lead": lead, "tricks": tricks, "score": score, "ns_pair_number": ns_pair_number, "ew_pair_number": ew_pair_number, "ns_pair": ns_pair, "ew_pair": ew_pair, "tr_highlight": tr_highlight, "indicator": indicator, "ns_imp_points": ns_imp_points, "ew_imp_points": ew_imp_points, "imp_points": imp_points, } ) return board_data
[docs] def get_imp_traveller_info( usebio, board_number, player_dict, ns_flag, pair_id, request, ns_imp_key, ew_imp_key ): """Extract traveller information for an IMP-scored board (Cross IMP or Butler).""" for board in usebio["EVENT"]["BOARD"]: if int(board.get("BOARD_NUMBER")) == board_number: return _get_imp_traveller_info_process_board( board, player_dict, pair_id, board_number, ns_flag, request, ns_imp_key, ew_imp_key, )
[docs] @login_required() def imp_board_view(request, results_file_id, board_number, pair_id): """Show the traveller + hand record for a CROSS_IMP board.""" results_file = get_object_or_404(ResultsFile, pk=results_file_id) usebio = parse_usebio_file(results_file) player_dict = _get_player_names_by_id(usebio["EVENT"]) ns_flag = _get_pair_direction_for_board(usebio, board_number, pair_id) if results_file.event_type == ResultsFile.EventType.BUTLER_PAIRS: ns_imp_key = "NS_BUTLER_POINTS" ew_imp_key = "EW_BUTLER_POINTS" else: ns_imp_key = "NS_CROSS_IMP_POINTS" ew_imp_key = "EW_CROSS_IMP_POINTS" board_data = get_imp_traveller_info( usebio, board_number, player_dict, ns_flag, pair_id, request, ns_imp_key, ew_imp_key, ) hand = {} double_dummy = None if "HANDSET" not in usebio or "BOARD" not in usebio["HANDSET"]: return render( request, "utils/coblt_generic_error_page.html", { "title": "No Board Data Available", "message": "Sorry, we don't have any details available for this board.", }, ) for board in usebio["HANDSET"]["BOARD"]: if int(board["BOARD_NUMBER"]) == board_number: for compass in board["HAND"]: hand[compass["DIRECTION"]] = { "clubs": compass["CLUBS"], "diamonds": compass["DIAMONDS"], "hearts": compass["HEARTS"], "spades": compass["SPADES"], } double_dummy = double_dummy_from_usebio(board["HAND"]) break if not double_dummy: return HttpResponse(f"Board {board_number} not found for this result") # Sort by bridge score — correlates with cross IMP direction and keeps par insertion clean if ns_flag: board_data = sorted( board_data, key=lambda d: d["score"] if isinstance(d["score"], int) else 0, reverse=True, ) else: board_data = sorted( board_data, key=lambda d: d["score"] if isinstance(d["score"], int) else 0, ) dealer, vulnerability = dealer_and_vulnerability_for_board(board_number) par_score, par_string = par_score_and_contract(double_dummy, vulnerability, dealer) board_data = _insert_par_data_into_list(board_data, par_score, par_string, ns_flag) high_card_points, losing_trick_count = calculate_hcp_and_ltc(hand) previous_board = board_number - 1 if board_number > 1 else None total_boards = len(usebio["HANDSET"]["BOARD"]) next_board = board_number + 1 if board_number < total_boards else None pair_contract_level = pair_contract_trump = pair_contract_declarer = None pair_contract_double = "" if pair_id and pair_id != "0": for row in board_data: if pair_id in (row.get("ns_pair_number"), row.get("ew_pair_number")): level, trump, double = _parse_usebio_contract(row.get("contract", "")) played_by = row.get("played_by", "") if level and trump and played_by in ("N", "E", "S", "W"): pair_contract_level = level pair_contract_trump = trump pair_contract_declarer = played_by pair_contract_double = double break return render( request, "results/usebio/usebio_results_board_detail_imp.html", { "usebio": usebio.get("EVENT"), "results_file": results_file, "board_data": board_data, "board_number": board_number, "pair_id": pair_id, "hand": hand, "double_dummy": double_dummy, "dealer": dealer, "vulnerability": vulnerability, "par_score": par_score, "par_string": par_string, "high_card_points": high_card_points, "losing_trick_count": losing_trick_count, "next_board": next_board, "previous_board": previous_board, "total_boards": total_boards, "ns_flag": ns_flag, "total_boards_range": range(1, total_boards + 1), "pair_contract_level": pair_contract_level, "pair_contract_trump": pair_contract_trump, "pair_contract_declarer": pair_contract_declarer, "pair_contract_double": pair_contract_double, }, )
[docs] @login_required() def show_results_for_club_htmx(request): """Show recent results for a club. Called from the club org_profile.""" club = get_object_or_404(Organisation, pk=request.POST.get("club_id")) results = ResultsFile.objects.filter( organisation=club, status=ResultsFile.ResultsStatus.PUBLISHED ).order_by("-created_at") things = cobalt_paginator(request, results) for thing in things: thing.day = thing.created_at.strftime("%A") thing.date = thing.created_at.strftime("%d %b %Y") hx_post = reverse("results:show_results_for_club_htmx") hx_vars = f"club_id:{club.id}" hx_target = "#club-results" return render( request, "results/club/show_results_for_club_htmx.html", { "things": things, "hx_post": hx_post, "hx_vars": hx_vars, "hx_target": hx_target, }, )