Source code for club_sessions.views.imports

import codecs
import csv
import io
import json
import re

from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required

from accounts.views.core import add_un_registered_user_with_mpc_data
from club_sessions.views.core import PLAYING_DIRECTOR, VISITOR, SITOUT
from club_sessions.forms import FileImportForm
from club_sessions.models import SessionEntry, SessionMiscPayment, Session, SessionType
from organisations.models import ClubLog, Organisation
from organisations.views.club_menu import tab_sessions_htmx
from payments.models import OrgPaymentMethod
from rbac.core import rbac_user_has_role
from rbac.views import rbac_forbidden


def _import_file_upload_htmx_simple_csv(request, club, session):
    """Sub to handle simple CSV file. This is a generic format, not from the real world"""

    messages = []
    csv_file = request.FILES["file"]

    # get CSV reader (convert bytes to strings)
    csv_data = csv.reader(codecs.iterdecode(csv_file, "utf-8"))

    # skip header
    next(csv_data, None)

    # process file
    for line_no, line in enumerate(csv_data, start=2):
        # Add dummy name to file import
        line.append("Unknown")
        response = _import_file_upload_htmx_process_line(
            line, line_no, session, club, request
        )
        if response:
            messages.append(response)

    session.import_messages = json.dumps(messages)
    session.save()


def _import_file_upload_htmx_compscore2(request, club, session):
    """Sub to handle simple Compscore2 text file format

    File is like:

    North Shore Bridge Club	Compscore2W7
    WS WED 12.45PM OPEN
    Pr No	Player Names
    NORTH-SOUTH
    1	ALAN ADMIN / BETTY BUNTING (100 / 101)
    2	ANOTHER NAME / ANOTHER PARTNER (9999 / 99999)


    EAST-WEST
    1	COLIN CORGY / DEBBIE DYSON (102 / 103)
    2	PHANTOM / PHANTOM ( / )

    There is a tab character after the table number
    """

    messages = []
    text_file = request.FILES["file"]

    # We get North-South first
    current_direction = ["N", "S"]
    line_no = 0

    lines = text_file.readlines()

    # Go through the lines looking for a valid line, or the change of direction line
    for line in lines:

        # change bytes to str
        line = line.decode("utf-8")
        line_no += 1

        # See if direction changed
        if line.find("EAST-WEST") >= 0:
            current_direction = ["E", "W"]
            continue

        # Look for a valid line
        try:
            parts = line.split("\t")
        except ValueError:
            continue

        # try to get player numbers and names
        # line is (e.g.):
        # 1(tab char)PLAYING DIRECTOR / SIMON SEZ (1 / 118)
        try:
            table = int(parts[0])
            # Any digits are the player numbers
            player1, player2 = re.findall(r"\d+", parts[1])

            # For player names, look for anything before "/" and anything after upto "("
            player_1_file_name, player2_file_name = re.findall(
                r"(.+)\s\/\s(.+)\(", parts[1]
            )[0]
            player2_file_name = player2_file_name.strip()

        except ValueError:
            continue

        # ugly way to loop through player and direction
        player = player1
        player_file_name = player_1_file_name
        for direction in current_direction:

            response = _import_file_upload_htmx_process_line(
                [table, direction, player, player_file_name],
                line_no,
                session,
                club,
                request,
            )
            if response:
                messages.append(response)
            player = player2
            player_file_name = player2_file_name

    # The session title is the second line - strip anything dodgy
    session.description = lines[1].decode("utf-8")[:50]

    # strip anything dodgy
    session.description = re.sub(r"[^0-9a-zA-Z\w]+", " ", session.description)

    session.import_messages = json.dumps(messages)
    session.save()


def _import_file_upload_htmx_compscore3(request, club, session):
    """Sub to handle simple Compscore3 CSV file format

        File is like:

    PairNumber,	Name,	ABF No,	Initial Seating,	Phone
    1,	SHIRLEY RUTTER,	936235,	1-NS,
    1,	YEUNG CHEUNG,	936510,	1-NS,
    2,	CHRISTINE GRECH,	447633,	2-NS,
    2,	LEO VILENSKY,	1049471,	2-NS,

    For a visitor, sit out or playing director we get no system_number. For a sit out the name is PHANTOM

    """

    messages = []

    # Muck about with file format
    text_file = request.FILES["file"]
    file = text_file.read().decode("utf-8")
    reader = csv.reader(io.StringIO(file))

    # Skip header
    next(reader)

    # count the rows, odds are N or E, evens are S or W
    for row_no, row in enumerate(reader, start=1):
        player_file_name = row[1]
        system_number = row[2]
        initial_seating = row[3]
        try:
            table, both_direction = initial_seating.split("-")
        except ValueError:
            # CS3 had a bug where it didn't show phantoms properly and had them with no information
            # likely fixed by the time you read this
            # Ignore this if player is a phantom, we will fill in the gap later
            if player_file_name.strip() != "PHANTOM":
                raise ValueError
            continue
        direction = both_direction[1] if row_no % 2 == 0 else both_direction[0]

        # Handle sit out and playing director
        if not system_number:
            if player_file_name.strip() == "PHANTOM":
                system_number = SITOUT
            elif player_file_name.find("DIRECTOR") >= 0:
                system_number = PLAYING_DIRECTOR
            else:
                system_number = VISITOR

        response = _import_file_upload_htmx_process_line(
            [table, direction, system_number, player_file_name],
            row_no,
            session,
            club,
            request,
        )
        if response:
            messages.append(response)

    # The session title is part of the file name
    session.description = text_file.name

    # Reformat
    session.description = session.description.replace(".csv", "")
    session.description = session.description.replace("Names - ", "")

    # strip anything dodgy
    session.description = re.sub(r"[^0-9a-zA-Z\w]+", " ", session.description)[:50]

    session.import_messages = json.dumps(messages)
    session.save()


[docs] @login_required() def import_file_upload_htmx(request): """ Upload player names for a session Called from club admin to create a new session and fill it with players from the uploaded file """ # Get club club = get_object_or_404(Organisation, pk=request.POST.get("club_id")) # Check access - we don't use the decorator as we don't have a session yet club_role = f"club_sessions.sessions.{club.id}.edit" if not rbac_user_has_role(request.user, club_role): return rbac_forbidden(request, club_role) form = FileImportForm(request.POST, request.FILES) if form.is_valid(): # If we got a session type then use that, otherwise use first (only) one if "session_type" in request.POST: session_type = get_object_or_404( SessionType, pk=request.POST.get("session_type") ) else: session_type = SessionType.objects.filter(organisation=club).first() session = Session( director=request.user, session_type=session_type, description="Added session", default_secondary_payment_method=club.default_secondary_payment_method, ) session.save() try: if "generic_csv" in request.POST: _import_file_upload_htmx_simple_csv(request, club, session) elif "compscore2" in request.POST: _import_file_upload_htmx_compscore2(request, club, session) elif "compscore3" in request.POST: _import_file_upload_htmx_compscore3(request, club, session) except (IndexError, ValueError): SessionEntry.objects.filter(session=session).delete() session.delete() return tab_sessions_htmx(request, message="Invalid file format") _import_file_upload_htmx_fill_in_table_gaps(session) else: print(form.errors) session = None response = tab_sessions_htmx(request) if session: response[ "HX-Trigger" ] = f"""{{"file_upload_finished":{{"id": "{session.id}" }}}}""" return response
def _import_file_upload_htmx_process_line(line, line_no, session, club, request): """Process a single line from the import file""" message = None # Extract data try: table = line[0] direction = line[1] system_number = int(line[2]) player_file_name = line[3] except ValueError: return f"Invalid data found on line {line_no}. Ignored." # If this user isn't registered then add them from the MPC user_type, response = add_un_registered_user_with_mpc_data( system_number, club, request.user ) if response: ClubLog( organisation=club, actor=request.user, action=f"Added un-registered user {response['GivenNames']} {response['Surname']} through session import", ).save() message = ( f"Added new user to system - {response['GivenNames']} {response['Surname']}" ) # set payment method based upon user type if user_type == "user" and system_number not in [VISITOR, PLAYING_DIRECTOR, SITOUT]: payment_method = OrgPaymentMethod.objects.filter( organisation=club, active=True, payment_method="Bridge Credits" ).first() if not payment_method: payment_method = session.default_secondary_payment_method else: payment_method = session.default_secondary_payment_method # See if this club is using the last payment method for the user if club.use_last_payment_method_for_player_sessions: last_payment = ( SessionEntry.objects.filter( system_number=system_number, session__session_type__organisation=club ) .order_by("pk") .last() ) try: # JPG Query - added check for inactivated methods if ( last_payment.payment_method.payment_method != "IOU" and last_payment.payment_method.active ): payment_method = last_payment.payment_method except AttributeError: pass # create session entry session_entry = SessionEntry( session=session, pair_team_number=table, seat=direction, system_number=system_number, payment_method=payment_method, player_name_from_file=player_file_name, ) # set sitout or director to zero fee and paid if session_entry.system_number in [SITOUT, PLAYING_DIRECTOR]: session_entry.fee = 0 session_entry.is_paid = True try: session_entry.save() except IntegrityError as err: return f"Invalid data found on line {line_no}. {err}." # Add additional session payments if set if session.additional_session_fee > 0: SessionMiscPayment( session_entry=session_entry, description=session.additional_session_fee_reason, amount=session.additional_session_fee, ).save() return message def _import_file_upload_htmx_fill_in_table_gaps(session): """if there were missing positions in the file upload we want to fill them in. e.g if 3E had an error we don't want to have a missing seat""" # Get all data session_entries = SessionEntry.objects.filter(session=session) tables = {} for session_entry in session_entries: if session_entry.pair_team_number not in tables: tables[session_entry.pair_team_number] = [] tables[session_entry.pair_team_number].append(session_entry.seat) # look for errors for table, value in tables.items(): for compass in "NSEW": if compass not in value: SessionEntry( session=session, pair_team_number=table, seat=compass, system_number=-1, fee=0, ).save()