""" The file has the code relating to a convener managing an existing event """
import csv
from datetime import timedelta
from itertools import chain
from threading import Thread
import bleach
from dateutil.relativedelta import relativedelta
from django.shortcuts import render, get_object_or_404, redirect
from django.forms import formset_factory
from django.http import HttpResponse, JsonResponse
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.utils import timezone, dateformat
from django.db.models import Sum
from django.db.utils import IntegrityError
from events.decorators import check_convener_access
from events.views.core import (
sort_events_by_start_date,
get_completed_congresses_with_money_due,
fix_closed_congress,
)
from notifications.models import BlockNotification, BatchID, BatchActivity, Recipient
from notifications.views.core import (
contact_member,
send_cobalt_email_with_template,
create_rbac_batch_id,
)
from logs.views import log_event
from django.db import transaction, connection
from utils.views.general import download_csv
from events.models import (
Congress,
Category,
Event,
EventEntry,
EventEntryPlayer,
EVENT_PLAYER_FORMAT_SIZE,
EventLog,
EventPlayerDiscount,
Bulletin,
BasketItem,
)
from accounts.models import User
import requests
from events.forms import (
EventEntryPlayerForm,
RefundForm,
EventPlayerDiscountForm,
EmailForm,
BulletinForm,
LatestNewsForm,
OffSystemPPForm,
EventEntryPlayerTBAForm,
)
from rbac.views import rbac_user_has_role, rbac_forbidden
from payments.views.core import (
org_balance,
update_account,
update_organisation,
)
from django.contrib import messages
import copy
from cobalt.settings import (
GLOBAL_ORG,
BRIDGE_CREDITS,
TIME_ZONE,
TBA_PLAYER,
BLEACH_ALLOWED_TAGS,
BLEACH_ALLOWED_ATTRIBUTES,
BLEACH_ALLOWED_STYLES,
ALL_SYSTEM_ACCOUNTS,
ALL_SYSTEM_ACCOUNT_SYSTEM_NUMBERS,
)
from utils.utils import cobalt_paginator
import pytz
from decimal import Decimal
from cobalt.settings import GLOBAL_MPSERVER
TZ = pytz.timezone(TIME_ZONE)
[docs]
@login_required()
def admin_summary(request, congress_id, message=""):
"""Admin View"""
congress = get_object_or_404(Congress, pk=congress_id)
events = Event.objects.filter(congress=congress)
# check access
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
total = {
"entries": 0,
"tables": 0.0,
"due": Decimal(0),
"paid": Decimal(0),
"pending": Decimal(0),
}
# calculate summary
for event in events:
event_entries = EventEntry.objects.filter(event=event).exclude(
entry_status="Cancelled"
)
event.entries = event_entries.count()
if event.entry_early_payment_discount:
event.early_fee = event.entry_fee - event.entry_early_payment_discount
else:
event.early_fee = event.entry_fee
# calculate tables
players_per_entry = EVENT_PLAYER_FORMAT_SIZE[event.player_format]
# Teams of 3 - only need 3 to make a table
if players_per_entry == 3:
players_per_entry = 4
# For teams we only have 4 per table
if event.player_format == "Teams":
players_per_entry = 4
event.tables = event.entries * players_per_entry / 4.0
# remove decimal if not required
if event.tables == int(event.tables):
event.tables = int(event.tables)
# Get the event entry players for this event
event_entry_list = event_entries.values_list("id")
event_entry_players = EventEntryPlayer.objects.filter(
event_entry__in=event_entry_list
)
# Total entry fee due
event.due = event_entry_players.exclude(
event_entry__entry_status="Cancelled"
).aggregate(Sum("entry_fee"))["entry_fee__sum"]
if event.due is None:
event.due = Decimal(0)
event.paid = event_entry_players.exclude(
event_entry__entry_status="Cancelled"
).aggregate(Sum("payment_received"))["payment_received__sum"]
if event.paid is None:
event.paid = Decimal(0)
event.pending = event.due - event.paid
# update totals
total["entries"] += event.entries
total["tables"] += event.tables
total["due"] += event.due
total["paid"] += event.paid
total["pending"] += event.pending
# fix total formatting
if total["tables"] == int(total["tables"]):
total["tables"] = int(total["tables"])
# add start date and sort by start date
events_list_sorted = sort_events_by_start_date(events)
return render(
request,
"events/congress_admin/summary.html",
{
"events": events_list_sorted,
"total": total,
"congress": congress,
"message": message,
},
)
[docs]
@login_required()
def admin_event_summary(request, event_id):
"""Admin Event View"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = f"events.org.{event.congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
event_entries = (
EventEntry.objects.filter(event=event)
.exclude(entry_status="Cancelled")
.order_by("first_created_date")
.prefetch_related("evententryplayer_set__player")
)
print(event_entries.query)
# build summary
total_received = Decimal(0.0)
total_outstanding = Decimal(0.0)
total_entry_fee = Decimal(0.0)
for event_entry in event_entries:
# event_entry_players = EventEntryPlayer.objects.filter(event_entry=event_entry)
event_entry_players = event_entry.evententryplayer_set.all()
event_entry.received = Decimal(0.0)
event_entry.outstanding = Decimal(0.0)
event_entry.entry_fee = Decimal(0.0)
event_entry.players = []
event_entry.status = "Deceased, Alive"
event_entry.masterpoints = "100,45"
for event_entry_player in event_entry_players:
received = event_entry_player.payment_received or Decimal(0.0)
event_entry.received += received
event_entry.outstanding += event_entry_player.entry_fee - received
event_entry.entry_fee += event_entry_player.entry_fee
event_entry.players.append(event_entry_player)
total_received += received
total_outstanding += event_entry_player.entry_fee - received
total_entry_fee += event_entry_player.entry_fee
# check on categories
categories = Category.objects.filter(event=event).exists()
return render(
request,
"events/congress_admin/event_summary.html",
{
"event": event,
"event_entries": event_entries,
"total_received": total_received,
"total_outstanding": total_outstanding,
"total_entry_fee": total_entry_fee,
"categories": categories,
},
)
[docs]
@login_required()
def admin_evententry(request, evententry_id):
"""Admin Event Entry View"""
event_entry = get_object_or_404(EventEntry, pk=evententry_id)
event = event_entry.event
congress = event.congress
role = f"events.org.{congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
event_entry_players = EventEntryPlayer.objects.filter(
event_entry=event_entry
).order_by("first_created_date")
has_categories = Category.objects.filter(event=event).exists()
event_logs = EventLog.objects.filter(event_entry=event_entry).order_by("-id")
return render(
request,
"events/congress_admin/event_entry.html",
{
"event_entry": event_entry,
"event": event,
"congress": congress,
"event_entry_players": event_entry_players,
"event_logs": event_logs,
"has_categories": has_categories,
},
)
[docs]
@login_required()
def admin_event_entry_recalculate_htmx(request, evententry_id):
"""Recalculate entry fees for a team of 5/6"""
event_entry = get_object_or_404(EventEntry, pk=evententry_id)
event = event_entry.event
congress = event.congress
role = f"events.org.{congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if not event_entry.recalculate_fees(default_payment_type="other-system-dollars"):
# should never happen as button should be disabled
return HttpResponse("Recalculate not allowed")
response = HttpResponse("Redirecting...", status=302)
response["HX-Redirect"] = reverse(
"events:admin_evententry",
kwargs={"evententry_id": evententry_id},
)
return response
[docs]
@login_required()
def admin_evententryplayer(request, evententryplayer_id):
"""Admin Event Entry Player View"""
event_entry_player = get_object_or_404(EventEntryPlayer, pk=evententryplayer_id)
old_entry = copy.copy(event_entry_player)
event = event_entry_player.event_entry.event
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if request.method == "POST":
form = EventEntryPlayerForm(request.POST, instance=event_entry_player)
if form.is_valid():
form.save()
# check if event entry payment status has changed
event_entry_player.event_entry.check_if_paid()
# Delete from players basket if present
BasketItem.objects.filter(
event_entry=event_entry_player.event_entry
).delete()
messages.success(
request, "Entry updated", extra_tags="cobalt-message-success"
)
# Log it
for changed in form.changed_data:
old_value = getattr(old_entry, changed)
new_value = getattr(event_entry_player, changed)
action = f"Convener Action: Changed {changed} from {old_value} to {new_value} on Entry:{old_entry.id} - {event_entry_player.event_entry}"
log_action = f"Convener Action: Changed {changed} from {old_value} to {new_value} on Entry:{event_entry_player.event_entry.href}"
EventLog(
event=event,
event_entry=event_entry_player.event_entry,
actor=request.user,
action=action,
).save()
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=log_action,
)
return redirect(
"events:admin_evententry",
evententry_id=event_entry_player.event_entry.id,
)
else:
print(form.errors)
else:
form = EventEntryPlayerForm(instance=event_entry_player)
tba_form = EventEntryPlayerTBAForm(instance=event_entry_player)
return render(
request,
"events/congress_admin/event_entry_player.html",
{"event_entry_player": event_entry_player, "form": form, "tba_form": tba_form},
)
[docs]
@login_required()
def admin_event_csv(request, event_id):
"""Download a CSV file with details of the entries"""
event = get_object_or_404(Event, pk=event_id)
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get details
entries = event.evententry_set.exclude(entry_status="Cancelled").order_by(
"first_created_date"
)
entries.prefetch_related("evententryplayer_set")
local_dt = timezone.localtime(timezone.now(), TZ)
today = dateformat.format(local_dt, "Y-m-d H:i:s")
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={event}.csv"
writer = csv.writer(response)
writer.writerow(
[event.event_name, "Downloaded by %s" % request.user.full_name, today]
)
# Event Entry details
header = [
"Players",
"Entry Fee",
"Received",
"Outstanding",
"Status",
"First Created Date",
"Entry Complete Date",
]
categories = Category.objects.filter(event=event).exists()
if categories:
header.append("Category")
if event.free_format_question:
header.append(f"{event.free_format_question}")
header.append("Player comment")
header.append("Organiser Notes")
if event.allow_team_names:
header = ["Team Name"] + header
writer.writerow(header)
for row in entries:
players = ""
received = Decimal(0)
entry_fee = Decimal(0)
for player in row.evententryplayer_set.order_by("pk").all():
if player.player.id == TBA_PLAYER and player.override_tba_name:
players += (
player.override_tba_name + "(manually set by administrator) - "
)
else:
players += player.player.full_name + " - "
try:
received += player.payment_received
except TypeError:
pass # ignore if payment_received is None
entry_fee += player.entry_fee
# remove trailing " - "
players = players[:-3]
local_dt = timezone.localtime(row.first_created_date, TZ)
local_dt2 = timezone.localtime(row.entry_complete_date, TZ)
this_row = [
players,
entry_fee,
received,
entry_fee - received,
row.entry_status,
dateformat.format(local_dt, "Y-m-d H:i:s"),
dateformat.format(local_dt2, "Y-m-d H:i:s"),
]
if categories:
this_row.append(row.category)
if event.free_format_question:
this_row.append(row.free_format_answer)
this_row.append(row.comment)
this_row.append(row.notes)
if event.allow_team_names:
this_row = [row.get_team_name()] + this_row
writer.writerow(this_row)
# Event Entry Player details
writer.writerow([])
writer.writerow([])
writer.writerow(
[
"Primary Entrant",
"Player",
"Player - First Name",
"Player - Last Name",
"Player - Email",
"Player - contact",
"Player - Number",
"Player - Status",
"Player - masterpoints",
"Payment Type",
"Entry Fee",
"Received",
"Outstanding",
"Entry Fee Reason",
"Payment Status",
]
)
for entry in entries:
for row in entry.evententryplayer_set.all():
if row.payment_received:
outstanding = row.entry_fee - row.payment_received
else:
outstanding = row.entry_fee
masterpoints, status = get_player_mp_stats(row.player)
# Use the override name if this is TBA and name is set
if row.player.id == TBA_PLAYER and row.override_tba_name:
names = row.override_tba_name.split(" ")
if len(names) > 1:
player_first_name = names[0]
player_last_name = " ".join(names[1:])
else:
player_first_name = row.override_tba_name
player_last_name = ""
player_last_name = f"{player_last_name}(manually set by administrator)"
else:
player_first_name = row.player.first_name
player_last_name = row.player.last_name
writer.writerow(
[
entry.primary_entrant,
row.player,
player_first_name,
player_last_name,
row.player.email,
row.player.mobile,
row.player.system_number,
status,
masterpoints,
row.payment_type,
row.entry_fee,
row.payment_received,
outstanding,
row.reason,
row.payment_status,
]
)
# Log it
EventLog(event=event, actor=request.user, action=f"CSV Download of {event}").save()
return response
[docs]
@login_required()
def admin_event_csv_scoring(request, event_id):
"""Download a CSV file with info to import to a scoring program"""
event = get_object_or_404(Event, pk=event_id)
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get details
entries = event.evententry_set.exclude(entry_status="Cancelled").order_by(
"first_created_date"
)
entries.select_related("event_entry_player")
print(entries.query)
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={event} - Scoring.csv"
writer = csv.writer(response)
_csv_event_writer(writer, entries, event)
# Log it
EventLog(
event=event, actor=request.user, action=f"CSV Download of scoring for {event}"
).save()
return response
[docs]
@login_required()
def admin_congress_csv_scoring(request, congress_id):
"""Download a CSV file with info to import to a scoring program. For whole congress"""
congress = get_object_or_404(Congress, pk=congress_id)
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={congress} - Scoring.csv"
writer = csv.writer(response)
events = Event.objects.filter(congress=congress)
events.prefetch_related("evententry_set")
for event in events:
writer.writerow(
[
event,
]
)
entries = event.evententry_set.exclude(entry_status="Cancelled").order_by(
"first_created_date"
)
_csv_event_writer(writer, entries, event)
# Log it
EventLog(
event=event,
actor=request.user,
action=f"CSV Download of scoring for whole congress. {congress}",
).save()
return response
def _csv_event_writer(writer, entries, event):
"""sub task to create event listing for CSV output"""
title = "Pair No" if event.player_format == "Pairs" else "Team No"
# Event Entry details
header = [
title,
"Name",
"ABF Number",
"Player phone",
"Player Email",
"Team Name",
"Category",
"question",
"Player comment",
"Organiser notes",
]
writer.writerow(header)
for count, entry in enumerate(entries, start=1):
entry_line = 1
team_name = entry.get_team_name()
for row in entry.evententryplayer_set.order_by("-player").all():
# Handle overriding the TBA details
if row.player.id == TBA_PLAYER and row.override_tba_name:
player_name = row.override_tba_name
player_no = row.override_tba_system_number
player_email = ""
else:
player_name = row.player.full_name
player_no = row.player.system_number
player_email = row.player.email
data_row = [
count,
player_name.upper(),
player_no,
row.player.mobile,
player_email,
team_name,
]
if entry_line == 1:
data_row.append(entry.category)
data_row.append(entry.free_format_answer)
data_row.append(entry.comment)
data_row.append(entry.notes)
else:
data_row.append("")
data_row.append("")
data_row.append("")
data_row.append("")
writer.writerow(data_row)
entry_line += 1
# add extra blank rows for teams if needed
if event.player_format == "Teams":
for _ in range(7 - entry_line):
writer.writerow([count, "", "", "", "", team_name])
[docs]
@login_required()
def admin_event_offsystem(request, event_id):
"""Handle off system payments such as cheques and bank transfers"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get players with manual payment methods
players = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(
payment_type__in=[
"my-system-dollars",
"their-system-dollars",
"other-system-dollars",
"off-system-pp",
]
)
.exclude(event_entry__entry_status="Cancelled")
)
return render(
request,
"events/congress_admin/event_offsystem.html",
{"event": event, "players": players},
)
[docs]
@login_required()
def admin_event_offsystem_pp(request, event_id):
"""Handle Club PP system to allow clubs to use their existing PP system as a payment method"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get players with pps
players = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.filter(payment_type="off-system-pp")
.exclude(event_entry__entry_status="Cancelled")
)
return render(
request,
"events/congress_admin/event_offsystem_pp.html",
{"event": event, "players": players},
)
[docs]
@login_required()
def admin_event_unpaid(request, event_id):
"""Unpaid Report"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get players with unpaid entries
players = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(event_entry__entry_status="Cancelled")
)
return render(
request,
"events/congress_admin/event_unpaid.html",
{"event": event, "players": players},
)
[docs]
@login_required()
def admin_players_report(request, event_id):
"""Unpaid Report"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get players with unpaid entries
players = EventEntryPlayer.objects.filter(event_entry__event=event).exclude(
event_entry__entry_status="Cancelled"
)
for player in players:
player.masterpoint, player.status = get_player_mp_stats(player.player)
return render(
request,
"events/congress_admin/players_report.html",
{"event": event, "players": players},
)
[docs]
def get_player_mp_stats(player):
"""
Get summary data
"""
qry = "%s/mps/%s" % (
GLOBAL_MPSERVER,
player.system_number,
)
try:
r = requests.get(qry, timeout=5).json()
except Exception as exc:
print(exc)
r = []
if len(r) == 0:
return "Unknown ABF no", "Unknown ABF no"
is_active = r[0]["IsActive"]
if is_active == "Y":
is_active = "Active"
else:
is_active = "Inactive"
return r[0]["TotalMPs"], is_active
[docs]
@login_required()
def admin_event_log(request, event_id):
"""Show logs for an event"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
logs = EventLog.objects.filter(event=event).order_by("-action_date")
things = cobalt_paginator(request, logs)
return render(
request,
"events/congress_admin/event_log.html",
{"event": event, "things": things},
)
[docs]
@login_required()
def admin_evententry_delete(request, evententry_id):
"""Delete an event entry"""
event_entry = get_object_or_404(EventEntry, pk=evententry_id)
# check access
role = "events.org.%s.edit" % event_entry.event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if event_entry.entry_status == EventEntry.EntryStatus.CANCELLED:
return HttpResponse("Event is already cancelled")
event_entry_players = EventEntryPlayer.objects.filter(event_entry=event_entry)
# We use a formset factory to have multiple players on the same form
RefundFormSet = formset_factory(RefundForm, extra=0)
if request.method == "POST":
refund_form_set = RefundFormSet(data=request.POST)
if refund_form_set.is_valid():
for form in refund_form_set:
player = get_object_or_404(User, pk=form.cleaned_data["player_id"])
# Check for TBA - if we have a TBA user who has been paid for then
# a refund is due. Give it to the person who made the entry.
if player.id == TBA_PLAYER:
player = event_entry.primary_entrant
# We don't want to email TBA though
is_tba = True
else:
is_tba = False
amount = float(form.cleaned_data["refund"])
amount_str = "%.2f credits" % amount
if amount > 0.0:
# create payments in org account
update_organisation(
organisation=event_entry.event.congress.congress_master.org,
amount=-amount,
description=f"Refund to {player} for {event_entry.event.event_name}",
payment_type="Refund",
member=player,
event=event_entry.event,
)
# create payment for member
update_account(
organisation=event_entry.event.congress.congress_master.org,
amount=amount,
description=f"Refund from {event_entry.event.congress.congress_master.org} for {event_entry.event.event_name}",
payment_type="Refund",
member=player,
)
# update payment records
for event_entry_player in event_entry_players:
event_entry_player.payment_received = Decimal(0)
event_entry_player.save()
# Log it
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Refund of {amount_str} to {player}",
event_entry=event_entry,
).save()
messages.success(
request,
f"Refund of {amount_str} to {player} successful",
extra_tags="cobalt-message-success",
)
# Notify member
email_body = f"""{request.user.full_name} has cancelled your entry to
<b>{event_entry.event.event_name}</b> in
<b>{event_entry.event.congress.name}.</b><br><br>
"""
if amount > 0.0:
email_body += f"""A refund of {amount:.2f} credits
has been transferred to your {BRIDGE_CREDITS}
account.<br><br>
"""
email_body += f"Please contact {request.user.first_name} directly if you have any queries.<br><br>"
# send
if not is_tba:
# 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"Event Entry Cancelled - {event_entry.event}",
batch_size=1,
complete=True,
)
contact_member(
member=player,
msg="Entry to %s cancelled" % event_entry.event.event_name,
contact_type="Email",
html_msg=email_body,
link="/events/view",
subject="Event Entry Cancelled - %s" % event_entry.event,
batch_id=batch_id,
)
# Log it
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Entry deleted for {event_entry.primary_entrant}",
event_entry=event_entry,
).save()
event_entry.entry_status = EventEntry.EntryStatus.CANCELLED
event_entry.save()
# Also remove from basket if still present
BasketItem.objects.filter(event_entry=event_entry).delete()
messages.success(
request, "Entry cancelled", extra_tags="cobalt-message-success"
)
return redirect("events:admin_event_summary", event_id=event_entry.event.id)
else: # not POST - build summary
event_entry.received = Decimal(0)
initial = []
# we need to default refund per player to who actually paid it, not
# who was entered. If Fred paid for Bill's entry then Fred should get
# the refund not Bill
refund_dict = {}
for event_entry_player in event_entry_players:
refund_dict[event_entry_player.player] = Decimal(0)
for event_entry_player in event_entry_players:
# check if we have a player who paid
if event_entry_player.paid_by:
# maybe this player is no longer part of the team
if event_entry_player.paid_by not in refund_dict.keys():
refund_dict[event_entry_player.paid_by] = Decimal(0)
refund_dict[
event_entry_player.paid_by
] += event_entry_player.payment_received
# Not sure who paid so default it back to the entry name
else:
try:
refund_dict[
event_entry_player.player
] += event_entry_player.payment_received
except TypeError:
pass
for player_refund in refund_dict.keys():
event_entry.received += refund_dict[player_refund]
initial.append(
{
"player_id": player_refund.id,
"player": f"{player_refund}",
"refund": refund_dict[player_refund],
}
)
refund_form_set = RefundFormSet(initial=initial)
club = event_entry.event.congress.congress_master.org
club_balance = org_balance(club)
return render(
request,
"events/congress_admin/event_entry_delete.html",
{
"event_entry": event_entry,
"event_entry_players": event_entry_players,
"refund_form_set": refund_form_set,
"club": club,
"club_balance": club_balance,
},
)
[docs]
@login_required()
def admin_event_player_discount(request, event_id):
"""Manage discounted entry to events"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
event_player_discounts = EventPlayerDiscount.objects.filter(event=event)
if request.method == "POST":
form = EventPlayerDiscountForm(request.POST)
if form.is_valid():
player = form.cleaned_data["player"]
reason = form.cleaned_data["reason"]
already = EventPlayerDiscount.objects.filter(
event=event, player=player
).count()
if already:
messages.error(
request,
f"There is already a discount for {player}",
extra_tags="cobalt-message-error",
)
entered = (
EventEntryPlayer.objects.filter(event_entry__event=event, player=player)
.exclude(event_entry__entry_status="Cancelled")
.exists()
)
if entered:
messages.error(
request,
f"{player} has already entered this event. Update the entry to change the entry fee.",
extra_tags="cobalt-message-error",
)
else:
entry_fee = form.cleaned_data["entry_fee"]
event_player_discount = EventPlayerDiscount()
event_player_discount.player = player
event_player_discount.admin = request.user
event_player_discount.event = event
event_player_discount.reason = reason
event_player_discount.entry_fee = entry_fee
event_player_discount.save()
# Log it
EventLog(
event=event,
actor=request.user,
action=f"Added discount of {entry_fee} for {player} in {event}",
).save()
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=f"Added discount of {entry_fee} for {player.href} in {event.href}",
)
messages.success(
request, "Entry added", extra_tags="cobalt-message-success"
)
# check if player is entered
for event_player_discount in event_player_discounts:
if event_player_discount.event.already_entered(event_player_discount.player):
event_player_discount.status = "Entered - "
# check status of entry
event_entry_player = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.filter(player=event_player_discount.player)
.exclude(event_entry__entry_status="Cancelled")
.first()
)
event_player_discount.status += "%s" % event_entry_player.payment_status
event_player_discount.event_entry_player_id = event_entry_player.id
else:
event_player_discount.status = "Not Entered"
form = EventPlayerDiscountForm()
return render(
request,
"events/congress_admin/event_player_discount.html",
{
"event": event,
"event_player_discounts": event_player_discounts,
"form": form,
},
)
# DEPRECATED - replaced by initiate_admin_event_email
[docs]
@login_required()
def admin_event_email(request, event_id):
"""Email all entrants to an event"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# who will receive this
all_recipients = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(event_entry__entry_status="Cancelled")
.values_list("player__first_name", "player__last_name", "player__email")
.distinct()
)
return _admin_email_common(request, all_recipients, event.congress, event)
# DEPRECATED - replaced by initiate_admin_unpaid_email
[docs]
@login_required()
def admin_unpaid_email(request, event_id):
"""Email all entrants to an event who have not paid"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# who will receive this
# Real people
all_recipients_real = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.exclude(player_id=TBA_PLAYER)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(event_entry__entry_status="Cancelled")
.values_list("player__first_name", "player__last_name", "player__email")
.distinct()
)
# For TBAs get the primary entrant
all_recipients_tba = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.filter(player_id=TBA_PLAYER)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(event_entry__entry_status="Cancelled")
.values_list(
"event_entry__primary_entrant__first_name",
"event_entry__primary_entrant__last_name",
"event_entry__primary_entrant__email",
)
.distinct()
)
all_recipients = list(chain(all_recipients_real, all_recipients_tba))
return _admin_email_common(request, all_recipients, event.congress, event)
# DEPRECATED - replaced by initiate_admin_congress_email
[docs]
@login_required()
def admin_congress_email(request, congress_id):
"""Email all entrants to an entire congress"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# who will receive this
all_recipients = (
EventEntryPlayer.objects.filter(event_entry__event__congress=congress)
.exclude(event_entry__entry_status="Cancelled")
.values_list("player__first_name", "player__last_name", "player__email")
.distinct()
)
return _admin_email_common(request, all_recipients, congress, event=None)
# DEPRECATED - replaced by club menu | comms | edit batch etc
def _admin_email_common_thread(request, congress, subject, body, recipients, batch_id):
"""we run a thread so we can return to the user straight away.
Probably not necessary now we have Django Post Office"""
# For some old congresses, contact_email was not required. Should be able to remove this in the future
reply_to = congress.contact_email or request.user.email
for recipient in recipients:
context = {
"name": recipient[0],
"title1": f"Message from {request.user.full_name} on behalf of {congress}",
"title2": subject,
"email_body": body,
"subject": subject,
}
# COB-793: added batch_size for notification configuration set management
send_cobalt_email_with_template(
to_address=recipient[2],
context=context,
template="system - two headings",
reply_to=reply_to,
batch_id=batch_id,
batch_size=len(recipients),
)
# Django creates a new database connection for this thread so close it
connection.close()
# DEPRECATED - replaced by club menu | comms | edit batch etc
def _admin_email_common(request, all_recipients, congress, event=None):
"""Common function for sending emails to entrants"""
form = EmailForm(request.POST or None)
if request.method == "POST" and form.is_valid():
subject = form.cleaned_data["subject"]
body = form.cleaned_data["body"]
body = bleach.clean(
body,
strip=True,
tags=BLEACH_ALLOWED_TAGS,
attributes=BLEACH_ALLOWED_ATTRIBUTES,
styles=BLEACH_ALLOWED_STYLES,
)
recipients = (
[(request.user.first_name, request.user.last_name, request.user.email)]
if "test" in request.POST
else all_recipients
)
batch_id = create_rbac_batch_id(
f"events.org.{congress.congress_master.org.id}.view",
organisation=congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_EVENT
if event
else BatchID.BATCH_TYPE_CONGRESS,
batch_size=len(recipients),
description=subject,
complete=True,
)
# create a BatchActivity record
batch = BatchID.objects.get(batch_id=batch_id)
activity = BatchActivity()
activity.batch = batch
activity.activity_type = (
BatchActivity.ACTIVITY_TYPE_EVENT
if event
else BatchActivity.ACTIVITY_TYPE_CONGRESS
)
activity.activity_id = event.id if event else congress.id
activity.save()
# start thread
args = {
"request": request,
"congress": congress,
"subject": subject,
"body": body,
"recipients": recipients,
"batch_id": batch_id,
}
thread = Thread(target=_admin_email_common_thread, kwargs=args)
thread.setDaemon(True)
thread.start()
if "test" in request.POST:
messages.success(
request,
f"Test message queued to be sent to {request.user.email}",
extra_tags="cobalt-message-success",
)
else: # Send for real
if len(recipients) == 1:
msg = "Message queued"
else:
msg = "%s messages queued" % (len(recipients))
messages.success(request, msg, extra_tags="cobalt-message-success")
if event:
EventLog(
event=event,
actor=request.user,
action="Sent email to all event entrants",
).save()
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=f"Sent email to all entrants in {event.href}",
)
else:
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=f"Sent email to whole congress {congress.href}",
)
return redirect(
"notifications:watch_emails",
batch_id=batch_id,
)
recipient_count = len(all_recipients)
# Screen will timeout if too many recipients - only really an issue for testing
if len(all_recipients) > 1000:
all_recipients = ["Too many to show"]
return render(
request,
"events/congress_admin/email.html",
{
"form": form,
"congress": congress,
"event": event,
"count": recipient_count,
"recipients": all_recipients,
},
)
[docs]
@login_required()
def initiate_admin_event_email(request, event_id):
"""Start a new email batch to all entrants to an event"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = f"events.org.{event.congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get entrants
candidates = (
EventEntryPlayer.objects.filter(
event_entry__event=event, player__is_active=True
)
.exclude(
event_entry__entry_status="Cancelled",
)
.values_list(
"player__system_number",
"player__first_name",
"player__last_name",
"player__email",
)
.distinct()
)
return _initiate_entrant_batch(
request,
candidates,
None,
event.congress,
event,
)
[docs]
@login_required()
def initiate_admin_unpaid_email(request, event_id):
"""Start a new email batch to entrants of an event who have not paid"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = f"events.org.{event.congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# Real people
candidates_real = (
EventEntryPlayer.objects.filter(
event_entry__event=event,
player__is_active=True,
)
.exclude(player_id=TBA_PLAYER)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(event_entry__entry_status="Cancelled")
.values_list(
"player__system_number",
"player__first_name",
"player__last_name",
"player__email",
)
.distinct()
)
# For TBAs get the primary entrant
candidates_tba = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.filter(player_id=TBA_PLAYER)
.exclude(payment_status="Paid")
.exclude(payment_status="Free")
.exclude(event_entry__entry_status="Cancelled")
.exclude(event_entry__primary_entrant__is_active=False)
.values_list(
"event_entry__primary_entrant__system_number",
"event_entry__primary_entrant__first_name",
"event_entry__primary_entrant__last_name",
"event_entry__primary_entrant__email",
)
.distinct()
)
candidates = list(chain(candidates_real, candidates_tba))
return _initiate_entrant_batch(
request,
candidates,
None,
event.congress,
event,
)
[docs]
@login_required()
def initiate_admin_congress_email(request, congress_id):
"""Start a new email batch to all entrants to an entire congress"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = f"events.org.{congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get entrants
candidates = (
EventEntryPlayer.objects.filter(
event_entry__event__congress=congress,
player__is_active=True,
)
.exclude(event_entry__entry_status="Cancelled")
.values_list(
"player__system_number",
"player__first_name",
"player__last_name",
"player__email",
)
.distinct()
)
return _initiate_entrant_batch(
request,
candidates,
None,
congress,
event=None,
)
@login_required()
def _initiate_entrant_batch(request, candidates, description, congress, event=None):
"""Initiate a new email batch with the specified candidate recipients for
the specified congress or event.
Creates the batch, saves the candidate recipients, then starts the edit batch flow
under the club menu, comms tab.
"""
# create the batch header
batch_id = create_rbac_batch_id(
f"events.org.{congress.congress_master.org.id}.edit",
organisation=congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_EVENT if event else BatchID.BATCH_TYPE_CONGRESS,
batch_size=len(candidates),
description=description,
complete=False,
)
# create a BatchActivity record
batch = BatchID.objects.get(batch_id=batch_id)
activity = BatchActivity()
activity.batch = batch
activity.activity_type = (
BatchActivity.ACTIVITY_TYPE_EVENT
if event
else BatchActivity.ACTIVITY_TYPE_CONGRESS
)
activity.activity_id = event.id if event else congress.id
activity.save()
# save the recipients
for candidate in candidates:
# COB-940 ALL_SYSTEM_ACCOUNTS contains ids not system numbers
# so use ALL_SYSTEM_ACCOUNT_SYSTEM_NUMBERS instead
if candidate[0] not in ALL_SYSTEM_ACCOUNT_SYSTEM_NUMBERS:
try:
recpient = Recipient()
recpient.batch = batch
recpient.system_number = candidate[0]
recpient.first_name = candidate[1]
recpient.last_name = candidate[2]
recpient.email = candidate[3]
recpient.save()
except IntegrityError:
# TBAs may result in duplicates in unpaid batches where the primary entrant is emailed
# instead of the TBA, just ignore it
pass
# go to club menu, comms tab, edit batch
return redirect(
"notifications:compose_email_recipients",
congress.congress_master.org.id,
batch.id,
)
[docs]
@login_required()
def admin_bulletins(request, congress_id):
"""Manage bulletins"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if request.method == "POST":
form = BulletinForm(request.POST, request.FILES)
if form.is_valid():
form.save()
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=f"Added bulletin to {congress.href}",
)
messages.success(
request, "Bulletin uploaded", extra_tags="cobalt-message-success"
)
else:
form = BulletinForm()
# Get bulletins
bulletins = Bulletin.objects.filter(congress=congress).order_by("-pk")
return render(
request,
"events/congress_admin/bulletins.html",
{"form": form, "congress": congress, "bulletins": bulletins},
)
[docs]
@login_required()
def admin_latest_news(request, congress_id):
"""Manage latest news section"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = f"events.org.{congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if request.method == "POST":
form = LatestNewsForm(request.POST)
if form.is_valid():
congress.latest_news = form.cleaned_data["latest_news"]
congress.save()
messages.success(
request, "Latest News Updated", extra_tags="cobalt-message-success"
)
log_event(
user=request.user,
severity="INFO",
source="Events",
sub_source="events_admin",
message=f"Updated latest news for {congress.href}",
)
return redirect("events:view_congress", congress_id=congress.id)
else:
form = LatestNewsForm(initial={"latest_news": congress.latest_news})
return render(
request,
"events/congress_admin/latest_news.html",
{"form": form, "congress": congress},
)
[docs]
@login_required()
@transaction.atomic
def admin_move_entry(request, event_entry_id):
"""Move an entry to another event"""
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
congress = event_entry.event.congress
# check access
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if request.method == "POST":
new_event_id = request.POST.get("new_event_id")
new_event = get_object_or_404(Event, pk=new_event_id)
old_entry = copy.copy(event_entry.event)
event_entry.event = new_event
event_entry.save()
# Log it
EventLog(
event=old_entry,
event_entry=event_entry,
actor=request.user,
action=f"Moved entry to {new_event}",
).save()
EventLog(
event=event_entry.event,
event_entry=event_entry,
actor=request.user,
action=f"Moved entry from {old_entry}",
).save()
# Notify players
# create batch ID
batch_id = create_rbac_batch_id(
rbac_role=f"events.org.{congress.congress_master.org.id}.edit",
organisation=congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_ENTRY,
description="Entry moved to new event",
complete=True,
)
batch_size = 0
for recipient in event_entry.evententryplayer_set.all():
# send
contact_member(
member=recipient.player,
msg="Entry moved to new event",
contact_type="Email",
html_msg=f"{request.user.full_name} has moved your entry to {event_entry.event}.<br><br>",
link="/events/view",
subject="Entry moved to new event",
batch_id=batch_id,
)
batch_size += 1
# update the batch_id
batch_id.batch_size = batch_size
batch_id.state = BatchID.BATCH_STATE_COMPLETE
batch_id.save()
messages.success(request, "Entry Moved", extra_tags="cobalt-message-success")
return redirect("events:admin_event_summary", event_id=event_entry.event.id)
# Events we can move to, must be in this congress and same format
events = (
Event.objects.filter(congress=congress)
.filter(player_format=event_entry.event.player_format)
.exclude(id=event_entry.event.id)
)
return render(
request,
"events/congress_admin/move_entry.html",
{"events": events, "event_entry": event_entry},
)
[docs]
@login_required()
@transaction.atomic
def admin_event_entry_add(request, event_id):
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if request.method == "POST":
# Get the players from the form
players = []
for count in range(6):
player_id = request.POST.get(f"player_{count}", None)
if player_id:
player = get_object_or_404(User, pk=player_id)
players.append(player)
# create entry - make first entrant the primary_entrant
event_entry = EventEntry()
event_entry.event = event
event_entry.primary_entrant = players[0]
event_entry.notes = f"Entry added by convener {request.user.full_name}"
# see if we got a category
category = request.POST.get("category", None)
if category:
event_entry.category = get_object_or_404(Category, pk=category)
event_entry.save()
# Log it
EventLog(
event=event,
actor=request.user,
action=f"Event entry {event_entry.id} ({event_entry.primary_entrant}) created by convener",
event_entry=event_entry,
).save()
# create event entry players
player_index = 0
for player in players:
event_entry_player = EventEntryPlayer()
event_entry_player.event_entry = event_entry
event_entry_player.player = player
event_entry_player.payment_type = "TBA"
entry_fee, discount, reason, description = event.entry_fee_for(
event_entry_player.player
)
if player_index < 4:
event_entry_player.entry_fee = entry_fee
event_entry_player.reason = reason
else:
event_entry_player.entry_fee = 0
event_entry_player.reason = "Team > 4"
event_entry_player.payment_status = "Free"
player_index += 1
event_entry_player.save()
# notify players
# create batch ID
batch_id = create_rbac_batch_id(
rbac_role=f"events.org.{event.congress.congress_master.org.id}.edit",
organisation=event.congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_ENTRY,
description="New convener entry",
complete=True,
)
batch_size = 0
for recipient in players:
# send
contact_member(
member=recipient,
msg="New convener entry",
contact_type="Email",
html_msg=f"{request.user.full_name} has entered you into {event}.<br><br>",
link="/events/view",
subject="New convener entry",
batch_id=batch_id,
)
batch_size += 1
# Log it
EventLog(
event=event,
actor=request.user,
action=f"Player {recipient} added to entry: {event_entry.id} by convener",
event_entry=event_entry,
).save()
# update the batch_id
batch_id.batch_size = batch_size
batch_id.state = BatchID.BATCH_STATE_COMPLETE
batch_id.save()
messages.success(request, "Entry Added", extra_tags="cobalt-message-success")
return redirect("events:admin_evententry", evententry_id=event_entry.id)
player_count_number = EVENT_PLAYER_FORMAT_SIZE[event.player_format]
player_count = range(player_count_number)
categories = Category.objects.filter(event=event)
return render(
request,
"events/congress_admin/event_entry_add.html",
{
"event": event,
"player_count": player_count,
"player_count_number": player_count_number,
"categories": categories,
},
)
[docs]
@login_required()
@transaction.atomic
def admin_event_entry_player_add(request, event_entry_id):
"""Add a player to a team"""
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
# check access
role = "events.org.%s.edit" % event_entry.event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if event_entry.event.player_format != "Teams":
messages.error(
request,
"Not a teams event - cannot add player",
extra_tags="cobalt-message-error",
)
return redirect("events:admin_evententry", evententry_id=event_entry.id)
tba = User.objects.get(pk=TBA_PLAYER)
event_entry_player = EventEntryPlayer()
event_entry_player.event_entry = event_entry
event_entry_player.player = tba
# event_entry_player.payment_type = "TBA"
event_entry_player.payment_type = "other-system-dollars"
event_entry_player.entry_fee = 0.0
event_entry_player.save()
# Log it
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Player added to {event_entry.id} ({event_entry.primary_entrant}) created by convener",
event_entry=event_entry,
).save()
messages.success(request, "Player Added", extra_tags="cobalt-message-success")
return redirect(
"events:admin_evententryplayer", evententryplayer_id=event_entry_player.id
)
[docs]
@login_required()
@transaction.atomic
def admin_event_entry_player_delete(request, event_entry_player_id):
"""Delete a player from a team"""
event_entry_player = get_object_or_404(EventEntryPlayer, pk=event_entry_player_id)
event_entry = event_entry_player.event_entry
event_entry_player_count = EventEntryPlayer.objects.filter(
event_entry=event_entry
).count()
# check access
role = "events.org.%s.edit" % event_entry.event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if event_entry.event.player_format != "Teams":
messages.error(
request,
"Not a teams event - cannot delete player",
extra_tags="cobalt-message-error",
)
return redirect("events:admin_evententry", evententry_id=event_entry.id)
if request.method == "POST":
# Log it
EventLog(
event=event_entry.event,
actor=request.user,
action=f"Player {event_entry_player.player} deleted from {event_entry.id} ({event_entry.primary_entrant}) by convener",
event_entry=event_entry,
).save()
# Previously - delete if payment_type is free (5th or 6th player) otherwise change to TBA
# COB-569: Deletes if zero received and zero fee
if (
event_entry_player.payment_received == 0
and event_entry_player.entry_fee == 0
and event_entry_player_count > 4
):
event_entry_player.delete()
messages.success(
request, "Player Deleted", extra_tags="cobalt-message-success"
)
else:
# 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="Removed from team by Convener",
batch_size=1,
complete=True,
)
contact_member(
member=event_entry_player.player,
msg="Convener removed you from team",
contact_type="Email",
html_msg=f"{request.user.full_name} has removed you from a team entry to {event_entry.event}.<br><br>",
link="/events/view",
subject="Removed from team by Convener",
batch_id=batch_id,
)
tba = User.objects.get(pk=TBA_PLAYER)
event_entry_player.player = tba
event_entry_player.save()
if event_entry_player_count == 4:
messages.success(
request,
"Player changed player to TBA. The entry must include at least four players.",
extra_tags="cobalt-message-success",
)
else:
messages.success(
request,
"Player changed player to TBA. To delete the player the entry fee and payments recieved must be zero",
extra_tags="cobalt-message-success",
)
return redirect("events:admin_evententry", evententry_id=event_entry.id)
return render(
request,
"events/congress_admin/event_entry_player_delete.html",
{"event_entry_player": event_entry_player},
)
[docs]
@login_required()
@transaction.atomic
def admin_event_offsystem_pp_batch(request, event_id):
"""Handle Club PP system to allow clubs to use their existing PP system
as a payment method. This handles batch work so upload a spreadsheet
to the external PP system"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# get players with pps
event_entry_players = (
EventEntryPlayer.objects.filter(event_entry__event=event)
.filter(payment_type="off-system-pp")
.exclude(event_entry__entry_status="Cancelled")
.exclude(payment_status="Paid")
)
# calculate outstanding
for event_entry_player in event_entry_players:
event_entry_player.outstanding = (
event_entry_player.entry_fee - event_entry_player.payment_received
)
event_entry_players_list = []
for event_entry_player in event_entry_players:
event_entry_players_list.append(
(event_entry_player.id, event_entry_player.player.full_name)
)
if request.method == "POST":
form = OffSystemPPForm(
request.POST, event_entry_players=event_entry_players_list
)
if form.is_valid():
# event_entry_players_list is the full list, event-entry_players_ids is
# those that are selected
event_entry_player_ids = form.cleaned_data["event_entry_players_list"]
event_entry_players = EventEntryPlayer.objects.filter(
id__in=event_entry_player_ids
)
if "export" in request.POST: # CSV download
local_dt = timezone.localtime(timezone.now(), TZ)
today = dateformat.format(local_dt, "Y-m-d H:i:s")
response = HttpResponse(content_type="text/csv")
response[
"Content-Disposition"
] = 'attachment; filename="my-abf-to-pp.csv"'
writer = csv.writer(response)
writer.writerow(
[
"My ABF to PP Export",
"Downloaded by %s" % request.user.full_name,
today,
]
)
writer.writerow(
[
f"{GLOBAL_ORG} No.",
"Name",
"Amount",
]
)
for event_entry_player in event_entry_players:
writer.writerow(
[
event_entry_player.player.system_number,
event_entry_player.player.full_name,
event_entry_player.entry_fee
- event_entry_player.payment_received,
]
)
return response
else: # confirm payments
for event_entry_player in event_entry_players:
outstanding = (
event_entry_player.entry_fee
- event_entry_player.payment_received
)
event_entry_player.payment_status = "Paid"
event_entry_player.payment_received = event_entry_player.entry_fee
event_entry_player.save()
event_entry_player.event_entry.check_if_paid()
# Delete from players basket if present
BasketItem.objects.filter(
event_entry=event_entry_player.event_entry
).delete()
EventLog(
event=event,
event_entry=event_entry_player.event_entry,
actor=request.user,
action=f"{event_entry_player.player} paid {outstanding} through off system PP",
).save()
length = event_entry_players.count()
messages.success(
request,
f"Off System PP payments processed successfully. {length} players updated",
extra_tags="cobalt-message-success",
)
return redirect(
"events:admin_event_summary",
event_id=event.id,
)
else:
form = OffSystemPPForm(event_entry_players=event_entry_players_list)
return render(
request,
"events/congress_admin/event_offsystem_pp_batch.html",
{"event_entry_players": event_entry_players, "form": form, "event": event},
)
[docs]
@login_required()
def player_events_list(request, member_id, congress_id):
"""List what events a player has entered"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = "events.org.%s.edit" % congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
player = get_object_or_404(User, pk=member_id)
event_entry_players = EventEntryPlayer.objects.filter(
event_entry__event__congress=congress
).filter(player=player)
# Add partners onto the list
for event_entry_player in event_entry_players:
partners = EventEntryPlayer.objects.filter(
event_entry=event_entry_player.event_entry
)
# Put the player first in the list
txt = f"{event_entry_player.player.full_name}, "
for partner in partners:
if partner != event_entry_player:
txt += f"{partner.player.full_name}, "
# remove trailing comma and space
event_entry_player.partners = txt[:-2]
# Get log events
events_entries_list = event_entry_players.values("event_entry")
event_logs = EventLog.objects.filter(event_entry__in=events_entries_list)
return render(
request,
"events/congress_admin/player_events_list.html",
{
"player": player,
"congress": congress,
"event_entry_players": event_entry_players,
"event_logs": event_logs,
},
)
[docs]
@login_required()
def admin_event_payment_methods(request, event_id):
"""List of payment methods including Cancelled entries"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
event_entry_players = EventEntryPlayer.objects.filter(event_entry__event=event)
# Get log events
event_logs = EventLog.objects.filter(event=event)
return render(
request,
"events/congress_admin/event_payment_methods.html",
{
"event": event,
"event_entry_players": event_entry_players,
"event_logs": event_logs,
},
)
[docs]
@login_required()
def admin_event_payment_methods_csv(request, event_id):
"""List of payment methods including Cancelled entries as csv"""
event = get_object_or_404(Event, pk=event_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
event_entry_players = EventEntryPlayer.objects.filter(event_entry__event=event)
# This function is used with the Django Admin and requires self as a parameter, although it doesn't get used
return download_csv(None, request, event_entry_players)
[docs]
@login_required()
def admin_event_entry_change_category_htmx(request):
"""Allow admins to change categories for entries in an event that has categories defined"""
event_id = request.POST.get("event_id")
event = get_object_or_404(Event, pk=event_id)
event_entry_id = request.POST.get("event_entry_id")
event_entry = get_object_or_404(EventEntry, pk=event_entry_id)
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if "update" in request.POST:
new_category_id = request.POST.get("category")
new_category = get_object_or_404(Category, pk=new_category_id)
event_entry.category = new_category
event_entry.save()
EventLog(
event=event,
event_entry=event_entry,
actor=request.user,
action=f"Administrator changed category to {new_category}",
).save()
return render(
request,
"events/congress_admin/admin_event_entry_category_htmx.html",
{"event": event, "event_entry": event_entry},
)
categories = Category.objects.filter(event=event)
return render(
request,
"events/congress_admin/admin_event_entry_change_category_htmx.html",
{"event": event, "event_entry": event_entry, "categories": categories},
)
[docs]
@login_required()
def convener_settings(request, congress_id):
"""Allow conveners to manage their personal settings for congresses"""
congress = get_object_or_404(Congress, pk=congress_id)
# check access
role = f"events.org.{congress.congress_master.org.id}.edit"
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
# We change settings through a GET so see if we need to do anything
if request.method == "GET":
# Everything
if request.GET.get("all_off") == "True":
# Turn off all and delete anything else
BlockNotification.objects.filter(member=request.user).delete()
BlockNotification(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
model_id=None,
).save()
if request.GET.get("all_off") == "False":
# Turn on all
BlockNotification.objects.filter(
member=request.user,
).delete()
# This org
if request.GET.get("this_org_off") == "True":
# Turn off org and delete anything else
BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
).delete()
BlockNotification(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_ORG,
model_id=congress.congress_master.org.id,
).save()
if request.GET.get("this_org_off") == "False":
# Turn on org
BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_ORG,
model_id=congress.congress_master.org.id,
).delete()
if request.GET.get("this_congress_off") == "True":
# Turn off congress and delete anything else
# BlockNotification.objects.filter(member=request.user).delete()
BlockNotification(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
model_id=congress.congress_master.id,
).save()
if request.GET.get("this_congress_off") == "False":
# Turn on congress
BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
model_id=congress.congress_master.id,
).delete()
# Build current state for view
this_congress_off = BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
model_id=congress.congress_master.id,
).exists()
this_org_off = BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_ORG,
model_id=congress.congress_master.org.id,
).exists()
all_off = BlockNotification.objects.filter(
member=request.user,
identifier=BlockNotification.Identifier.CONVENER_EMAIL_BY_EVENT,
model_id=None,
).exists()
congress_contact_email = congress.contact_email
is_contact = congress_contact_email == request.user.email
return render(
request,
"events/congress_admin/convener_settings.html",
{
"congress": congress,
"congress_contact_email": congress_contact_email,
"is_contact": is_contact,
"this_congress_off": this_congress_off,
"this_org_off": this_org_off,
"all_off": all_off,
},
)
[docs]
@login_required()
def edit_team_name_htmx(request):
"""HTMX snippet to edit the team name"""
event_entry = get_object_or_404(EventEntry, pk=request.POST.get("event_entry_id"))
# check access
role = "events.org.%s.edit" % event_entry.event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
if "cancel" in request.POST:
return render(
request,
"events/congress_admin/event_entry_team_name_htmx.html",
{"event_entry": event_entry},
)
if "save" in request.POST:
team_name = request.POST.get("team_name")
event_entry.team_name = team_name
event_entry.save()
EventLog(
event=event_entry.event,
event_entry=event_entry,
actor=request.user,
action=f"Admin changed team name to '{team_name}'",
).save()
return render(
request,
"events/congress_admin/event_entry_team_name_htmx.html",
{"event_entry": event_entry},
)
return render(
request,
"events/congress_admin/event_entry_team_name_edit_htmx.html",
{"event_entry": event_entry},
)
[docs]
@login_required()
def edit_player_name_htmx(request):
"""HTMX snippet to edit the player name on an EventEntryPlayer"""
event_entry_player = get_object_or_404(
EventEntryPlayer, pk=request.POST.get("event_entry_player_id")
)
event = event_entry_player.event_entry.event
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return JsonResponse({"status": "Failure", "message": "Access Denied"})
old_user = copy.copy(event_entry_player.player)
new_user = get_object_or_404(User, pk=request.POST.get("player_id"))
event_entry_player.player = new_user
# replace any TBA data
event_entry_player.override_tba_name = None
event_entry_player.override_tba_system_number = 0
event_entry_player.save()
message = "User changed"
# create batch ID if an email is going to be sent
if old_user.id != TBA_PLAYER or new_user.id != TBA_PLAYER:
batch_id = create_rbac_batch_id(
rbac_role=f"events.org.{event.congress.congress_master.org.id}.edit",
organisation=event.congress.congress_master.org,
batch_type=BatchID.BATCH_TYPE_ENTRY,
description=f"Edited player name in {event}",
complete=True,
)
batch_size = 0
else:
batch_id = None
# notify deleted member
if old_user.id != TBA_PLAYER:
# send
contact_member(
member=old_user,
msg=f"Removed from - {event}",
contact_type="Email",
html_msg=f"The convener, {request.user.full_name}, has removed you from this event.<br><br>",
link="/events/view",
subject=f"Removed from - {event}",
batch_id=batch_id,
)
batch_size += 1
# notify added member
if new_user.id != TBA_PLAYER:
# send
contact_member(
member=new_user,
msg="Added to - %s" % event,
contact_type="Email",
html_msg=f"The convener, {request.user.full_name}, has added you to this event.<br><br>",
link="/events/view",
subject="Added to - %s" % event,
batch_id=batch_id,
)
batch_size += 1
if batch_id:
# update the batch_id
batch_id.batch_size = batch_size
batch_id.state = BatchID.BATCH_STATE_COMPLETE
batch_id.save()
EventLog(
event=event,
event_entry=event_entry_player.event_entry,
actor=request.user,
action=f"Convener Action: Changed player from {old_user} to {new_user} on Entry:{event_entry_player.event_entry.href}",
).save()
# We used to return an HTMX/HTML block, but it is cleaner to reload the page so just return Json
return JsonResponse({"status": "Success", "message": message})
[docs]
@login_required()
def edit_tba_player_details_htmx(request):
"""Override the name and ABF number of a TBA player"""
event_entry_player = get_object_or_404(
EventEntryPlayer, pk=request.POST.get("event_entry_player_id")
)
event = event_entry_player.event_entry.event
# check access
role = "events.org.%s.edit" % event.congress.congress_master.org.id
if not rbac_user_has_role(request.user, role):
return rbac_forbidden(request, role)
tba_form = EventEntryPlayerTBAForm(request.POST, instance=event_entry_player)
message = ""
if tba_form.is_valid():
tba_form.save()
message = "Data saved"
return render(
request,
"events/congress_admin/event_entry_player_name_htmx.html",
{
"event_entry_player": event_entry_player,
"tba_form": tba_form,
"message": message,
},
)
[docs]
@login_required()
def congress_finished_with_overdue_payments_htmx(request, message=""):
"""
When a congress is complete, we shouldn't have any overdue payments. If we do, then there is
a problem. It could be because the convener doesn't really care (has no impact on them), or
it could be because they are still chasing up money.
Why is this a problem? Mainly for Bridge Credits - the player will be asked to make the payment
but it is likely that the convener has also sorted this out and doesn't expect them to pay.
After a congress is finished, if there are overdue payments then we email the convener. We also
add a dialog box to the congress admin summary page (this function generates that).
A convener can:
1) Hit the "fix it" button, and we mark everything as done.
2) Do nothing and after a period of time we fix it automatically.
3) Press the "don't fix" button, so we don't automatically close the congress (we will after 3 months).
"""
congress_id = request.POST.get("congress_id")
congress = get_object_or_404(Congress, pk=congress_id)
# See if this congress is on the naughty list - finished but not closed off
bad_congresses = get_completed_congresses_with_money_due(congress)
# We may not have a date we are paused to, but calculate it in case the template needs it
if bad_congresses:
pause_date = congress.end_date + relativedelta(months=+3)
else:
pause_date = None
return render(
request,
"events/congress_admin/congress_finished_with_overdue_payments.html",
{
"congress": congress,
"bad_congresses": bad_congresses,
"message": message,
"pause_date": pause_date,
},
)
@check_convener_access()
def do_not_automatically_fix_closed_congress_htmx(request, congress):
"""Mark congress so that it doesn't get automatically closed out (entries marked as paid)"""
congress.do_not_auto_close_congress = True
congress.save()
return congress_finished_with_overdue_payments_htmx(request)
@check_convener_access()
def do_automatically_fix_closed_congress_htmx(request, congress):
"""Mark congress so that it does get automatically closed out (entries marked as paid)"""
congress.do_not_auto_close_congress = False
congress.save()
return congress_finished_with_overdue_payments_htmx(request)
@check_convener_access()
def fix_closed_congress_htmx(request, congress):
"""Fix a congress that is close with unpaid entries"""
message = fix_closed_congress(congress, request.user)
return admin_summary(request, congress_id=congress.id, message=message)