""" This module has the views that are used by normal players """
import calendar
from datetime import datetime, date, timedelta
from decimal import Decimal
import uuid
import pytz
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponseNotFound, HttpResponse
from django.template import loader
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db.models import Sum, Q
from django.db import transaction
from django.utils import timezone
from django.views.decorators.http import require_POST
from organisations.models import Organisation
from organisations.club_admin_core import is_player_a_member
from payments.views.payments_api import payment_api_interactive
from utils.templatetags.cobalt_tags import cobalt_credits
from notifications.views.core import (
send_cobalt_email_with_template,
create_rbac_batch_id,
)
from notifications.models import BatchID
from accounts.models import User, TeamMate
from rbac.core import (
rbac_user_allowed_for_model,
rbac_user_has_role,
)
from rbac.views import rbac_forbidden
from payments.views.core import (
update_account,
update_organisation,
)
from cobalt.settings import (
BRIDGE_CREDITS,
TIME_ZONE,
TBA_PLAYER,
ABF_STATES,
)
from events.models import (
Congress,
Category,
Event,
Session,
EventEntry,
EventEntryPlayer,
EVENT_PLAYER_FORMAT_SIZE,
BasketItem,
PlayerBatchId,
EventLog,
Bulletin,
PartnershipDesk,
CongressDownload,
CONGRESS_TYPES,
)
from events.forms import (
PartnershipForm,
)
from events.views.core import (
events_payments_primary_callback,
notify_conveners,
get_basket_for_user,
)
from utils.utils import cobalt_paginator, cobalt_round
TZ = pytz.timezone(TIME_ZONE)
[docs]
def congress_listing(request, reverse_list=False):
"""Show list of events
reverse_list is used to show historic data
"""
# get states from settings
states = [state_list[1] for state_list in ABF_STATES.values()]
states.sort()
# If not logged in, show different view
if not request.user.is_authenticated:
return congress_listing_logged_out(request)
# Hardcode the venue types. We don't match with the database.
# People want to filter by online or face to face, not mixed
# For online we have "O" for all online or "OB" for just BBO etc
congress_venue_types = [("F", "Face-to-Face"), ("O", "Online - All")]
for online_platform in Congress.OnlinePlatform:
# skip Unknown - looks ugly and covered by selecting all online
if online_platform == "U":
continue
congress_venue_types.append(
(f"O{online_platform}", f"Online - {online_platform.label}")
)
return render(
request,
"events/players/congress_listing.html",
{
"states": states,
"congress_types": CONGRESS_TYPES,
"congress_venue_types": congress_venue_types,
"reverse_list": reverse_list,
},
)
[docs]
def congress_listing_logged_out(request):
"""Congress view when logged out"""
# Get today
date_now = date.today()
congresses = (
Congress.objects.filter(
Q(start_date__gte=date_now) | (Q(end_date__gte=date_now))
)
.filter(status="Published")
.select_related("congress_master__org")
.order_by("start_date")
)
month_list = {}
for congress in congresses:
month = congress.start_date.strftime("%B %Y")
if month not in month_list:
month_list[month] = []
month_list[month].append(congress)
return render(
request,
"events/players/congress_listing_logged_out.html",
{"month_list": month_list},
)
[docs]
@require_POST
def congress_listing_data_htmx(request):
"""Returns the data for the events listing page.
There is a limited number of future events, so we just return them all.
For historic events, we will get a much larger list, so we paginate it but loading 6 months at a time.
"""
# default values - only used going backwards, but needed for template call
date_string = None
show_back_arrow = None
show_forward_arrow = None
# Get any parameters from the form
state = request.POST.get("state")
congress_type = request.POST.get("congress_type")
congress_venue_type = request.POST.get("congress_venue_type")
congress_search_string = request.POST.get("congress_search_string")
# Reverse list means we want the historic date (closed events, going backwards)
reverse_list = request.POST.get("reverse_list")
# Optional - what was the last date we showed the user
last_data_date = request.POST.get("last_data_date")
# Optional - does user want to go further back or come forward
where_to_go = request.POST.get("where_to_go")
# Get today
date_now = date.today()
if reverse_list:
# We are going backwards
(
congresses,
date_string,
show_back_arrow,
show_forward_arrow,
last_data_date,
) = congress_listing_data_backwards(last_data_date, where_to_go, date_now)
else:
# Going forwards, show everything
congresses = (
Congress.objects.filter(
Q(start_date__gte=date_now) | (Q(end_date__gte=date_now))
)
.filter(status="Published")
.select_related("congress_master__org")
.order_by("start_date")
)
# Now add modifiers for the queryset
if state != "All":
congresses = congresses.filter(congress_master__org__state=state)
if congress_type != "All":
congresses = congresses.filter(congress_type=congress_type)
if congress_venue_type != "All":
# If user searches for face-to-face or online also show mixed
if congress_venue_type == "F":
congresses = congresses.filter(congress_venue_type__in=["F", "M"])
# Check for online - we can get "O" or "Ox"
if congress_venue_type[0] == "O":
congresses = congresses.filter(congress_venue_type__in=["O", "M"])
if len(congress_venue_type) == 2:
# User wants specific platforms e.g. BBO or StepBridge
online_platform = congress_venue_type[1]
congresses = congresses.filter(online_platform=online_platform)
if congress_search_string:
congresses = congresses.filter(
Q(name__icontains=congress_search_string)
| Q(congress_master__org__name__icontains=congress_search_string)
)
# We want to order the congresses into months to put in boxes
month_list = {}
for congress in congresses:
month = congress.start_date.strftime("%B %Y")
if month not in month_list:
month_list[month] = []
month_list[month].append(congress)
return render(
request,
"events/players/congress_listing_data_htmx.html",
{
"month_list": month_list,
"last_data_date": last_data_date,
"show_back_arrow": show_back_arrow,
"show_forward_arrow": show_forward_arrow,
"date_string": date_string,
"reverse_list": reverse_list,
},
)
[docs]
def congress_listing_data_backwards(last_data_date, where_to_go, date_now):
"""sub to handle going backwards
Args:
last_data_date(str): date used for last display, can be None
where_to_go(str): "back", "forward" or None
date_now(date): date today
Returns:
congresses(queryset): Queryset of Congress objects to show
date_string(str): string to display to user representing this date range
show_back_arrow(bool): show the back arrow
show_forward_arrow(bool): show forward arrow
last_data_date(str): UPDATED date string for this data set - returned to us later
"""
# default arrows
show_forward_arrow = False
if last_data_date:
# Not on page 1
show_forward_arrow = True
if where_to_go == "back":
# Get end date - one month earlier
year = int(last_data_date.split("-")[0])
month = int(last_data_date.split("-")[1]) - 1
if month == 0:
year -= 1
month = 12
ref_date_end = date(year, month, calendar.monthrange(year, month)[1])
else:
# Get end date - 12 months earlier
year = int(last_data_date.split("-")[0]) + 1
month = int(last_data_date.split("-")[1]) + 1
if month == 13:
year -= 1
month = 12
# If we are back at the start, handle that
if date_now.month == month and date_now.year == year:
ref_date_end = date_now
show_forward_arrow = False
else:
ref_date_end = date(year, month, calendar.monthrange(year, month)[1])
else:
# first page of the reverse view, end on today
ref_date_end = date_now
month = int(ref_date_end.strftime("%m"))
year = int(ref_date_end.strftime("%Y"))
# Get the previous 6 months
month -= 6
if month < 1:
year -= 1
month += 12
# set the start of the period we are looking at and last_data_date, so we get this returned to us if user wants more
ref_date_start = date(year, month, 1)
last_data_date = ref_date_start.strftime("%Y-%m")
congresses = (
Congress.objects.filter(
start_date__lt=ref_date_end, start_date__gte=ref_date_start
)
.filter(status="Published")
.select_related("congress_master__org")
.order_by("-start_date")
)
# Check if we have more data
ref_date_day_before = ref_date_start - timedelta(days=1)
show_back_arrow = bool(
Congress.objects.filter(start_date__lt=ref_date_day_before)
.filter(status="Published")
.exists()
)
if ref_date_end == date_now:
date_string = f"Yesterday back to {ref_date_start:%B %Y}"
else:
date_string = f"{ref_date_end:%B %Y} back to {ref_date_start:%B %Y}"
return congresses, date_string, show_back_arrow, show_forward_arrow, last_data_date
[docs]
def view_congress(request, congress_id, fullscreen=False):
"""basic view of an event.
Can be called when not logged in.
Args:
request(HTTPRequest): standard user request
congress_id(int): congress to view
fullscreen(boolean): if true shows just the page, not the standard surrounds
Also accepts a GET parameter of msg to display for returning from event entry
Returns:
page(HTTPResponse): page with details about the event
"""
congress = get_object_or_404(Congress, pk=congress_id)
# Which template to use
if fullscreen:
master_template = "empty.html"
template = "events/players/congress.html"
elif request.user.is_authenticated:
master_template = "base.html"
template = "events/players/congress.html"
# check if published or user has rights
if congress.status != "Published":
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
else:
template = "events/players/congress_logged_out.html"
master_template = "empty.html"
if congress.status != "Published":
return HttpResponseNotFound("Not published")
if request.method == "GET" and "msg" in request.GET:
msg = request.GET["msg"]
else:
msg = None
# We need to build a table for the program from events that has
# rowspans for the number of days. This is too complex for the
# template so we build it here.
#
# basic structure:
#
# <tr><td>Simple Pairs Event<td>Monday<td>12/09/2025 10am<td>Links</tr>
#
# <tr><td rowspan=2>Long Teams Event<td>Monday <td>13/09/2025 10am<td rowspan=2>Links</tr>
# <tr> !Nothing! <td>Tuesday<td>14/09/2025 10am !Nothing! </tr>
# get all events for this congress so we can build the program table.
# We use list_priority_order to set the order within events on the same day if required
events = congress.event_set.all().order_by("-list_priority_order")
if not events:
return HttpResponseNotFound(
"No Events set up for this congress. Please notify the convener."
)
# add start date and sort by start date
events_list = {}
for event in events:
event.event_start_date = event.start_date()
if event.event_start_date:
events_list[event] = event.event_start_date
events_list_sorted = {
key: value
for key, value in sorted(events_list.items(), key=lambda item: item[1])
}
# check on eligibility to enter a members only event
# Note - not relevant if the user is not logged in
if congress.members_only and request.user.is_authenticated:
eligible_to_enter = is_player_a_member(
congress.congress_master.org,
request.user.system_number,
)
else:
eligible_to_enter = True
# program_list will be passed to the template, each entry is a <tr> element
program_list = []
includes_teams_event = False
# every day of an event gets its own row so we use rowspan for event name and links
for event in events_list_sorted:
program = {}
# see if user has entered already
if request.user.is_authenticated:
program["entry"] = event.already_entered(request.user)
else:
program["entry"] = False
# get all sessions for this event plus days and number of rows (# of days)
sessions = event.session_set.all()
# We want the first session for each day
days = sessions.order_by("session_date", "session_start").distinct(
"session_date"
)
rows = days.count()
total_entries = (
EventEntry.objects.filter(event=event)
.exclude(entry_status="Cancelled")
.count()
)
program["event_id"] = event.id
program["event_name"] = event.event_name
program[
"entries_total"
] = f"<td rowspan='{rows}'><span class='title'>{total_entries}</td>"
# day td
first_row_for_event = True
for day in days:
if first_row_for_event:
players_per_entry = EVENT_PLAYER_FORMAT_SIZE[event.player_format]
if event.player_format == "Teams":
players_per_entry = 4
if congress.members_only:
entry_fee = cobalt_credits(
cobalt_round(event.member_entry_fee / players_per_entry)
)
else:
if congress.allow_member_entry_fee:
members_fee = cobalt_credits(
cobalt_round(event.member_entry_fee / players_per_entry)
)
non_members_fee = cobalt_credits(
cobalt_round(event.entry_fee / players_per_entry)
)
entry_fee = (
f"{members_fee} members<br>{non_members_fee} non-members"
)
else:
entry_fee = cobalt_credits(
cobalt_round(event.entry_fee / players_per_entry)
)
if event.player_format == "Teams":
entry_fee += "*"
includes_teams_event = True
program[
"event"
] = f"<td rowspan='{rows}'><span class='title'>{event.event_name}</td><td rowspan='{rows}'><span class='title'>{entry_fee}</span></td>"
if program["entry"]:
program[
"links"
] = f"<td rowspan='{rows}'><a href='/events/congress/event/change-entry/{congress.id}/{event.id}' class='btn btn-block btn-sm btn-primary'>View Your Entry</a>"
else:
# See if taking entries
is_open, reason = event.is_open_with_reason()
if is_open:
if eligible_to_enter:
program[
"links"
] = f"<td rowspan='{rows}'><a href='/events/congress/event/enter/{congress.id}/{event.id}' class='btn btn-block btn-sm btn-success'>Enter</a>"
else:
program[
"links"
] = f"<td rowspan='{rows}'><button class='btn btn-block btn-sm btn-success' disabled>Enter</button>"
else:
program[
"links"
] = f"<td rowspan='{rows}' class='text-center'>{reason}"
# Handle common parts of links
program["links"] += (
f"<a href='/events/congress/event/view-event-entries/{congress.id}/{event.id}' "
"class='btn btn-block btn-sm btn-info'>View Entries</a>"
)
# Logged out needs extra breaks
if not request.user.is_authenticated:
program["links"] = program["links"].replace("</a>", "</a><br>")
first_row_for_event = False
program["day"] = "<td>%s</td>" % day.session_date.strftime("%A")
# handle multiple times on same day
# time needs a bit of manipulation as %-I not supported (maybe just Windows?)
session_start_hour = day.session_start.strftime("%I")
session_start_hour = "%d" % int(session_start_hour)
session_minutes = day.session_start.strftime("%M")
if session_minutes == "00":
time_str = "%s - %s%s" % (
day.session_date.strftime("%d-%m-%Y"),
session_start_hour,
day.session_start.strftime("%p"),
)
else:
time_str = "%s - %s:%s" % (
day.session_date.strftime("%d-%m-%Y"),
session_start_hour,
day.session_start.strftime("%M%p"),
)
times = Session.objects.filter(
event__pk=day.event.id, session_date=day.session_date
).order_by("session_start")
for time in times[1:]:
session_start_hour = time.session_start.strftime("%I")
session_start_hour = "%d" % int(session_start_hour)
session_minutes = time.session_start.strftime("%M")
if session_minutes == "00":
time_str = "%s & %s%s" % (
time_str,
session_start_hour,
time.session_start.strftime("%p"),
)
else:
time_str = "%s & %s:%s" % (
time_str,
session_start_hour,
time.session_start.strftime("%M%p"),
)
program["time"] = "<td>%s</td>" % time_str.lower() # AM -> pm
program_list.append(program)
program = {}
# Get bulletins
bulletins = Bulletin.objects.filter(congress=congress).order_by("-pk")
# Get downloads
downloads = CongressDownload.objects.filter(congress=congress).order_by("pk")
# Check for admin rights to show edit/manage buttons
if request.user.is_authenticated:
is_admin = rbac_user_has_role(
request.user, f"events.org.{congress.congress_master.org.id}.edit"
)
# try global admin
if not is_admin:
is_admin = rbac_user_has_role(request.user, "events.org.edit")
else:
is_admin = False
return render(
request,
template,
{
"congress": congress,
"template": master_template,
"program_list": program_list,
"bulletins": bulletins,
"downloads": downloads,
"msg": msg,
"is_admin": is_admin,
"includes_teams_event": includes_teams_event,
},
)
def _checkout_perform_action(request):
"""sub function for the checkout screen, also called directly if only one item in cart"""
# Need to mark the entries that this is covering. The payment call is asynchronous so
# we can't just load all the open basket_entries when we come back or more could have been
# added.
# Get list of event_entry_player records to include.
event_entries = BasketItem.objects.filter(player=request.user).values_list(
"event_entry"
)
event_entry_players = (
EventEntryPlayer.objects.filter(event_entry__in=event_entries)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.filter(payment_type="my-system-dollars")
.distinct()
)
# players that are using club pp to pay are pending payments not unpaid
# unpaid would prompt the player to pay for event which is not desired here
# However, if we are auto paying club PP we want to ignore this is exclude status="Paid"
event_entry_player_club_pp = (
EventEntryPlayer.objects.filter(event_entry__in=event_entries)
.filter(payment_type="off-system-pp")
.exclude(payment_status="Paid")
.distinct()
)
for event_entry in event_entry_player_club_pp:
event_entry.payment_status = "Pending Manual"
event_entry.save()
unique_id = str(uuid.uuid4())
# map this user (who is paying) to the batch id
PlayerBatchId(player=request.user, batch_id=unique_id).save()
# Get total amount
amount = event_entry_players.aggregate(Sum("entry_fee"))
if amount["entry_fee__sum"]: # something for Payments to do
for event_entry_player in event_entry_players:
event_entry_player.batch_id = unique_id
event_entry_player.save()
# Log it
EventLog(
event=event_entry_player.event_entry.event,
actor=request.user,
action=f"Checkout for event entry {event_entry_player.event_entry.id} for {event_entry_player.player}",
event_entry=event_entry_player.event_entry,
).save()
return payment_api_interactive(
request=request,
member=request.user,
description="Congress Entry",
amount=amount["entry_fee__sum"],
route_code="EVT",
route_payload=unique_id,
next_url=reverse("events:enter_event_success"),
# url_fail=reverse("events:enter_event_payment_fail"),
book_internals=False,
payment_type="Entry to an event",
)
else: # no payment required go straight to the callback
events_payments_primary_callback("Success", unique_id)
messages.success(
request, "Entry successful", extra_tags="cobalt-message-success"
)
return redirect("events:enter_event_success")
[docs]
@login_required()
def checkout(request):
"""Checkout view - make payments, get details"""
basket_items = BasketItem.objects.filter(player=request.user).exclude(
event_entry__entry_status="Cancelled"
)
if request.method == "POST":
return _checkout_perform_action(request)
# Not a POST, build the form
# Get list of event_entry_player records to include.
event_entries = basket_items.values_list("event_entry")
event_entry_players = (
EventEntryPlayer.objects.filter(event_entry__in=event_entries).exclude(
payment_status="Paid"
)
# .exclude(payment_status="Free")
)
# get totals per congress
congress_total = {}
total_today = Decimal(0)
total_entry_fee = Decimal(0)
for event_entry_player in event_entry_players:
total_entry_fee += event_entry_player.entry_fee
congress = event_entry_player.event_entry.event.congress
if congress in congress_total:
congress_total[congress]["entry_fee"] += event_entry_player.entry_fee
if event_entry_player.payment_type == "my-system-dollars":
congress_total[congress]["today"] += event_entry_player.entry_fee
total_today += event_entry_player.entry_fee
else:
congress_total[congress]["later"] += event_entry_player.entry_fee
else:
congress_total[congress] = {"entry_fee": event_entry_player.entry_fee}
if event_entry_player.payment_type == "my-system-dollars":
congress_total[congress]["today"] = event_entry_player.entry_fee
total_today += event_entry_player.entry_fee
congress_total[congress]["later"] = Decimal(0.0)
else:
congress_total[congress]["later"] = event_entry_player.entry_fee
congress_total[congress]["today"] = Decimal(0.0)
grouped_by_congress = {}
for event_entry_player in event_entry_players:
congress = event_entry_player.event_entry.event.congress
data = {
"event_entry_player": event_entry_player,
"entry_fee": congress_total[congress]["entry_fee"],
"today": congress_total[congress]["today"],
"later": congress_total[congress]["later"],
}
if congress in grouped_by_congress:
grouped_by_congress[congress].append(data)
else:
grouped_by_congress[congress] = [data]
# The name basket_items is used by the base template so use a different name
return render(
request,
"events/players/checkout.html",
{
"grouped_by_congress": grouped_by_congress,
"total_today": total_today,
"total_entry_fee": total_entry_fee,
"total_outstanding": total_entry_fee - total_today,
"basket_items_list": basket_items,
},
)
[docs]
@login_required()
def view_events(request):
"""View Events you are entered into"""
# get event entries with event entry player entries for this user
event_entries_list = (
EventEntry.objects.filter(evententryplayer__player=request.user).exclude(
entry_status="Cancelled"
)
).values_list("id")
# get events where event_entries_list is entered
events = Event.objects.filter(evententry__in=event_entries_list)
# Only include the ones in the future
event_dict = {}
for event in events:
start_date = event.start_date()
if start_date >= datetime.now().date():
event.entry_status = event.entry_status(request.user)
event_dict[event] = start_date
# sort by start date
event_list = {
key: value
for key, value in sorted(event_dict.items(), key=lambda item: item[1])
}
# check for pending payments
pending_payments = (
EventEntryPlayer.objects.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(payment_type="off-system-pp")
.filter(player=request.user)
.exclude(event_entry__entry_status="Cancelled")
)
return render(
request,
"events/players/view_events.html",
{"event_list": event_list, "pending_payments": pending_payments},
)
[docs]
@login_required()
@transaction.atomic
def pay_outstanding(request):
"""Pay anything that is not in a status of paid"""
# Get outstanding payments for this user
event_entry_players = (
EventEntryPlayer.objects.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.filter(player=request.user)
.exclude(event_entry__entry_status="Cancelled")
)
# redirect if nothing owing
if not event_entry_players:
messages.warning(
request, "You have nothing due to pay", extra_tags="cobalt-message-warning"
)
return redirect("events:view_events")
# Get total amount
amount = event_entry_players.aggregate(Sum("entry_fee"))
# identifier
unique_id = str(uuid.uuid4())
# apply identifier to each record
for event_entry_player in event_entry_players:
event_entry_player.batch_id = unique_id
event_entry_player.payment_type = "my-system-dollars"
event_entry_player.save()
# Log it
EventLog(
event=event_entry_player.event_entry.event,
actor=request.user,
action=f"Checkout for {request.user}",
event_entry=event_entry_player.event_entry,
).save()
# map this user (who is paying) to the batch id
PlayerBatchId(player=request.user, batch_id=unique_id).save()
# let payments API handle getting the money
return payment_api_interactive(
request=request,
member=request.user,
description="Congress Entry",
amount=amount["entry_fee__sum"],
route_code="EVT",
route_payload=unique_id,
next_url=reverse("events:enter_event_success"),
payment_type="Entry to an event",
book_internals=False,
)
# This was the updated version - not released due to testing differences and shortage of time
# def view_event_entries(request, congress_id, event_id):
# """Screen to show entries to an event"""
#
# congress = get_object_or_404(Congress, pk=congress_id)
# event = get_object_or_404(Event, pk=event_id)
# event_entries = (
# EventEntry.objects.filter(event=event)
# .exclude(entry_status="Cancelled")
# .select_related("category")
# .order_by("entry_complete_date")
# )
#
# # identify this users entry
# my_entry = None
# event_entry_players = (
# EventEntryPlayer.objects.filter(event_entry__event=event)
# .select_related("player", "event_entry")
# .order_by("pk")
# )
#
# if request.user.is_authenticated:
# for player in event_entry_players:
# if player.player == request.user:
# my_entry = player.event_entry
# break
#
# # Loop through event entries and add event entry players in order they entered
# # We use the values we already have from the database to prevent additional calls
# for event_entry in event_entries:
# event_entry.this_event_entry_players = []
# for event_entry_player in event_entry_players:
# if event_entry_player.event_entry == event_entry:
# event_entry.this_event_entry_players.append(event_entry_player)
#
# categories = Category.objects.filter(event=event).exists()
# date_string = event.print_dates()
#
# # See if this user is already entered
# try:
# user_entered = (
# EventEntryPlayer.objects.filter(event_entry__event=event)
# .filter(player=request.user)
# .exclude(event_entry__entry_status="Cancelled")
# .exists()
# )
# except TypeError:
# # may be anonymous
# user_entered = False
#
# return render(
# request,
# "events/players/view_event_entries.html",
# {
# "congress": congress,
# "event": event,
# "event_entries": event_entries,
# "categories": categories,
# "date_string": date_string,
# "user_entered": user_entered,
# "my_entry": my_entry,
# },
# )
[docs]
def view_event_entries(request, congress_id, event_id):
"""Screen to show entries to an event"""
# Get data
congress = get_object_or_404(Congress, pk=congress_id)
event = get_object_or_404(Event, pk=event_id)
# Assume user not entered, check further down
user_entered = False
user_entry_id = 0
# Get player entries for this event
player_entries = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(event_entry__entry_status="Cancelled")
.order_by("event_entry__entry_complete_date", "id")
.select_related("event_entry", "player")
)
# convert to dictionary keyed by event entry with a list of player entries for that event entry
entries_dict = {}
for player_entry in player_entries:
# add to dict if not there already
if player_entry.event_entry not in entries_dict:
entries_dict[player_entry.event_entry] = []
# Check if this is the entry made by this use
if request.user.is_authenticated and player_entry.player == request.user:
user_entry_id = player_entry.event_entry_id
user_entered = True
entries_dict[player_entry.event_entry].append(player_entry)
# Get categories
categories = Category.objects.filter(event=event).exists()
# Get nice formatted date string
date_string = event.print_dates()
# Check sessions for different start time. 1pm on first day, then 10am etc
sessions = Session.objects.filter(event=event).order_by(
"session_date", "session_start"
)
earliest_start_by_day = {}
for session in sessions:
if session.session_date not in earliest_start_by_day:
# not in dict so add it
earliest_start_by_day[session.session_date] = session.session_start
elif session.session_start < earliest_start_by_day[session.session_date]:
# in dict and earlier than current entry, update it
earliest_start_by_day[session.session_date] = session.session_start
# see if start times are the same
multiple_start_times = len(set(earliest_start_by_day.values())) > 1
return render(
request,
"events/players/view_event_entries.html",
{
"congress": congress,
"event": event,
"entries_dict": entries_dict,
"categories": categories,
"date_string": date_string,
"user_entered": user_entered,
"multiple_start_times": multiple_start_times,
"sessions": sessions,
"user_entry_id": user_entry_id,
},
)
[docs]
@login_required()
def enter_event_success(request):
"""url for payments to go to after successful entry"""
messages.success(
request,
"Payment complete. You will receive a confirmation email.",
extra_tags="cobalt-message-success",
)
return view_events(request)
[docs]
@login_required()
@transaction.atomic
def edit_event_entry(
request, congress_id=None, event_id=None, pay_status=None, event_entry_id=None
):
"""edit an event entry
pay_status is used by the "Pay Now" and "Pay All" buttons as the call
to payment_api cannot add a success or failure message.
event_entry_id is provided for when someone is editing an entry that they created that doesn't have them as a player
"""
# If we got an event entry, then try to load it.
if event_entry_id:
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
# Check this is legitimate
if not (
event_entry.primary_entrant == request.user
or EventEntryPlayer.objects.filter(
event_entry=event_entry, player=request.user
).exists()
):
return HttpResponse(
f"Invalid request - attempt to edit EventEntry:{event_entry.id} which has a Primary Entrant of {event_entry.primary_entrant} by another player {request.user}"
)
# Load the event
event = event_entry.event
congress = event.congress
# Otherwise, try to load entry
else:
# Load the event
event = get_object_or_404(Event, pk=event_id)
congress = get_object_or_404(Congress, pk=congress_id)
# find matching event entries
event_entry_player = (
EventEntryPlayer.objects.filter(player=request.user)
.filter(event_entry__event=event)
.exclude(event_entry__entry_status="Cancelled")
.first()
)
if event_entry_player:
event_entry = event_entry_player.event_entry
else:
# see if primary_entrant
event_entry = (
EventEntry.objects.filter(primary_entrant=request.user)
.exclude(entry_status="Cancelled")
.filter(event=event)
.first()
)
if not event_entry:
# not entered so redirect
return redirect(
"events:enter_event", event_id=event.id, congress_id=congress_id
)
# add a flag to the event_players to identify players 5 and 6
event_entry_players = EventEntryPlayer.objects.filter(
event_entry=event_entry
).order_by("first_created_date")
# Check if payment method can be edited
for event_entry_player in event_entry_players:
if (
event_entry_player.payment_status not in ["Paid", "Free"]
or event_entry_player.payment_type == "off-system-pp"
):
event_entry_player.can_edit_payment_type = True
else:
event_entry_player.can_edit_payment_type = False
pay_count = 0
for count, event_entry_player in enumerate(event_entry_players, start=1):
if count > 4:
event_entry_player.extra_player = True
# check payment outstanding so we can show a Pay All button if mmore than 1
if event_entry_player.entry_fee - event_entry_player.payment_received > 0:
pay_count += 1
pay_all = pay_count >= 2
# Check if still in basket
in_basket = BasketItem.objects.filter(event_entry=event_entry).count()
# Optional bits
# see if event has categories
categories = Category.objects.filter(event=event)
# if we got a free format question that will already be on the event
# We can handle that in the HTML
# check if pay_status was set and add a message
if pay_status:
if pay_status == "success":
messages.success(
request,
"Payment successful",
extra_tags="cobalt-message-success",
)
elif pay_status == "fail":
messages.error(
request,
"Payment failed",
extra_tags="cobalt-message-error",
)
# valid payment methods
payment_methods = congress.get_payment_methods()
# Include ask them to pay
payment_methods.append(("other-system-dollars", "Ask them to pay"))
# prevent last person being TBA if others are already TBA
has_only_one_real_player = (
event_entry_players.exclude(player_id=TBA_PLAYER).count() == 1
)
return render(
request,
"events/players/edit_event_entry.html",
{
"event": event,
"congress": congress,
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"categories": categories,
"in_basket": in_basket,
"pay_all": pay_all,
"payment_methods": payment_methods,
"has_only_one_real_player": has_only_one_real_player,
"check_membership": congress.members_only,
},
)
[docs]
@login_required()
@transaction.atomic
def delete_event_entry(request, event_entry_id):
"""Delete an entry to an event"""
# Get data
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
event_entry_players = EventEntryPlayer.objects.filter(
event_entry=event_entry
).order_by("-pk")
# Validate
status, response = _delete_event_entry_validation(request, event_entry)
if not status:
return response
# get total paid
amount = event_entry_players.aggregate(Sum("payment_received"))
total = amount["payment_received__sum"]
# check if still in basket
basket_item = BasketItem.objects.filter(event_entry=event_entry).first()
# handle a post request
if request.method == "POST":
return _delete_event_entry_handle_post(
request, event_entry, event_entry_players, basket_item
)
return render(
request,
"events/players/delete_event_entry.html",
{
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"total": total,
"basket_item": basket_item,
},
)
def _delete_event_entry_validation(request, event_entry):
"""Perform validation for the delete entry screen"""
# check if already cancelled
if event_entry.entry_status == EventEntry.EntryStatus.CANCELLED:
error = "This entry is already in a cancelled state."
title = "This entry is already cancelled"
return False, render(
request, "events/players/error.html", {"title": title, "error": error}
)
# check if in future
if event_entry.event.start_date() < datetime.now().date():
error = "You cannot change an entry after the start date of the event."
title = "This Event has already started"
return False, render(
request, "events/players/error.html", {"title": title, "error": error}
)
# check if passed the automatic refund date
if (
event_entry.event.congress.automatic_refund_cutoff
and event_entry.event.congress.automatic_refund_cutoff < datetime.now().date()
):
error = "You need to contact the tournament organiser directly to make any changes to this entry."
title = "It is too near to the start of this event"
return False, render(
request, "events/players/error.html", {"title": title, "error": error}
)
# Check if this is their entry to edit
if not event_entry.user_can_change(request.user):
error = """You are not the person who made this entry or one of the players.
You cannot change this entry."""
title = "You do not have permission"
return False, render(
request, "events/players/error.html", {"title": title, "error": error}
)
# If we got here it is okay
return True, True
def _delete_event_entry_handle_post(
request, event_entry, event_entry_players, basket_item
):
"""Handle a user posting a delete request to the delete event entry screen"""
# If in basket and no payments then delete
if basket_item and not event_entry_players.exclude(payment_received=0).exists():
return _delete_event_entry_handle_post_basket(event_entry, request, basket_item)
event_entry.entry_status = EventEntry.EntryStatus.CANCELLED
event_entry.save()
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Event entry {event_entry.id} cancelled",
event_entry=event_entry,
).save()
messages.success(
request,
f"Event entry for {event_entry.event} deleted",
extra_tags="cobalt-message-success",
)
# create batch ID
batch_id = create_rbac_batch_id(
rbac_role=f"events.org.{event_entry.event.congress.congress_master.org.id}.edit",
organisation=event_entry.event.congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_ENTRY,
description=f"Entry cancelled to {event_entry.event.event_name}",
complete=True,
)
batch_size = 0
# Notify conveners
batch_size += _delete_event_entry_handle_post_notify_conveners(
request, event_entry, event_entry_players, batch_id
)
# Handle refunds
refunds, cancelled = _delete_event_entry_handle_post_refunds(
request, event_entry, event_entry_players
)
# Notify people
batch_size += _delete_event_entry_handle_post_notify_users(
request, event_entry, event_entry_players, refunds, cancelled, batch_id
)
# update the batch_id
batch_id.batch_size = batch_size
batch_id.state = BatchID.BATCH_STATE_COMPLETE
batch_id.save()
return redirect("events:view_events")
def _delete_event_entry_handle_post_basket(event_entry, request, basket_item):
"""Delete an entry that was in the basket and had no payments made"""
EventLog(
event=event_entry.event,
actor=request.user,
action="Deleted event from cart",
).save()
messages.success(
request,
"Event deleted from shopping cart",
extra_tags="cobalt-message-success",
)
basket_item.delete()
event_entry.delete()
return redirect("events:view_events")
def _delete_event_entry_handle_post_notify_conveners(
request, event_entry, event_entry_players, batch_id
):
"""Notify conveners when entry is deleted
Returns count of emails sent"""
html = loader.render_to_string(
"events/players/email/notify_convener_about_event_entry_cancellation.html",
{
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"user": request.user,
},
)
return notify_conveners(
event_entry.event.congress,
event_entry.event,
f"Entry cancelled to {event_entry.event.event_name}",
html,
batch_id=batch_id,
)
def _delete_event_entry_handle_post_refunds(request, event_entry, event_entry_players):
"""Handle refunds to players who withdraw from events"""
# dict of people getting money and what they are getting
refunds = {}
# list of people getting cancelled
cancelled = []
# Update records and return paid money
for event_entry_player in event_entry_players:
# check for refunds
amount = float(event_entry_player.payment_received)
if amount > 0.0:
event_entry_player.payment_received = Decimal(0)
event_entry_player.save()
amount_str = "%.2f credits" % amount
# Check for blank paid_by - can happen if manually edited
if not event_entry_player.paid_by:
event_entry_player.paid_by = event_entry_player.player
# Check for TBA - if set electrocute the Convener and pay to primary entrant
if event_entry_player.paid_by.id == TBA_PLAYER:
event_entry_player.paid_by = event_entry.primary_entrant
# create payments in org account
update_organisation(
organisation=event_entry.event.congress.congress_master.org,
amount=-amount,
description=f"Refund to {event_entry_player.paid_by} for {event_entry.event.event_name}",
payment_type="Refund",
member=event_entry_player.paid_by,
event=event_entry.event,
)
# create payment for member
update_account(
organisation=event_entry.event.congress.congress_master.org,
amount=amount,
description=f"Refund for {event_entry.event}",
payment_type="Refund",
member=event_entry_player.paid_by,
)
# Log it
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Refund of {amount_str} to {event_entry_player.paid_by}",
event_entry=event_entry,
).save()
messages.success(
request,
f"Refund of {amount_str} to {event_entry_player.paid_by} successful",
extra_tags="cobalt-message-success",
)
# record refund amount
if event_entry_player.paid_by in refunds:
refunds[event_entry_player.paid_by] += amount
else:
refunds[event_entry_player.paid_by] = amount
# also record players getting cancelled
if event_entry_player.player not in cancelled:
cancelled.append(event_entry_player.player)
return refunds, cancelled
def _delete_event_entry_handle_post_notify_users(
request, event_entry, event_entry_players, refunds, cancelled, batch_id
):
"""After the refunds have been done and the entry cancelled we let them know
Returns count of emails sent"""
sent_count = 0
for member, value in refunds.items():
# cancelled and refunds are not necessarily the same
# someone can enter an event and then be swapped out
if member in cancelled:
cancelled.remove(member) # already taken care of
# skip TBA
if member.id == TBA_PLAYER:
continue
html = loader.render_to_string(
"events/players/email/player_event_entry_cancellation.html",
{
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"user": request.user,
"member": member,
"value": value,
},
)
context = {
"name": member.first_name,
"title": "Event Entry - %s Cancelled" % event_entry.event.congress,
"email_body": html,
"link": "/events/view",
"link_text": "View Congress Entries",
"subject": "Entry Cancelled - %s" % event_entry.event,
}
send_cobalt_email_with_template(
to_address=member.email,
context=context,
batch_id=batch_id,
apply_default_template_for_club=event_entry.event.congress.congress_master.org,
)
sent_count += 1
# There can be people left on cancelled who didn't pay for their entry - let them know
for member in cancelled:
# skip TBA
if member.id == TBA_PLAYER:
continue
html = loader.render_to_string(
"events/players/email/player_event_entry_cancellation.html",
{
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"user": request.user,
"member": member,
"value": 0,
},
)
context = {
"name": member.first_name,
"title": "Event Entry - %s Cancelled" % event_entry.event.congress,
"email_body": html,
"link": "/events/view",
"link_text": "View Congress Entries",
"subject": "Entry Cancelled - %s" % event_entry.event,
"box_colour": "#dc3545",
}
send_cobalt_email_with_template(
to_address=member.email,
context=context,
batch_id=batch_id,
apply_default_template_for_club=event_entry.event.congress.congress_master.org,
)
sent_count += 1
return sent_count
[docs]
@login_required()
def third_party_checkout_player(request, event_entry_player_id):
"""Used by edit entry screen to pay for a single other player in the team"""
event_entry_player = get_object_or_404(EventEntryPlayer, pk=event_entry_player_id)
return _third_party_checkout_entry_common(
request, event_entry_player.event_entry, [event_entry_player]
)
[docs]
@login_required()
def third_party_checkout_entry(request, event_entry_id):
"""Used by edit entry screen to pay for all outstanding fees on an entry"""
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
event_entry_players = EventEntryPlayer.objects.filter(event_entry=event_entry)
return _third_party_checkout_entry_common(request, event_entry, event_entry_players)
def _third_party_checkout_entry_common(request, event_entry, event_entry_players):
"""Takes a list of event_entry_players (or a Queryset, as long as it is iterable and returns EventEntryPlayer)
for a single event_entry and handles the checkout process for this user to pay for them"""
# Get this user's event_entry_player object for this event entry if there is one
event_entry_players_me = EventEntryPlayer.objects.filter(
event_entry=event_entry
).filter(player=request.user)
# check for association with entry
if not event_entry_players_me and event_entry.primary_entrant != request.user:
error = """You are not the person who made this entry or one of the players.
You cannot change this entry."""
title = "You do not have permission"
return render(
request, "events/players/error.html", {"title": title, "error": error}
)
# check for cancelled
if event_entry.entry_status == EventEntry.EntryStatus.CANCELLED:
error = "This entry has been cancelled. You cannot change this entry."
title = "Cancelled Entry"
return render(
request, "events/players/error.html", {"title": title, "error": error}
)
# check amount
amount = 0.0
for event_entry_player in event_entry_players:
amount += float(
event_entry_player.entry_fee - event_entry_player.payment_received
)
if amount <= 0:
error = """You have tried to pay for an entry, but there is nothing owing. Don't worry, everything seems fine.
"""
title = "Nothing owing"
return render(
request, "events/players/error.html", {"title": title, "error": error}
)
unique_id = str(uuid.uuid4())
# map this user (who is paying) to the batch id
PlayerBatchId(player=request.user, batch_id=unique_id).save()
# add batch id to the event_entry_player objects who are getting paid for
for event_entry_player in event_entry_players:
if event_entry_player.payment_received - event_entry_player.entry_fee == 0:
# player had already paid don't do anything
continue
event_entry_player.batch_id = unique_id
event_entry_player.payment_type = "my-system-dollars"
event_entry_player.save()
# make payment
return payment_api_interactive(
request=request,
member=request.user,
description="Congress Entry",
amount=amount,
route_code="EV2",
route_payload=unique_id,
next_url=reverse(
"events:edit_event_entry",
kwargs={
# "event_id": event_entry.event.id,
# "congress_id": event_entry.event.congress.id,
"event_entry_id": event_entry.id,
"pay_status": "success",
},
),
payment_type="Entry to an event",
book_internals=False,
)
[docs]
@login_required()
def enter_event_payment_fail(request):
"""payment required auto top up which failed"""
error = "Auto top up failed. We were unable to process your transaction."
title = "Payment Failed"
return render(
request, "events/players/error.html", {"title": title, "error": error}
)
[docs]
def enter_event_non_post(event, congress, request, enter_for_another):
"""Handle a blank entry. Build the page and return to user."""
our_form = []
# get payment types for this congress
pay_methods = congress.get_payment_methods()
# Get teammates for this user - exclude anyone entered already
all_team_mates = TeamMate.objects.filter(user=request.user)
team_mates_list = all_team_mates.values_list("team_mate")
entered_team_mates = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(event_entry__entry_status="Cancelled")
.filter(player__in=team_mates_list)
.values_list("player")
)
team_mates = all_team_mates.exclude(team_mate__in=entered_team_mates)
name_list = [(0, "Search..."), (TBA_PLAYER, "TBA")]
for team_mate in team_mates:
if congress.members_only:
# filter out non-members amongst team mates
if is_player_a_member(
congress.congress_master.org,
team_mate.team_mate.system_number,
):
item = team_mate.team_mate.id, f"{team_mate.team_mate.full_name}"
name_list.append(item)
else:
item = team_mate.team_mate.id, f"{team_mate.team_mate.full_name}"
name_list.append(item)
# set values for player0 (the user)
entry_fee, discount, reason, description = event.entry_fee_for(request.user)
payment_selected = pay_methods[0]
entry_fee_pending = ""
entry_fee_you = entry_fee
# Player 0 settings depend upon whether this is the first entry or entering for someone else
if enter_for_another:
# Add ask them to pay as an option
pay_methods_player0 = pay_methods.copy()
if congress.payment_method_system_dollars:
pay_methods_player0.append(("other-system-dollars", "Ask them to pay"))
player0 = {
"id": TBA_PLAYER,
"payment_choices": pay_methods_player0,
"payment_selected": payment_selected,
"name": "TBA",
"name_choices": name_list,
"entry_fee_you": f"{entry_fee_you}",
"entry_fee_pending": f"{entry_fee_pending}",
}
else:
player0 = {
"id": request.user.id,
"payment_choices": pay_methods.copy(),
"payment_selected": payment_selected,
"name": request.user.full_name,
"name_choices": name_list,
"entry_fee_you": f"{entry_fee_you}",
"entry_fee_pending": f"{entry_fee_pending}",
}
# add another option for everyone except the current user
if congress.payment_method_system_dollars:
pay_methods.append(("other-system-dollars", "Ask them to pay"))
# set values for other players
team_size = EVENT_PLAYER_FORMAT_SIZE[event.player_format]
min_entries = team_size
if team_size == 6:
min_entries = 4
name_selected = None
for ref in range(1, team_size):
payment_selected = pay_methods[0]
entry_fee = None
# only ABF dollars go in the you column
if payment_selected == "my-system-dollars":
entry_fee_you = entry_fee
entry_fee_pending = ""
else:
entry_fee_you = ""
entry_fee_pending = entry_fee
if payment_selected == "their-system-dollars":
augment_payment_types = [
("their-system-dollars", f"Their {BRIDGE_CREDITS}")
]
else:
augment_payment_types = []
item = {
"player_no": ref,
"payment_choices": pay_methods + augment_payment_types,
"payment_selected": payment_selected,
"name_choices": name_list,
"name_selected": name_selected,
"entry_fee_you": entry_fee_you,
"entry_fee_pending": entry_fee_pending,
}
our_form.append(item)
# Start time of event
sessions = Session.objects.filter(event=event).order_by(
"session_date", "session_start"
)
event_start = sessions.first()
# use reason etc from above to see if discounts apply
alert_msg = None
if reason == "Early discount":
date_field = event.congress.early_payment_discount_date.strftime("%d/%m/%Y")
alert_msg = [
"Early Entry Discount",
f"You qualify for an early discount if you enter now. You will save {cobalt_credits(discount)} on this event. Discount valid until {date_field}.",
]
if reason == "Youth discount":
alert_msg = [
"Youth Discount",
f"You qualify for a youth discount for this event. A saving of {cobalt_credits(discount)}.",
]
if reason == "Youth+Early discount":
alert_msg = [
"Youth and Early Discount",
f"You qualify for a youth discount as well as an early entry discount for this event. A saving of {cobalt_credits(discount)}.",
]
# categories
categories = Category.objects.filter(event=event)
# organiser check
is_tournament_admin = rbac_user_has_role(
request.user, f"events.org.{congress.congress_master.org.id}.edit"
)
return render(
request,
"events/players/enter_event.html",
{
"player0": player0,
"our_form": our_form,
"congress": congress,
"event": event,
"categories": categories,
"sessions": sessions,
"event_start": event_start,
"alert_msg": alert_msg,
"discount": discount,
"description": description,
"min_entries": min_entries,
"enter_for_another": enter_for_another,
"is_tournament_admin": is_tournament_admin,
"check_membership": congress.members_only,
},
)
def _get_team_mates_for_event(user, event):
"""Get available team mates for this event and this user"""
# Get teammates for this user - exclude anyone entered already
all_team_mates = TeamMate.objects.filter(user=user)
team_mates_list = all_team_mates.values_list("team_mate")
entered_team_mates = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(event_entry__entry_status="Cancelled")
.filter(player__in=team_mates_list)
.values_list("player")
)
return all_team_mates.exclude(team_mate__in=entered_team_mates)
[docs]
def enter_event_post(request, congress, event):
"""Handle a post request to enter an event"""
# create event_entry
event_entry = EventEntry()
event_entry.event = event
event_entry.primary_entrant = request.user
event_entry.comment = request.POST.get("comment", None)
# see if we got a category
category = request.POST.get("category", None)
if category:
event_entry.category = get_object_or_404(Category, pk=category)
# see if we got a free format answer
answer = request.POST.get("free_format_answer", None)
if answer:
event_entry.free_format_answer = answer[:60]
# see if we got a team name
team_name = request.POST.get("team_name", None)
if team_name and team_name != "":
event_entry.team_name = team_name
event_entry.save()
# Log it
EventLog(
event=event,
actor=event_entry.primary_entrant,
action=f"Event entry {event_entry.id} created",
event_entry=event_entry,
).save()
# add to basket
basket_item = BasketItem()
basket_item.player = request.user
basket_item.event_entry = event_entry
basket_item.save()
# COB-569: Teams of 5 or 6
# ------------------------
# Prior to this change team members 5 and 6 were always treated as free
# and the entry fee was rederived in this function with Event.entry_fee_for.
# With COB-569 this function needs to cater for both a recalculation of team fees
# (team members 5/6 sharing the cost) or not (team members 5/6 free).
# The normal solution would be to retirve all of the data from the page, but this
# would involve extensive changes (eg returning teh feesa dn reasons). A simpler
# approach is to count the number of paying entrants and recalculate accordingly
# (ie if >4 have paid => user has recalced, otherwise use the old logic)
# Get players from form
players = {}
player_payments = {}
team_size_for_fee_calc = 0
for p_id in range(6):
# names of player id and payment method fields
p_string = f"player{p_id}"
ppay_string = f"player{p_id}_payment"
if p_string in request.POST:
p_string_value = request.POST.get(p_string)
if p_string_value != "":
players[p_id] = get_object_or_404(User, pk=int(p_string_value))
player_payments[p_id] = request.POST.get(ppay_string)
if player_payments[p_id] is None:
player_payments[p_id] = "Free"
if player_payments[p_id] != "Free":
team_size_for_fee_calc += 1
recalc_done = team_size_for_fee_calc > 4
# validate
if (event.player_format == "Pairs" and len(players) != 2) or (
event.player_format == "Teams" and len(players) < 4
):
print("invalid number of entries")
return
# create player entries
for p_id in range(len(players)):
event_entry_player = EventEntryPlayer()
event_entry_player.event_entry = event_entry
event_entry_player.player = players[p_id]
if recalc_done and player_payments[p_id] != "Free":
# team of 5/6 and a recalculation done, so treat all paying team members the
# same and split the costs
entry_fee, discount, reason, description = event.entry_fee_for(
event_entry_player.player, actual_team_size=team_size_for_fee_calc
)
event_entry_player.entry_fee = entry_fee
event_entry_player.reason = reason
event_entry_player.payment_type = player_payments[p_id]
else:
# pre COB-569 logic
entry_fee, discount, reason, description = event.entry_fee_for(
event_entry_player.player
)
if p_id < 4:
event_entry_player.entry_fee = entry_fee
event_entry_player.reason = reason
event_entry_player.payment_type = player_payments[p_id]
else:
# team of 5/6 but no recalculation so 5/6 are free
event_entry_player.entry_fee = 0
event_entry_player.reason = "Team > 4"
event_entry_player.payment_status = "Free"
event_entry_player.payment_type = "Free"
# set payment status depending on payment type
if event_entry_player.payment_status not in [
"Paid",
"Free",
] and event_entry_player.payment_type in [
"bank-transfer",
"cash",
"cheque",
]:
event_entry_player.payment_status = "Pending Manual"
event_entry_player.save()
# Log it
EventLog(
event=event,
actor=event_entry.primary_entrant,
action=f"Event entry player {event_entry_player.id} created for {event_entry_player.player}",
event_entry=event_entry,
).save()
# Check status
event_entry.check_if_paid()
if "now" in request.POST:
# if only one thing in basket, go straight to checkout
if get_basket_for_user(request.user) == 1:
return _checkout_perform_action(request)
else:
return redirect("events:checkout")
else: # add to cart and keep shopping
msg = "Added to your cart"
return redirect(f"/events/congress/view/{event.congress.id}?msg={msg}#program")
[docs]
@login_required()
def enter_event(request, congress_id, event_id, enter_for_another=0):
"""enter an event
Some people want to enter on behalf of their friends so we allow an extra parameter to handle this.
"""
# Load the event
event = get_object_or_404(Event, pk=event_id)
congress = get_object_or_404(Congress, pk=congress_id)
# Check if already entered or entering for someone else
if not enter_for_another and event.already_entered(request.user):
return redirect(
"events:edit_event_entry", event_id=event.id, congress_id=event.congress.id
)
# Check if entries are open
if not event.is_open():
return render(request, "events/players/event_closed.html", {"event": event})
# Check if full
if event.is_full():
return render(request, "events/players/event_full.html", {"event": event})
# Check if draft
if congress.status != "Published":
return render(request, "events/players/event_closed.html", {"event": event})
if request.method == "POST":
return enter_event_post(request, congress, event)
else:
return enter_event_non_post(event, congress, request, enter_for_another)
[docs]
@login_required()
def view_event_partnership_desk(request, congress_id, event_id):
"""Show the partnership desk for an event"""
event = get_object_or_404(Event, pk=event_id)
partnerships = PartnershipDesk.objects.filter(event=event)
# admins can see private entries
role = "events.org.%s.edit" % event.congress.congress_master.org.id
admin = rbac_user_has_role(request.user, role)
already = bool(partnerships.filter(player=request.user))
return render(
request,
"events/players/view_event_partnership_desk.html",
{
"partnerships": partnerships,
"event": event,
"admin": admin,
"already": already,
},
)
[docs]
@login_required()
def partnership_desk_signup(request, congress_id, event_id):
"""sign up for the partnership desk"""
event = get_object_or_404(Event, pk=event_id)
if request.method == "POST":
form = PartnershipForm(request.POST)
if form.is_valid():
form.save()
messages.success(
request,
"Partnership request accepted. Look out for emails from potential partners.",
extra_tags="cobalt-message-success",
)
return redirect(
"events:view_event_partnership_desk",
event_id=event.id,
congress_id=event.congress.id,
)
else:
print(form.errors)
else:
form = PartnershipForm()
return render(
request,
"events/players/partnership_desk_signup.html",
{"form": form, "event": event},
)
[docs]
@login_required()
def show_congresses_for_club_htmx(request):
"""Show upcoming congresses for a club. Called from the club org_profile."""
club = get_object_or_404(Organisation, pk=request.POST.get("club_id"))
congresses = Congress.objects.filter(
congress_master__org=club, status="Published", end_date__gte=timezone.now()
).order_by("start_date")
things = cobalt_paginator(request, congresses)
hx_post = reverse("events:show_congresses_for_club_htmx")
hx_vars = f"club_id:{club.id}"
hx_target = "#club-congresses"
return render(
request,
"events/players/clubs/show_congresses_for_club_htmx.html",
{
"things": things,
"hx_post": hx_post,
"hx_vars": hx_vars,
"hx_target": hx_target,
},
)
[docs]
@login_required()
def get_other_entries_to_event_for_user_htmx(request, event_id, this_event_entry_id):
"""
Get entries for this user in this event which don't include them.
Required to allow a user to edit entries that they have made which are not their own.
event_entry_id is the one that calls us, we return all others
"""
# Get event
event = get_object_or_404(Event, pk=event_id)
# Get entries made by this user, where they aren't a player, exclude the one provided
# Also include entries for this user (not primary)
entries = (
# only want entries for this event
EventEntry.objects.filter(event=event)
# Not cancelled
.exclude(entry_status="Cancelled")
# exclude the provided event_id, this is the one asking for others so they don't want themselves back
.exclude(pk=this_event_entry_id)
# primary entrant OR any player
.filter(primary_entrant=request.user).distinct()
# .filter(
# Q(primary_entrant=request.user) | Q(evententryplayer__player=request.user)
# ).distinct()
)
return render(
request,
"events/players/get_other_entries_to_event_for_user_htmx.html",
{"entries": entries},
)