diff --git a/Readme.md b/Readme.md index 7324625..9c1fff1 100644 --- a/Readme.md +++ b/Readme.md @@ -73,6 +73,22 @@ Install Weasyprint https://doc.courtbouillon.org/weasyprint/stable/first_steps.h - Linux: `python3 compy.py` - Windows: `python3.exe compy.py` - Navigate your browser to `localhost:5000` + - The admin interface is at `localhost:5000/admin?auth=XXXXXX` where `XXXXXX` are the first 6 + characters of your `FLASK_SECRET_KEY` from `.env` + +## Test data + +A generator for a realistic sample competition (30 athletes, 3 days: CWT, STA, DYN, +results and cards for past days, DNS entries, open results for today) lives in +`tools/generate_test_data.py`: + + - `python3 tools/generate_test_data.py test_competition.xlsx` + - Start the server and open the admin page + - Set a competition name, save, choose the generated file and press "Refresh data" + +Alternatively a pre-seeded `compy.sqlite` with the competition "Compy Test Open 2026" +(including `test_competition.xlsx`) may already be present, in which case you can simply +load it from "Load competition" on the Settings tab. ## Want to help? diff --git a/compy_constants.py b/compy_constants.py new file mode 100644 index 0000000..0609a77 --- /dev/null +++ b/compy_constants.py @@ -0,0 +1,34 @@ + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# ━━━━━━━━━━━━━ +# ┏┓┏┓┳┳┓┏┓┓┏ +# ┃ ┃┃┃┃┃┃┃┗┫ +# ┗┛┗┛┛ ┗┣┛┗┛ +# ━━━━━━━━━━━━━ +# +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# Competition organization tool +# for freediving competitions. +# +# Copyright 2023 - Arno Mayrhofer +# +# Licensed under the GNU AGPL +# +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# Authors: +# +# - Arno Mayrhofer +# +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +"""Shared domain constants for Compy.""" + +INVALID_DATE = "0000-00-00" +INVALID_TIME = "99:99" +POOL_DISCIPLINES = ["STA", "DNF", "DYN", "DYNB"] +DEPTH_DISCIPLINES = ["FIM", "CNF", "CWT", "CWTB"] +DISCIPLINES = DEPTH_DISCIPLINES + POOL_DISCIPLINES +FEDERATIONS = ["aida", "cmas"] diff --git a/compy_data.py b/compy_data.py index d99a33a..4ccfbbc 100644 --- a/compy_data.py +++ b/compy_data.py @@ -39,37 +39,24 @@ import math import random import os -import json -import glob -from datetime import datetime, timedelta, time +import sqlite3 +from datetime import datetime, timedelta, time, timezone from io import BytesIO -import flask -try: - import weasyprint as wp -except ImportError: - print("Could not find weasyprint. Install with 'pip3 install weasyprint'") - exit(-1) import base64 from PIL import Image -from io import BytesIO -import numpy as np import regex import sys import athlete from compy_config import CompyConfig +from compy_constants import (INVALID_DATE, INVALID_TIME, POOL_DISCIPLINES, + DEPTH_DISCIPLINES, DISCIPLINES, FEDERATIONS) +from compy_pdf import PdfReportMixin import compy_utilities as u from openpyxl import load_workbook -INVALID_DATE="0000-00-00" -INVALID_TIME="99:99" -POOL_DISCIPLINES = ["STA", "DNF", "DYN", "DYNB"] -DEPTH_DISCIPLINES=["FIM", "CNF", "CWT", "CWTB"] -DISCIPLINES = DEPTH_DISCIPLINES + POOL_DISCIPLINES -FEDERATIONS=["aida", "cmas"] - -class CompyData: +class CompyData(PdfReportMixin): version_ = None @@ -94,7 +81,7 @@ def __init__(self, db, app, comp_id = -1): self.name_ = "undefined" with self.app_.app_context(): - #self.updateNationalRecords(); + #self.updateNationalRecords() # try and find it first if comp_id == -1: c_id = self.db_.execute("SELECT id FROM competition WHERE name=?", self.name_) @@ -107,7 +94,6 @@ def __init__(self, db, app, comp_id = -1): self.load(comp_id) @property - @staticmethod def version(self): if self.version_ is None: base_path = os.path.dirname(os.path.realpath(__file__)) @@ -304,14 +290,14 @@ def refresh(self): # read second sheet (list of athletes) df = pd.read_excel(self.comp_file_, sheet_name="Athletes and Judges", skiprows=1) for i,r in df.iterrows(): - a = athlete.Athlete.fromArgs(r['Id'], r['FirstName'], r['LastName'], r['Gender'], r['Country'], r['Club'] if 'club' in r else "", self.db_) + a = athlete.Athlete.fromArgs(r['Id'], r['FirstName'], r['LastName'], r['Gender'], r['Country'], r['Club'] if 'Club' in r else "", self.db_) logging.debug("Athlete: %s %s %s %s %s", r['Id'], r['FirstName'], r['LastName'], r['Gender'], r['Country']) a.associateWithComp(self.id_) logging.debug("Number of athletes: %d", self.number_of_athletes) if sr_ids is not None: for sr in sr_ids: - self.setRegistration(sr[0], True, False, "specialranking") + self.setRegistration(sr[0], True, "specialranking", warn=False) self.save() @@ -351,12 +337,11 @@ def refresh(self): card = "nan" penalty = "nan" block = blocks[dis] - print(block, dis, blocks) if rp is not None: self.db_.execute('''INSERT INTO start (competition_athlete_id, discipline, lane, OT, AP, rp, card, penalty, remarks, block) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (ca_id[0][0], dis, lane, u.convTime(ot), ap, rp, card, penalty, remarks, block)) else: self.db_.execute('''INSERT INTO start @@ -385,7 +370,6 @@ def setRegistration(self, athlete_id, is_checked, change_type, warn=True): return 1 def getSavedCompetitions(self): - os.chdir(self.config.storage_folder) saved_comp_info = [] comps = self.db_.execute("SELECT id, name, save_date FROM competition") if comps is None: @@ -443,7 +427,7 @@ def load(self, comp_id): FROM competition WHERE id=?''', comp_id) if load_data is None: - logging.error("Could not find competition with id '" + id + "'") + logging.error("Could not find competition with id '" + str(comp_id) + "'") self.id_ = None return None else: @@ -651,7 +635,7 @@ def updateStartList(self, day, block, to_remove, startlist): WHERE s.id == ? AND ca.competition_id == ?''', (ca_id, self.id_)) if ca_id is None: - log.warning("Invalid athlete not added to competition") + logging.warning("Invalid athlete not added to competition") continue ot = self.cleanTime(startlist[i]["OT"]) dive_time = self.getMinFromTime(self.cleanTime(startlist[i]["Dive Time"])) if "Dive Time" in startlist[i].keys() else 0 @@ -669,13 +653,13 @@ def updateStartList(self, day, block, to_remove, startlist): '''INSERT INTO start (competition_athlete_id, discipline, block, lane, OT, AP, PB, dive_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', - (ca_id[0][0], discipline, block, lane, u.convTime(ot), ap, pb, dive_time)); + (ca_id[0][0], discipline, block, lane, u.convTime(ot), ap, pb, dive_time)) else: #update start self.db_.execute( '''UPDATE start SET discipline=?, block=?, lane=?, OT=?, AP=?, PB=?, dive_time=? WHERE id == ?''', - (discipline, block, lane, u.convTime(ot), ap, pb, dive_time, ca_id[0][0])); + (discipline, block, lane, u.convTime(ot), ap, pb, dive_time, ca_id[0][0])) return 0 def convertPerformance(self, val, dis): @@ -695,107 +679,6 @@ def getWTfromOT(self, ot): wt = str(math.floor(wtf/60)) + ":" + str(wtf%60).zfill(2) return wt - def getStartListPDF(self, day="all", block="all", in_memory=False): - if day=="all" and block=="all": - day_block = self.getBlocks() - files = [] - for d in day_block: - for block in day_block[d].keys(): - files.append(self.getStartListPDF(d, block, True)) - pages = [] - for doc in files: - for page in doc.pages: - pages.append(page) - merged_pdf = files[0].copy(pages) - fname = os.path.join(self.config.download_folder, self.name + "_start_lists.pdf") - merged_pdf.write_pdf(fname) - return fname - start_df = pd.DataFrame(self.getStartList(day, block)) - start_df.drop("Id", axis=1, inplace=True) - blocks = self.getBlocks() - block_disciplines = blocks[day][int(block)]['dis_s'] - if self.comp_type == "aida": - start_df.drop("PB", axis=1, inplace=True) - elif block_disciplines == "STA": - start_df["AP"] = start_df.apply(lambda row: row["AP"] if row['Discipline'] == "STA" else "", axis=1) - self.dropUnusedColumns(block_disciplines, start_df) - html_string = start_df.to_html(index=False, justify="left", classes="df_table") - day_obj = datetime.strptime(day, "%Y-%m-%d") - human_day = day_obj.strftime("%d. %m. %Y") - dis = self.db_.execute('SELECT disciplines FROM block WHERE competition_id==? AND id==?', (self.id_, block)) - if dis is None: - return None - disciplines = self.disciplineIntToStr(dis[0][0]) - html_string = """ - -
- - - -