Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add import persons table #475

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## next

* Fix not working online results when the settings responsible for timing are missing
* Added the ability to enter started numbers
* Added the ability to import participants from the table (For example Excel)

## 2024-10-07 v1.7.1

Expand Down
2 changes: 2 additions & 0 deletions changelog_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

+ Добавлена возможность выбрать директорию с шаблонами
+ Исправление не отправляющихся результатов онлайн при отсутствии группы настроек, отвечающих за хронометраж ([#472](https://github.com/sportorg/pysport/issues/472))
+ Добавлена возможность ввода стартовавших номеров
+ Добавлена возможность импорта участников из таблицы (Например Excel)

## 2024-10-07 v1.7.1

Expand Down
24 changes: 24 additions & 0 deletions languages/ru_RU/LC_MESSAGES/sportorg.po
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ msgstr "Включить"
msgid "URL not valid"
msgstr "URL недействителен"

msgid "Import persons from table"
msgstr "Импорт участников из таблицы"

msgid "Set started numbers"
msgstr "Ввод стартовавших участников"

Expand Down Expand Up @@ -294,6 +297,15 @@ msgstr "Удалять исходный номер"
msgid "Replace source with reserve"
msgstr "Создать резервного участника для исходного номера"

msgid "Replacement by bib"
msgstr "Замена по стартовому номеру"

msgid "Replacement by name"
msgstr "Замена по имени и фамилии"

msgid "Insert new records"
msgstr "Вставка новых записей"

msgid "Team properties"
msgstr "Свойства коллектива"

Expand Down Expand Up @@ -1215,6 +1227,18 @@ msgstr "Люди"
msgid "Duplicate card numbers (card numbers are reset)"
msgstr "Дублирующиеся чипы (номера чипов обнулены)"

msgid "Bib header not found"
msgstr "Cтолбец Номер не найден"

msgid "Name header not found"
msgstr "Cтолбец c именем или фамилией не найден"

msgid "Person not found"
msgstr "Участник не найден"

msgid "Bib not found"
msgstr "Номер не найден"

msgid "Duplicate names"
msgstr "Повторяющиеся имена"

Expand Down
282 changes: 282 additions & 0 deletions sportorg/gui/dialogs/import_persons_table_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import logging
from enum import Enum

from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFormLayout,
QTableWidget,
QTableWidgetItem,
QApplication,
)

from sportorg import config
from sportorg.gui.dialogs.text_io import set_property
from sportorg.gui.global_access import GlobalAccess
from sportorg.gui.utils.custom_controls import AdvComboBox
from sportorg.language import translate
from sportorg.models import memory
from sportorg.models.memory import find, race


class ImportPersonsTableDialog(QDialog):
class ExtendedEnum(Enum):
@classmethod
def list(cls):
return list(map(lambda c: c.value, cls))

@classmethod
def name(cls, val):
return {v: k for k, v in dict(vars(cls)).items() if isinstance(v, int)}.get(
val, None
)

class HEADER(ExtendedEnum):
NONE = ""
BIB = translate("Bib")
GROUP = translate("Group")
TEAM = translate("Team")
NAME = translate("First name")
SURNAME = translate("Last name")
YEAR = translate("Year of birth")
COMMENT = translate("Comment")
QUAL = translate("Qualification")
CARD = translate("Card number")
START = translate("Start")
FINISH = translate("Finish")
START_GROUP = translate("Start group")

def __init__(self):
super().__init__(GlobalAccess().get_main_window())

def exec_(self):
self.init_ui()
return super().exec_()

def init_ui(self):
self.setWindowTitle(translate("Import persons from table"))
self.setWindowIcon(QIcon(config.ICON))
self.setSizeGripEnabled(True)
self.setModal(True)

self.layout = QFormLayout(self)

self.REPLACEMENT_BY_BIB = translate("Replacement by bib")
self.REPLACEMENT_BY_NAME = translate("Replacement by name")
self.INSERT_NEW = translate("Insert new records")

self.option_import = AdvComboBox()
self.option_import.addItems(
[self.REPLACEMENT_BY_BIB, self.REPLACEMENT_BY_NAME, self.INSERT_NEW]
)
self.layout.addRow(self.option_import)

self.headers = self.HEADER.list()

copied_values = self.parse_clipboard_value()

self.count_rows = len(copied_values)
self.count_columns = len(copied_values[0])

self.persons_info_table = QTableWidget(self)
self.persons_info_table.setRowCount(self.count_rows)
self.persons_info_table.setColumnCount(self.count_columns)

for i in range(self.count_columns):
header_import = AdvComboBox()
header_import.addItems(self.headers)
self.persons_info_table.setCellWidget(0, i, header_import)

for idRow, row in enumerate(copied_values):
for idColumn, cell in enumerate(row):
new_item = QTableWidgetItem(cell)
self.persons_info_table.setItem(idRow + 1, idColumn, new_item)

self.layout.addRow(self.persons_info_table)

def cancel_changes():
self.close()

def apply_changes():
try:
self.apply_changes_impl()
except Exception as e:
logging.exception(e)
self.close()

button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_ok = button_box.button(QDialogButtonBox.Ok)
self.button_ok.setText(translate("OK"))
self.button_ok.clicked.connect(apply_changes)
self.button_cancel = button_box.button(QDialogButtonBox.Cancel)
self.button_cancel.setText(translate("Cancel"))
self.button_cancel.clicked.connect(cancel_changes)
self.layout.addRow(button_box)

self.resize(900, 300)
self.show()

def apply_changes_impl(self):
import_data(self)
return

@staticmethod
def parse_clipboard_value():
text = QApplication.clipboard().text()
output_list = []
for row in filter(None, text.splitlines()):
output_list.append(row.split("\t"))
return output_list


def import_data(self):
obj = memory.race()
self.input_headers = {}
for i in range(self.count_columns):
item = self.persons_info_table.cellWidget(0, i)
self.input_headers[self.HEADER(item.currentText())] = i

for i in range(1, self.count_rows):
person = None
if self.option_import.currentText() == self.INSERT_NEW:
person = memory.Person()

if self.option_import.currentText() == self.REPLACEMENT_BY_BIB:
if self.HEADER.BIB not in self.input_headers:
logging.error("{}".format(translate("Bib header not found")))
break
bib = get_value_table(self, i, self.HEADER.BIB)
if not bib.isdigit():
logging.error("{}".format(translate("Bib not found") + ":" + bib))
continue
person = memory.race().find_person_by_bib(int(bib))
if person is None:
logging.error("{}".format(translate("Bib not found") + ":" + bib))
continue

if self.option_import.currentText() == self.REPLACEMENT_BY_NAME:
if (self.HEADER.NAME not in self.input_headers) or (
self.HEADER.SURNAME not in self.input_headers
):
logging.error("{}".format(translate("Name header not found")))
break
name = get_value_table(self, i, self.HEADER.NAME)
surname = get_value_table(self, i, self.HEADER.SURNAME)
person = find(race().persons, name=name, surname=surname)
if person is None:
logging.error(
"{}".format(
translate("Person not found") + ":" + name + " " + surname
)
)
continue

if self.HEADER.NAME in self.input_headers:
person.name = get_value_table(self, i, self.HEADER.NAME)

if self.HEADER.SURNAME in self.input_headers:
person.surname = get_value_table(self, i, self.HEADER.SURNAME)

if self.HEADER.YEAR in self.input_headers:
year = get_value_table(self, i, self.HEADER.YEAR)
if year.isdigit():
person.set_year(int(get_value_table(self, i, self.HEADER.YEAR)))

if self.HEADER.GROUP in self.input_headers:
group_name = get_value_table(self, i, self.HEADER.GROUP)
group = memory.find(obj.groups, name=group_name)
if group is None:
group = memory.Group()
group.name = group_name
group.long_name = group_name
obj.groups.append(group)
person.group = group

if self.HEADER.TEAM in self.input_headers:
team_name = get_value_table(self, i, self.HEADER.TEAM)
org = memory.find(obj.organizations, name=team_name)
if org is None:
org = memory.Organization()
org.name = team_name
obj.organizations.append(org)
person.organization = org

if self.HEADER.BIB in self.input_headers:
bib = get_value_table(self, i, self.HEADER.BIB)
if bib != "":
set_property(person, self.HEADER.BIB.value, bib)

if self.HEADER.CARD in self.input_headers:
card = get_value_table(self, i, self.HEADER.CARD)
if card != "":
set_property(person, self.HEADER.CARD.value, card)

if self.HEADER.QUAL in self.input_headers:
qual = get_value_table(self, i, self.HEADER.QUAL)
if qual != "":
set_property(person, self.HEADER.QUAL.value, qual)

if self.HEADER.COMMENT in self.input_headers:
set_property(
person,
self.HEADER.COMMENT.value,
get_value_table(self, i, self.HEADER.COMMENT),
)

if self.HEADER.START in self.input_headers:
set_property(
person,
self.HEADER.START.value,
get_value_table(self, i, self.HEADER.START),
)

if self.HEADER.FINISH in self.input_headers:
set_property(
person,
self.HEADER.FINISH.value,
get_value_table(self, i, self.HEADER.FINISH),
)

if self.HEADER.START_GROUP in self.input_headers:
set_property(
person,
self.HEADER.START_GROUP.value,
get_value_table(self, i, self.HEADER.START_GROUP),
)

if self.option_import.currentText() == self.INSERT_NEW:
obj.persons.append(person)

persons_dupl_cards = obj.get_duplicate_card_numbers()
persons_dupl_names = obj.get_duplicate_names()

if len(persons_dupl_cards):
logging.info(
"{}".format(translate("Duplicate card numbers (card numbers are reset)"))
)
for person in sorted(persons_dupl_cards, key=lambda x: x.card_number):
logging.info(
"{} {} {} {}".format(
person.full_name,
person.group.name if person.group else "",
person.organization.name if person.organization else "",
person.card_number,
)
)
person.set_card_number(0)
if len(persons_dupl_names):
logging.info("{}".format(translate("Duplicate names")))
for person in sorted(persons_dupl_names, key=lambda x: x.full_name):
logging.info(
"{} {} {} {}".format(
person.full_name,
person.get_year(),
person.group.name if person.group else "",
person.organization.name if person.organization else "",
)
)


def get_value_table(self, idx, header):
return self.persons_info_table.item(idx, self.input_headers[header]).text().strip()
7 changes: 7 additions & 0 deletions sportorg/gui/menu/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sportorg.gui.dialogs.marked_route_dialog import MarkedRouteDialog
from sportorg.gui.dialogs.merge_results import MergeResultsDialog
from sportorg.gui.dialogs.not_start_dialog import InputStartNumbersDialog
from sportorg.gui.dialogs.import_persons_table_dialog import ImportPersonsTableDialog
from sportorg.gui.dialogs.number_change import NumberChangeDialog
from sportorg.gui.dialogs.organization_mass_edit import OrganizationMassEditDialog
from sportorg.gui.dialogs.print_properties import PrintPropertiesDialog
Expand Down Expand Up @@ -709,6 +710,12 @@ def execute(self):
self.app.refresh()


class ImportPersonsAction(Action, metaclass=ActionFactory):
def execute(self):
ImportPersonsTableDialog().exec_()
self.app.refresh()


class CPDeleteAction(Action, metaclass=ActionFactory):
def execute(self):
CPDeleteDialog().exec_()
Expand Down
4 changes: 4 additions & 0 deletions sportorg/gui/menu/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def menu_list():
"title": translate("SportOrg HTML"),
"action": "RecoverySportorgHtmlAction",
},
{
"title": translate("Import persons from table"),
"action": "ImportPersonsAction",
},
],
},
{
Expand Down
Loading