diff --git a/languages/ru_RU/LC_MESSAGES/sportorg.po b/languages/ru_RU/LC_MESSAGES/sportorg.po index 1d830def..9c8f82e7 100644 --- a/languages/ru_RU/LC_MESSAGES/sportorg.po +++ b/languages/ru_RU/LC_MESSAGES/sportorg.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: SportOrg\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-12 11:13+0300\n" -"PO-Revision-Date: 2023-08-27 15:09+0500\n" +"PO-Revision-Date: 2023-12-17 19:16+0500\n" "Last-Translator: SportOrg\n" "Language-Team: SportOrg\n" "Language: ru_RU\n" @@ -12,7 +12,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Loco-Source-Locale: ru_RU\n" -"X-Generator: Poedit 3.3.2\n" +"X-Generator: Poedit 3.4.1\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Loco-Parser: loco_parse_po\n" "X-Poedit-Basepath: ../../../sportorg\n" @@ -1568,3 +1568,36 @@ msgstr "Ошибка открытия файла. Файл уже открыт msgid "Multi day race" msgstr "Многодневная гонка" + +msgid "SportOrg HTML" +msgstr "Файл HTML, SportOrg" + +msgid "SportOrg HTML report (*.html)" +msgstr "Файл HTML, SportOrg (*.html)" + +msgid "Open SportOrg HTML file" +msgstr "Открыть файл HTML, SportOrg" + +msgid "SportOrg SI log" +msgstr "Файл лога чипов, SportOrg" + +msgid "SportOrg SI log (*.log)" +msgstr "Файл лога чипов, SportOrg (*.log)" + +msgid "Open SportOrg SI log file" +msgstr "Открыть файл лога чипов, SportOrg" + +msgid "SPORTident master station CSV" +msgstr "Файл CSV мастер-станции SPORTident" + +msgid "CSV file (*.csv)" +msgstr "Файл CSV (*.csv)" + +msgid "Open SPORTident master station backup file" +msgstr "Открыть файл CSV мастер-станции SPORTident" + +msgid "Orgeo.ru CSV" +msgstr "Файл финиша orgeo.ru, CSV" + +msgid "Open orgeo.ru finish CSV file" +msgstr "Открыть файл финиша orgeo.ru, CSV (https://orgeo.ru/event/export?event_id=XXX&sub_id=Y&format=excel_finish)" diff --git a/sportorg/gui/dialogs/file_dialog.py b/sportorg/gui/dialogs/file_dialog.py index 758ea751..907d4b73 100644 --- a/sportorg/gui/dialogs/file_dialog.py +++ b/sportorg/gui/dialogs/file_dialog.py @@ -7,11 +7,11 @@ from sportorg.modules.configs.configs import Config, ConfigFile -def get_open_file_name(caption='', filter_text=''): +def get_open_file_name(caption='', filter_text='', set_dir=True): result = QFileDialog.getOpenFileName(None, caption, get_default_dir(), filter_text)[ 0 ] - if result: + if result and set_dir: set_default_dir(os.path.dirname(os.path.abspath(result))) return result diff --git a/sportorg/gui/menu/actions.py b/sportorg/gui/menu/actions.py index 024fa560..1d51c771 100644 --- a/sportorg/gui/menu/actions.py +++ b/sportorg/gui/menu/actions.py @@ -2,6 +2,7 @@ import socket import time import uuid +from os import remove from typing import Any, Dict, Type from PySide2 import QtCore @@ -59,6 +60,12 @@ from sportorg.modules.live.live import live_client from sportorg.modules.ocad import ocad from sportorg.modules.ocad.ocad import OcadImportException +from sportorg.modules.recovery import ( + recovery_orgeo_finish_csv, + recovery_si_master_csv, + recovery_sportorg_html, + recovery_sportorg_si_log, +) from sportorg.modules.rfid_impinj.rfid_impinj import ImpinjClient from sportorg.modules.sfr.sfrreader import SFRReaderClient from sportorg.modules.sportident.sireader import SIReaderClient @@ -345,6 +352,54 @@ def execute(self): ) +class RecoverySportorgHtmlAction(Action, metaclass=ActionFactory): + def execute(self): + file_name = get_open_file_name( + translate('Open SportOrg HTML file'), + translate('SportOrg HTML report (*.html)'), + False, + ) + tmp_filename = recovery_sportorg_html.recovery(file_name) + with open(tmp_filename) as f: + attr = get_races_from_file(f) + SportOrgImportDialog(*attr).exec_() + remove(tmp_filename) + self.app.refresh() + + +class RecoverySportorgSiLogAction(Action, metaclass=ActionFactory): + def execute(self): + file_name = get_open_file_name( + translate('Open SportOrg SI log file'), + translate('SportOrg SI log (*.log)'), + False, + ) + recovery_sportorg_si_log.recovery(file_name, race()) + self.app.refresh() + + +class RecoverySportidentMasterCsvAction(Action, metaclass=ActionFactory): + def execute(self): + file_name = get_open_file_name( + translate('Open SPORTident master station backup file'), + translate('CSV file (*.csv)'), + False, + ) + recovery_si_master_csv.recovery(file_name, race()) + self.app.refresh() + + +class RecoveryOrgeoFinishCsvAction(Action, metaclass=ActionFactory): + def execute(self): + file_name = get_open_file_name( + translate('Open orgeo.ru finish CSV file'), + translate('CSV file (*.csv)'), + False, + ) + recovery_orgeo_finish_csv.recovery(file_name, race()) + self.app.refresh() + + class AddObjectAction(Action, metaclass=ActionFactory): def execute(self): self.app.add_object() diff --git a/sportorg/gui/menu/menu.py b/sportorg/gui/menu/menu.py index 4243ce4b..f6972126 100644 --- a/sportorg/gui/menu/menu.py +++ b/sportorg/gui/menu/menu.py @@ -72,6 +72,22 @@ def menu_list(): 'title': translate('IOF xml'), 'action': 'IOFEntryListImportAction', }, + { + 'title': translate('SPORTident master station CSV'), + 'action': 'RecoverySportidentMasterCsvAction', + }, + { + 'title': translate('SportOrg SI log'), + 'action': 'RecoverySportorgSiLogAction', + }, + { + 'title': translate('Orgeo.ru CSV'), + 'action': 'RecoveryOrgeoFinishCsvAction', + }, + { + 'title': translate('SportOrg HTML'), + 'action': 'RecoverySportorgHtmlAction', + }, ], }, { diff --git a/sportorg/modules/recovery/recovery_orgeo_finish_csv.py b/sportorg/modules/recovery/recovery_orgeo_finish_csv.py new file mode 100644 index 00000000..ff5f55cb --- /dev/null +++ b/sportorg/modules/recovery/recovery_orgeo_finish_csv.py @@ -0,0 +1,112 @@ +""" +Parse finish CSV from online-service orgeo.ru (2023) + +-- Format: +CSV, separator ";" +SPLITS: [hh:mm:ss|code|]* +П/п;Группа;Фамилия, имя участника;Команда;№;Номер чипа;Место;Результат;Отст.;Время старта;[TV;90cp;]Сплиты; + +-- Example: +1;Ж10;Лимонникова Анна;72_СШ №2 Кобелева;39;8510418;1;00:09:10;+00:00;12:33:00;00:01:41|59|00:00:55|60|00:01:14|61| +2;Ж10;Глухарева Светлана;72_СШ №2 Глухарева;35;2102481;2;00:11:06;+01:56;12:29:00;00:01:30|59|00:00:43|60|00:01:28|61| +18;Ж10;Радченко Милана;72_СШ №2 Кобелева;37;9111137;;не старт;;12:37:00; +33;Ж12;Аристова Надежда;55_Омская обл.;162;8517947;;непр.отмет.;;13:09:00;00:03:00|70| +""" +import csv + +from sportorg.models.memory import ( + Group, + Organization, + Person, + Race, + ResultSportident, + ResultStatus, + Split, +) +from sportorg.utils.time import hhmmss_to_time + +POS_GROUP = 1 +POS_NAME = 2 +POS_TEAM = 3 +POS_BIB = 4 +POS_CARD = 5 +POS_RES = 7 +POS_START = 9 +POS_SPLITS = -1 + +DNS_STATUS = ['DNS', 'не старт'] +DSQ_STATUS = ['DSQ', 'непр.отмет.'] + + +def recovery(file_name: str, race: Race) -> None: + encoding = 'cp1251' + separator = ';' + spl_separator = '|' + + with open(file_name, encoding=encoding) as csv_file: + spam_reader = csv.reader(csv_file, delimiter=separator) + for tokens in spam_reader: + if len(tokens) <= POS_START: + continue + + bib = tokens[POS_BIB] + if bib == '' or not bib.isdigit(): + continue + + name = tokens[POS_NAME] + person = Person() + spl_pos = name.find(' ') + if spl_pos > 0: + person.surname = name[:spl_pos] + person.name = name[spl_pos + 1 :] + else: + person.name = name + person.bib = int(bib) + + team_name = tokens[POS_TEAM] + team = race.find_team(team_name) + if not team: + team = Organization() + team.name = team_name + race.organizations.append(team) + person.organization = team + + group_name = tokens[POS_GROUP] + group = race.find_group(group_name) + if not group: + group = Group() + group.name = group_name + race.groups.append(group) + person.group = group + + if len(tokens[POS_START]) > 0: + person.start_time = hhmmss_to_time(tokens[POS_START]) + + res = ResultSportident() + res.person = person + if tokens[POS_CARD].isdigit(): + res.card_number = int(tokens[POS_CARD]) + res.start_time = person.start_time + result = tokens[POS_RES] + if result.find(':') > 0: + result_value = hhmmss_to_time(result) + res.finish_time = res.start_time + result_value + else: + if result in DNS_STATUS: + res.status = ResultStatus.DID_NOT_START + elif result in DSQ_STATUS: + res.status = ResultStatus.DISQUALIFIED + + splits = tokens[POS_SPLITS] + if len(splits) > 1: + splits_array = splits.split(spl_separator) + cur_time = person.start_time + for i in range(len(splits_array) // 2): + split = Split() + cur_time += hhmmss_to_time(splits_array[i * 2]) + split.time = cur_time + split.code = int(splits_array[i * 2 + 1]) + res.splits.append(split) + + race.persons.append(person) + race.results.append(res) diff --git a/sportorg/modules/recovery/recovery_si_master_csv.py b/sportorg/modules/recovery/recovery_si_master_csv.py new file mode 100644 index 00000000..706e2cee --- /dev/null +++ b/sportorg/modules/recovery/recovery_si_master_csv.py @@ -0,0 +1,62 @@ +""" +Parse backup memory CSV file of BSM SPORTident station generated by SI Config Plus + +-- Format: +CSV, separator ";" +Needed positions: card number (2), start (15), finish (21), splits (44): count of punches, (code + time) * n + +No;Read on;SIID;Start no;Clear CN;Clear DOW;Clear time;Clear_r CN;Clear_r DOW;Clear_r time;Check CN;Check DOW;Check time +;Start CN;Start DOW;Start time;Start_r CN;Start_r DOW;Start_r time;Finish CN;Finish DOW;Finish time;Finish_r CN;Finish_r +DOW;Finish_r time;Class;First name;Last name;Club;Country;Email;Date of birth;Sex;Phone;Street;ZIP;City;Hardware version +;Software version;Battery date;Battery voltage;Clear count;Character set;SEL_FEEDBACK;No. of records;Record 1 CN;Record +1 DOW;Record 1 time;Record 2 CN;Record 2 DOW;Record 2 time;Record 3 CN; + +-- Example: +440;2023-12-17 11:59:02;2007313;;2;Su; 12:40:50;;;;2;Su; 12:40:50;;;;;;;1;Su; 14:50:45;;;;;2007313;SPORTident Ru;;;;;;;; +;;;;;;;;;10;34;Su; 12:41:33;49;Su; 12:54:39;43;Su; 13:22:28;39;Su; 13:51:28;42;Su; 14:02:34;46;Su; 14:15:04;40;Su; 14:31 +:03;47;Su; 14:39:54;37;Su; 14:47:02;90;Su; 14:49:21; + +""" +import csv + +from sportorg.common.otime import OTime +from sportorg.models.memory import Race, ResultSportident, Split +from sportorg.modules.sportident.fix_time_sicard_5 import fix_time +from sportorg.utils.time import hhmmss_to_time + +POS_CARD = 2 +POS_START = 15 +POS_FINISH = 21 +POS_COUNT = 44 + + +def recovery(file_name: str, race: Race) -> None: + separator = ';' + + zero_time_val = race.get_setting('system_zero_time', (8, 0, 0)) + zero_time = OTime( + hour=zero_time_val[0], minute=zero_time_val[1], sec=zero_time_val[2] + ) + + with open(file_name, encoding='cp1251') as csv_file: + spam_reader = csv.reader(csv_file, delimiter=separator) + for tokens in spam_reader: + if tokens[0] == 'No' or len(tokens) < 45: + continue + + res = ResultSportident() + res.card_number = int(tokens[POS_CARD]) + res.start_time = hhmmss_to_time(tokens[POS_START]) + res.finish_time = hhmmss_to_time(tokens[POS_FINISH]) + + punch_count = int(tokens[POS_COUNT]) + existing_punches = (len(tokens) - POS_COUNT - 1) // 3 + + for i in range(min(punch_count, existing_punches)): + punch = Split() + punch.code = tokens[POS_COUNT + 3 * i + 1] + punch.time = hhmmss_to_time(tokens[POS_COUNT + 3 * i + 3]) + res.splits.append(punch) + + fix_time(res, zero_time) + race.results.append(res) diff --git a/sportorg/modules/recovery/recovery_sportorg_html.py b/sportorg/modules/recovery/recovery_sportorg_html.py new file mode 100644 index 00000000..245da3c6 --- /dev/null +++ b/sportorg/modules/recovery/recovery_sportorg_html.py @@ -0,0 +1,27 @@ +""" +Parse SportOrg HTML report (containing full json) + +""" +import os.path +import string +from io import open +from random import choices +from tempfile import gettempdir + + +def recovery(file_name: str) -> str: + with open(file_name, 'r', encoding='utf-8') as f: + for line in f.readlines(): + if line.find("var race = {\"courses\":") > -1: + json = line.strip()[11:-1] + + # save json to tmp file and op[en with standard import action + tmp_filename = os.path.join( + gettempdir(), + f"sportorg_{''.join(choices(string.ascii_letters, k=10))}.json", + ) + with open(tmp_filename, 'w') as temp_file: + temp_file.write(json) + + return tmp_filename + return "" diff --git a/sportorg/modules/recovery/recovery_sportorg_si_log.py b/sportorg/modules/recovery/recovery_sportorg_si_log.py new file mode 100644 index 00000000..ab6c5fda --- /dev/null +++ b/sportorg/modules/recovery/recovery_sportorg_si_log.py @@ -0,0 +1,78 @@ +""" +Parse backup SI log generated in C:\\Program Files (x86)\\SportOrg\\log + +-- Format: + +start +[SI_CARD] +[START] - 0:00:00 if not exist +[FINISH] - 0:00:00 if not exist +split_start +[CODE_1] [TIME_1] +... +[CODE_N] [TIME_N] +split_end +end + +-- Example: + +start +8013787 +00:00:00 +11:39:25 +split_start +57 11:43:05 +69 11:37:59 +90 11:39:07 +split_end +end + +""" + +from sportorg.common.otime import OTime +from sportorg.models.memory import Race, ResultSportident, Split +from sportorg.modules.sportident.fix_time_sicard_5 import fix_time +from sportorg.utils.time import hhmmss_to_time + + +def recovery(file_name: str, race: Race) -> None: + zero_time_val = race.get_setting('system_zero_time', (8, 0, 0)) + zero_time = OTime( + hour=zero_time_val[0], minute=zero_time_val[1], sec=zero_time_val[2] + ) + + cur_res = ResultSportident() + read_num = False + read_start = False + read_spl = False + read_finish = False + + with open(file_name) as f: + for line in f.readlines(): + line = line.strip() + if read_num: + cur_res.card_number = int(line) + read_start = True + read_num = False + elif read_start: + cur_res.start_time = hhmmss_to_time(line) + read_start = False + read_finish = True + elif read_finish: + cur_res.finish_time = hhmmss_to_time(line) + read_finish = False + elif line == 'end': + fix_time(cur_res, zero_time) + race.results.append(cur_res) + cur_res = ResultSportident() + elif line == 'start': + read_num = True + elif line == 'split_start': + read_spl = True + elif line == 'split_end': + read_spl = False + elif read_spl: + spl = Split() + spl.code = line.split(' ')[0] + spl.time = hhmmss_to_time(line.split(' ')[1]) + cur_res.splits.append(spl) diff --git a/sportorg/modules/sportident/fix_time_sicard_5.py b/sportorg/modules/sportident/fix_time_sicard_5.py new file mode 100644 index 00000000..d6d81003 --- /dev/null +++ b/sportorg/modules/sportident/fix_time_sicard_5.py @@ -0,0 +1,45 @@ +from sportorg.common.otime import OTime + +DEF_START_TIME = OTime(hour=8) + + +def if_si_card_5(card_number): + return card_number < 1000000 + + +def _fix_time(time, zero_time): + """ + takes nearest to zero time, assuming, that original time is in only 12h format (SPORTident SICard 5) + exclude 00:00:00, meaning no time + 00:15:18 (10:00:00) -> 12:15:18 + 00:15:18 (14:00:00) -> 00:15:18 + 11:15:18 (10:00:00) -> 11:15:18 + 11:15:18 (17:00:00) -> 23:15:18 + 22:15:18 (10:00:00) -> 10:15:18 + 22:15:18 (11:00:00) -> 22:15:18 + """ + origin_time = time + + if time == OTime(0): + return time + + time_12h = OTime(hour=12) + + if time >= time_12h: + time -= time_12h + + if zero_time > time: + if zero_time - time < time_12h: + time += time_12h + + return time + + +# fix time in result for SI Card 5 (12h format) +def fix_time(res, zero_time=DEF_START_TIME): + if if_si_card_5(res.card_number): + res.finish_time = _fix_time(res.finish_time, zero_time) + res.start_time = _fix_time(res.start_time, zero_time) + + for i in range(len(res.splits)): + res.splits[i].time = _fix_time(res.splits[i].time, zero_time)