From 1c3fb72ac83e96e765a65186bb5e371dd1e5ebcb Mon Sep 17 00:00:00 2001 From: emkael Date: Tue, 1 Sep 2015 20:53:02 +0200 Subject: * build tools and environment --- .gitignore | 2 +- BUILD.md | 134 ++++++++++++++++++++ MAKE.bat | 1 + MAKE.ps1 | 9 ++ README.md | 48 ++++--- bidding_data.py | 303 -------------------------------------------- bidding_data.spec | 17 +++ bundle/.gitignore | 0 bundle/bidding_data-1.0.zip | Bin 0 -> 6039445 bytes css/bidding.css | 11 -- images/link.png | Bin 384 -> 0 bytes javas/bidding.js | 43 ------- res/css/bidding.css | 11 ++ res/images/link.png | Bin 0 -> 384 bytes res/javas/bidding.js | 43 +++++++ src/.gitignore | 1 + src/bidding_data.py | 303 ++++++++++++++++++++++++++++++++++++++++++++ src/icon.ico | Bin 0 -> 50862 bytes src/icon.xcf | Bin 0 -> 12001 bytes src/version | 39 ++++++ 20 files changed, 588 insertions(+), 377 deletions(-) create mode 100644 BUILD.md create mode 100644 MAKE.bat create mode 100644 MAKE.ps1 delete mode 100644 bidding_data.py create mode 100644 bidding_data.spec create mode 100644 bundle/.gitignore create mode 100644 bundle/bidding_data-1.0.zip delete mode 100644 css/bidding.css delete mode 100644 images/link.png delete mode 100644 javas/bidding.js create mode 100644 res/css/bidding.css create mode 100644 res/images/link.png create mode 100644 res/javas/bidding.js create mode 100644 src/.gitignore create mode 100644 src/bidding_data.py create mode 100644 src/icon.ico create mode 100644 src/icon.xcf create mode 100644 src/version diff --git a/.gitignore b/.gitignore index 0d20b64..a261f29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -*.pyc +dist/* diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..ffe215c --- /dev/null +++ b/BUILD.md @@ -0,0 +1,134 @@ + +JFR Pary - dane licytacji: Informacje dla programistów +====================================================== + +Struktura repozytorium kodu +--------------------------- + +Katalog [`src`](src) zawiera komponenty źródłowe programu: + +* [kod skryptu Pythona](src/bidding_data.py), który wykonuje całą robotę +* [ikonę programu](src/icon.ico) wraz ze [źródłami](src/icon.xcf) +* [metadane programu](src/version) dla PyInstallera + +Katalog [`res`](res) zawiera pliki dołączane do programu: style, skrypty JS i grafikę. + +Katalogi `dist` i `build` są domyślnie puste i są katalogami roboczymi +PyInstallera. + +Katalog [`bundle`](bundle) zawiera wynikowe paczki ZIP z kolejnymi wersjami programu. + +W katalogu głównym znajdują się rozmaite README oraz skrypty budujące program. + +Od zera do bohatera - proces budowania programu +----------------------------------------------- + +Jedynym wymaganym do działania narzędzia elementem repozytorium jest źródłowy +skrypt [`bidding_data.py`](src/bidding_data.py). Cała reszta to tylko +fajerwerki i opakowanie w plik wykonywalny. + +Skrypt można uruchomić w dowolnym środowisku, w którym działa Python, a jego +wymagania wymienione są poniżej. `bidding_data.py` przyjmuje parametry +identycznie do wynikowego pliku wykonywalnego. + +Wersja z głównej gałęzi repozytorium działa jedynie pod Windowsem, z racji +używania podłączania się bezpośrednio do pliku BWS poprzez Accessowe ODBC. +Gałąź `csv` zawiera kompatybilną między systemami operacyjnymi wersję operującą +na danych podanych w plikach CSV, do których można wyeksportować dane z BWS. + +Skrypt z gałęzi `csv` jest w pełni funkcjonalny, acz obdarty z fajerwerków. +Może również nie być dalej rozwijany, w zależności od upierdliwości scalania +zmian. + +--- + +Pliku źródłowego można użyć jako modułu, jeśli kogoś to kręci, importując go +do swojej aplikacji poprzez: +``` +from bidding_data import JFRBidding +``` + +--- + +Skrypt można samodzielnie skompilować do pliku wykonywalnego, używając do tego +PyInstallera. Można to zrobić z pomocą dołączonego pliku [`bidding_data.spec`](bidding_data.spec): +``` +pyinstaller bidding_data.spec +``` +lub samodzielnie, podając odpowiednie parametry do PyInstallera: +``` +pyinstaller --one-file --version=[src\version](src/version) --icon=[src\icon.ico](src/icon.ico) src\bidding_data.py +``` +Zarówno metadane z pliku `src/version`, jak i ikona programu są w 100% opcjonalne. + +Wynik działania PyInstallera (pojedynczy plik wykonywalny) znajdzie się w katalogu `dist`. + +--- + +Skrypt wsadowy [`MAKE.bat`](MAKE.bat) pakuje potrzebne do dystrybucji programu dane +w jedną, zgrabną paczkę. Uruchamia on jedynie skrypt Windows PowerShell [`MAKE.ps1`](MAKE.ps1), +który, kolejno: + +* kompiluje EXE przy użyciu PyInstallera +* kopiuje README do katalogu `dist` +* kopiuje zasoby z `res` do katalogu `dist` +* odczytuje metadane utworzonego EXE +* tworzy z nich nazwę dla paczki +* pakuje cały katalog `dist` do paczki i umieszcza ją w `bundle` + +Wymagania systemowe +------------------- + +Skrypt [`bidding_data.py`](src/bidding_data.py): + +* python 2.x (testowane i tworzone w wersji 2.7.10) +* BeautifulSoup4 +* lxml (jako parser dla BS4) +* argparse +* pypyodbc + +Kompilacja do EXE: + +* [PyInstaller](http://pythonhosted.org/PyInstaller/) +* PyWin32 + +Zbudowanie paczki z [`bundle`](bundle): + +* Windows PowerShell +* .NET 4.5 + +Kod żródłowy +------------ + +Kod źródłowy stara się, z grubsza: + +* zgadzać ze standardami [PEP8](https://www.python.org/dev/peps/pep-0008/) +* nie robić [głupich rzeczy](http://stackoverflow.com/a/1732454) +* nie psuć raz przekształconej strony przy próbie ponownego przekształcenia +* komentować rzeczy nieoczywiste + +Operacje na stronach JFR +------------------------ + +Ramowy algorytm działania programu: + +1. Wczytać dane z BWS: tabela `BiddingData` zawiera dane licytacji, tabela +`RoundData` zawiera dane rund (numery rozdań, numery par itp.). +2. Zmapować numery rozdań JFR Pary (jedna, ciągła numeracja dla całego turnieju) +na numery rozdań fizycznych pudełek na sali (numery rozdań w BWS). +3. Skompilować tabele z licytacjami i zapisać je do osobnych plików. Format +nazwy pliku to `[PREFIX_JFR]_bidding_[NUMER_JFR]_[NUMERY_PAR].txt`. Numery par +w nazwie pliku posortowane są rosnąco. Każdy plik zawiera gotowy kod HTML +z licytacją. +4. Dołączyć do plików protokołów (`[PREFIX_JFR][NUMER_JFR].html`) skrypty JS +niezbędne do pokazania licytacji w protokole (jQuery i [`bidding.js`](res/javas/bidding.js)). +5. W plikach zawartości protokołów (`[PREFIX_JFR][NUMER_JFR].txt`), do każdego wiersza, +dla którego dysponujemy licytacją, dołączyć link pokazujący licytację. + +Program obsługuje wiele rozdań o tym samym numerze w jednym BWS i mapuje +rozdania na odpowiednie numery JFR na podstawie zestawu danych: nr stołu +(z sektorem), nr rundy, nr pudełka rozdaniowego. + +~~~ + +`Hello image, sing me a line from your favourite song.` diff --git a/MAKE.bat b/MAKE.bat new file mode 100644 index 0000000..f9c4987 --- /dev/null +++ b/MAKE.bat @@ -0,0 +1 @@ +powershell -NoProfile -ExecutionPolicy bypass -File MAKE.ps1 diff --git a/MAKE.ps1 b/MAKE.ps1 new file mode 100644 index 0000000..d0ddd47 --- /dev/null +++ b/MAKE.ps1 @@ -0,0 +1,9 @@ +& pyinstaller bidding_data.spec +Copy-Item '*.md' -Destination 'dist' -Force +Copy-Item 'res\*' -Destination 'dist' -Force -Recurse +Set-Variable -Name VersionInfo -Value (Get-Item 'dist\bidding_data.exe').VersionInfo +Set-Variable -Name FileVersion -Value $VersionInfo.FileVersion.Split(',') +Set-Variable -Name BundleName -Value ('bundle\\' + $VersionInfo.InternalName + '-' + $FileVersion[0].Trim() + '.' + $FileVersion[1].Trim() + '.zip') +Remove-Item $BundleName -ErrorAction SilentlyContinue +Add-Type -Assembly 'System.IO.Compression.FileSystem' +[System.IO.Compression.ZipFile]::CreateFromDirectory('dist', $BundleName) diff --git a/README.md b/README.md index c050c9a..c710e25 100644 --- a/README.md +++ b/README.md @@ -12,39 +12,49 @@ Przykładowe efekty działania: Wymagania systemowe ------------------- -* python 2.x (testowane i tworzone w wersji 2.7.10) -* BeautifulSoup4 -* lxml (jako parser dla BS4) -* argparse -* pypyodbc +* system opearcyjny MS Windows (testowane na Win7 i Win8.1) +* sterownik ODBC dla plików MS Access (zwykle obecny domyślnie z Windows, +weryfikowalny w Panelu Sterowania -> Narzędziach Administracyjnych -> +Żródła danych ODBC) Instalacja ---------- -Ściągnij zawartość tego repozytorium. +Ściągnij paczkę z programem, dostępną w katalogu [`bundle`](bundle) tego +repozytorium i rozpakuj ją do wybranego przez siebie katalogu roboczego +programu. -W katalogu WWW Par skonfiguruj JS i CSS niezbędny do prezentacji danych +W katalogu WWW Par skonfiguruj zasoby niezbędnę do prezentacji danych licytacji: -* skopiuj [`css/bidding.css`](css/bidding.css) do katalogu WWW -* dołącz plik [`css/bidding.css`](css/bidding.css) gdzieś w arkuszach stylów turnieju -(np. poprzez `@import` w `kolorki.css`) -* skopiuj [`javas/bidding.js`](javas/bidding.js) do podkatalogu javas katalogu WWW (plik dołączany -jest automatycznie do stron z wynikami) -* skopiuj [`images/link.png`](images/link.png) do podkatalogu images katalogu WWW +* skopiuj [`css/bidding.css`](res/css/bidding.css) do katalogu WWW +* dołącz plik [`css/bidding.css`](res/css/bidding.css) gdzieś w arkuszach +stylów turnieju (np. poprzez `@import` w `kolorki.css`) +* skopiuj [`javas/bidding.js`](res/javas/bidding.js) do podkatalogu javas +katalogu WWW (plik dołączany jest automatycznie do stron z wynikami) +* skopiuj [`images/link.png`](res/images/link.png) do podkatalogu images +katalogu WWW Już, gotowe. +Kompilacja i praca z kodem narzędzia +------------------------------------ + +Patrz: [`BUILD.md`](BUILD.md) + Użycie ------ -Skrypt [`bidding_data.py`](bidding_data.py) operuje na następujących +Program skłąda się ze skompilowanego skryptu języka Python, dostępnego +w katalogu [`src`](src) tego repozytorium. + +Skrypt [`bidding_data.py`](src/bidding_data.py) operuje na następujących danych wejściowych: * plikach HTML wygenerowanych po zakończeniu turnieju stron statycznych * pliku BWS sesji -Skrypt przyjmuje parametry w sposób następujący: +Program przyjmuje parametry w sposób następujący: ``` -python bidding_data.py DANE_SESJI.bws PLIK_TURNIEJU.html +bidding_data.exe DANE_SESJI.bws PLIK_TURNIEJU.html ``` `DANE_SESJI.bws` to plik BWS z zebranymi danymi sesji. @@ -54,13 +64,13 @@ python bidding_data.py DANE_SESJI.bws PLIK_TURNIEJU.html Narzędzie obsługuje niestandardowe zakresy numeracji rozdań w turnieju. -Mapowanie numeru rozdań z Par na numer rozdania w BWS (numer fizycznego pudełka) -odbywa się automatycznie (na podstawie danych z BWS). +Mapowanie numeru rozdań z Par na numer rozdania w BWS (numer fizycznego +pudełka) odbywa się automatycznie (na podstawie danych z BWS). Kompatybilność -------------- -Narzędzie łączy się przez ODBC do bazy MSAccess, więc działa jedynie +Narzędzie łączy się przez ODBC do bazy MS Access, więc działa jedynie pod Windowsem. Wersja operująca na wyeksportowanych plikach CSV (np. przez `mdb-export`), diff --git a/bidding_data.py b/bidding_data.py deleted file mode 100644 index ae789f1..0000000 --- a/bidding_data.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -import glob -import re -import pypyodbc - -from os import path -from bs4 import BeautifulSoup as bs4 - - -class JFRBidding: - - # alignment of the bidding table - __directions = ['W', 'N', 'E', 'S'] - - # converts BWS lineup data to - # {round}.{sector}_{table}.{pair numbers} structure - def __parse_lineup_data(self, sitting_data): - round_lineups = {} - for sitting in sitting_data[1:]: - round_no = sitting[2] - table_no = str(sitting[0]) + '_' + str(sitting[1]) - if round_no not in round_lineups: - round_lineups[round_no] = {} - round_lineups[round_no][table_no] = sorted([ - sitting[3], sitting[4]]) - return round_lineups - - # converts BWS bidding to the structure: - # {board}_{round}_{sector}_{table}.{sector}_{table}.{round} -> {bidding}[], - # including erased calls - def __parse_bidding_data(self, bidding_data): - bids = {} - for bid in bidding_data[1:]: - round_no = bid[3] - table_no = str(bid[1]) + '_' + str(bid[2]) - board_no = str(bid[4]) + '_' + str(round_no) + '_' + table_no - bid_counter = bid[5] - bid_erased = bid[10] - if board_no not in bids: - bids[board_no] = {} - if table_no not in bids[board_no]: - bids[board_no][table_no] = {} - if round_no not in bids[board_no][table_no]: - bids[board_no][table_no][round_no] = {} - if bid_erased == 1: - if bid_counter in bids[board_no][table_no][round_no]: - if bids[board_no][table_no][round_no][bid_counter][ - 'direction'] == bid[6]: - bids[board_no][table_no][round_no].pop( - bid_counter, None) - if len(bids[board_no][table_no][round_no]) == 0: - bids[board_no][table_no].pop(round_no, None) - else: - bids[board_no][table_no][round_no][bid_counter] = { - 'direction': bid[6], 'bid': bid[7]} - return bids - - # converts bidding data into HTML table - def __format_bidding(self, bidding): - bid_match = re.compile('(\d)([SHDCN])') - html_output = bs4('', 'lxml') - header_row = html_output.new_tag('tr') - html_output.table.append(header_row) - for direction in self.__directions: - header_cell = html_output.new_tag('th') - header_cell.string = direction - header_row.append(header_cell) - for bid_round in bidding: - bidding_row = html_output.new_tag('tr') - html_output.table.append(bidding_row) - for bid in bid_round: - bid_cell = html_output.new_tag('td') - call_match = re.match(bid_match, bid) - if call_match: - bid_cell.append(call_match.group(1)) - bid_icon = html_output.new_tag( - 'img', src='images/' + call_match.group(2) + '.gif') - bid_cell.append(bid_icon) - else: - bid_cell.append(bid) - bidding_row.append(bid_cell) - return html_output.table.prettify() - - # returns file path for bidding HTML output - # {prefix}_bidding_{jfr_board_number}_{pair_numbers}.txt - def __get_bidding_file_output_path(self, - board_no, - round_no=None, - table_no=None, - pair_numbers=None): - return u'{0}_bidding_{1:03}_{2}.txt'.format( - self.__tournament_prefix, board_no, - '_'.join(map(str, - self.__round_lineups[round_no][table_no] - if pair_numbers is None # read numbers from lineup - else pair_numbers))) # or use provided numbers - - def __map_board_numbers(self): - self.__tournament_files = [ - f for f - in glob.glob(self.__tournament_prefix + '*.html') - if re.search(self.__tournament_files_match, f)] - for round_data in self.__lineup_data: - # 13th column has JFR number for the first board - if len(round_data) > 12: - jfr_number = round_data[12] - round_no = round_data[2] - sector_no = round_data[0] - table_no = round_data[1] - if jfr_number and round_no: - # 5th and 6th - actual board number - for board_number in range(int(round_data[5]), - int(round_data[6])+1): - board_string = '_'.join([ - str(board_number), - str(round_no), - str(sector_no), - str(table_no)]) - self.__board_number_mapping[ - board_string - ] = jfr_number + board_number - round_data[5] - # only include these board numbers from mapping - # which actually exist in JFR output - custom_files = [] - for b_number, jfr_number in self.__board_number_mapping.iteritems(): - board_files = [ - f for f - in self.__tournament_files - if f.endswith('{0:03}.html'.format(jfr_number))] - if len(board_files): - custom_files = custom_files + board_files - else: - self.__board_number_mapping[b_number] = None - self.__tournament_files = custom_files - - # sitting read from BWS - __round_lineups = {} - # bidding read from BWS - __bids = {} - - # full path + JFR prefix - __tournament_prefix = '' - # RegEx matching board HTML files - __tournament_files_match = None - # matched files, including board number mapping boundaries - __tournament_files = [] - - # BWS number -> JFR number mapping - __board_number_mapping = {} - - def __init__(self, bws_file, file_prefix): - with pypyodbc.win_connect_mdb(bws_file) as connection: - cursor = connection.cursor() - self.__lineup_data = cursor.execute( - 'SELECT * FROM RoundData').fetchall() - bid_data = cursor.execute('SELECT * FROM BiddingData').fetchall() - self.__round_lineups = self.__parse_lineup_data(self.__lineup_data) - self.__bids = self.__parse_bidding_data(bid_data) - self.__tournament_prefix = path.splitext( - path.realpath(file_prefix))[0] - self.__tournament_files_match = re.compile( - re.escape(self.__tournament_prefix) + '([0-9]{3})\.html') - self.__map_board_numbers() - - def write_bidding_tables(self): - for board_no, board_data in self.__bids.items(): - if board_no in self.__board_number_mapping: - for table_no, table_data in board_data.items(): - for round_no, round_data in table_data.items(): - if round_no in self.__round_lineups: - if table_no in self.__round_lineups[round_no]: - bidding = sorted(round_data) - dealer = round_data[bidding[0]]['direction'] - bidding_table = [[], [], [], []] - # compile bidding player-by-player - for bid_index in bidding: - bid = round_data[bid_index] - bidding_table[ - self.__directions.index( - bid['direction']) - ].append(bid['bid']) - last_bidder = bid['direction'] - # fill skipped calls for players before dealer - # in the first round of bidding - for pos in range( - 0, self.__directions.index(dealer)): - bidding_table[pos].insert(0, '') - # fill skipped calls for players after pass out - # (so that bidding table is a proper matrix) - for pos in range( - self.__directions.index(last_bidder), - len(self.__directions)): - bidding_table[pos].append('') - # transpose the bidding table - # aligning it row-by-row (bid round-by-round) - bidding_table = map(list, zip(*bidding_table)) - bidding_fpath = \ - self.__get_bidding_file_output_path( - self.__board_number_mapping[board_no], - round_no, table_no) - with file(bidding_fpath, 'w') as bidding_file: - bidding_file.write( - self.__format_bidding(bidding_table)) - - def write_bidding_scripts(self): - for tournament_file in self.__tournament_files: - with file(tournament_file, 'r+') as board_html: - board_content = bs4(board_html, 'lxml', from_encoding='utf-8') - header_scripts = board_content.select('head script') - # check for jQuery, append if necessary - jquery_scripts = [script for script in header_scripts - if script['src'] == 'javas/jquery.js'] - if not len(jquery_scripts): - jquery = board_content.new_tag( - 'script', src='javas/jquery.js', - type='text/javascript') - jquery_scripts.append(jquery) - board_content.head.append(jquery) - # check for bidding.js - bidding_scripts = [ - script for script in header_scripts - if script['src'] == 'javas/bidding.js'] - # and make sure bidding.js is appended after jQuery - for script in bidding_scripts: - script.extract() - bidding_script = board_content.new_tag( - 'script', src='javas/bidding.js', - type='text/javascript') - jquery_scripts[0].insert_after(bidding_script) - board_html.seek(0) - board_html.write(board_content.prettify( - 'utf-8', formatter='html')) - board_html.truncate() - - def write_bidding_links(self): - for tournament_file in self.__tournament_files: - file_number = re.match( - self.__tournament_files_match, - tournament_file).group(1) - board_text_path = path.splitext(tournament_file)[0] + '.txt' - with file(board_text_path, 'r+') as board_text: - board_text_content = bs4( - board_text, 'lxml', from_encoding='iso-8859-2') - for row in board_text_content.select('tr'): - cells = row.select('td') - # traveller table rows for specific score entries - # should have 11 cells - if len(cells) == 11: - try: - pair_numbers = sorted([ - int(cells[1].contents[0]), - int(cells[2].contents[0])]) - except ValueError: - continue - bidding_link = board_text_content.new_tag( - 'a', href='#', **{'class': 'biddingLink'}) - bidding_link.string = ' ' - bidding_link['data-bidding-link'] = path.basename( - self.__get_bidding_file_output_path( - int(file_number, 10), - pair_numbers=pair_numbers)) - # only append link if we've got bidding data - if path.isfile(path.join( - path.dirname(self.__tournament_prefix), - bidding_link['data-bidding-link'])): - # fourth cell is the contract - for link in cells[3].select('a.biddingLink'): - link.extract() - cells[3].append(bidding_link) - board_text.seek(0) - board_text.write(board_text_content.table.prettify( - 'iso-8859-2', formatter='html')) - board_text.truncate() - -if __name__ == '__main__': - import argparse - - argument_parser = argparse.ArgumentParser( - description='Display bidding data from BWS files on JFR Pary pages') - - def file_path(filepath): - filepath = unicode(filepath, sys.getfilesystemencoding()) - if path.isfile(filepath): - return filepath - else: - argument_parser.error('File %s does not exist' % filepath) - - argument_parser.add_argument('bws_file', metavar='BWS_FILE', - help='path to BWS file', - type=file_path) - argument_parser.add_argument('path', metavar='PATH', - help='tournament path (to PREFIX.html)', - type=file_path) - - arguments = argument_parser.parse_args() - - bidding_parser = JFRBidding( - bws_file=arguments.bws_file, - file_prefix=arguments.path, - ) - bidding_parser.write_bidding_tables() - bidding_parser.write_bidding_scripts() - bidding_parser.write_bidding_links() diff --git a/bidding_data.spec b/bidding_data.spec new file mode 100644 index 0000000..6fb6c0b --- /dev/null +++ b/bidding_data.spec @@ -0,0 +1,17 @@ +import os +a = Analysis(['src\\bidding_data.py'], + pathex=[os.path.abspath('.')], + hiddenimports=[], + hookspath=None, + runtime_hooks=None) +pyz = PYZ(a.pure) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='bidding_data.exe', + debug=False, + strip=None, + upx=True, + console=True , version='src\\version', icon='src\\icon.ico') diff --git a/bundle/.gitignore b/bundle/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/bundle/bidding_data-1.0.zip b/bundle/bidding_data-1.0.zip new file mode 100644 index 0000000..2ffa8b6 Binary files /dev/null and b/bundle/bidding_data-1.0.zip differ diff --git a/css/bidding.css b/css/bidding.css deleted file mode 100644 index ea4ef4a..0000000 --- a/css/bidding.css +++ /dev/null @@ -1,11 +0,0 @@ -/* Tabelka licytacji */ -#bidding_popup { background-color: white; padding: 10px; border-radius: 10px; border: 1px solid black; - -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); - -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); - box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); - z-index: 1000; -} -#bidding_popup table { width: 100%; text-align: center; } -#bidding_popup table th { text-align: center; } -a.biddingLink { background: white url('../images/link.png') no-repeat; display: inline-block; width: 16px; height: 16px; position: absolute; right: 0; margin: 1px 1px 1px 3px } -td.nol {position:relative; min-height: 16px; min-width: 55px} \ No newline at end of file diff --git a/images/link.png b/images/link.png deleted file mode 100644 index e417d59..0000000 Binary files a/images/link.png and /dev/null differ diff --git a/javas/bidding.js b/javas/bidding.js deleted file mode 100644 index 5932e66..0000000 --- a/javas/bidding.js +++ /dev/null @@ -1,43 +0,0 @@ -var display_bidding = function(element, bidding) { - var popup = $('
'); - popup.css({ - 'position': 'absolute', - 'width': '250px', - 'left': element.offset().left + element.width(), - 'top': element.offset().top - }); - popup.html(bidding); - $('body').append(popup); -} - -var load_bidding = function() { - $('#bidding_popup').remove(); - var elem = $(this); - $.ajax( - { - url: elem.attr('data-bidding-link'), - complete: function(xhr, status) { - if (status == 'success') { - display_bidding(elem, xhr.responseText); - } - else { - display_bidding(elem, 'Brak danych'); - } - } - } - ); - return false; -}; - -var bind_bidding_links = function() { - $('a.biddingLink').each(function() { - $(this).unbind('click').click(load_bidding); - }); - $(document).click(function() { - $('#bidding_popup').remove(); - }); -}; - -$(document).ready(function() { - setInterval(bind_bidding_links, 1000); -}); diff --git a/res/css/bidding.css b/res/css/bidding.css new file mode 100644 index 0000000..ea4ef4a --- /dev/null +++ b/res/css/bidding.css @@ -0,0 +1,11 @@ +/* Tabelka licytacji */ +#bidding_popup { background-color: white; padding: 10px; border-radius: 10px; border: 1px solid black; + -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); + box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75); + z-index: 1000; +} +#bidding_popup table { width: 100%; text-align: center; } +#bidding_popup table th { text-align: center; } +a.biddingLink { background: white url('../images/link.png') no-repeat; display: inline-block; width: 16px; height: 16px; position: absolute; right: 0; margin: 1px 1px 1px 3px } +td.nol {position:relative; min-height: 16px; min-width: 55px} \ No newline at end of file diff --git a/res/images/link.png b/res/images/link.png new file mode 100644 index 0000000..e417d59 Binary files /dev/null and b/res/images/link.png differ diff --git a/res/javas/bidding.js b/res/javas/bidding.js new file mode 100644 index 0000000..5932e66 --- /dev/null +++ b/res/javas/bidding.js @@ -0,0 +1,43 @@ +var display_bidding = function(element, bidding) { + var popup = $('
'); + popup.css({ + 'position': 'absolute', + 'width': '250px', + 'left': element.offset().left + element.width(), + 'top': element.offset().top + }); + popup.html(bidding); + $('body').append(popup); +} + +var load_bidding = function() { + $('#bidding_popup').remove(); + var elem = $(this); + $.ajax( + { + url: elem.attr('data-bidding-link'), + complete: function(xhr, status) { + if (status == 'success') { + display_bidding(elem, xhr.responseText); + } + else { + display_bidding(elem, 'Brak danych'); + } + } + } + ); + return false; +}; + +var bind_bidding_links = function() { + $('a.biddingLink').each(function() { + $(this).unbind('click').click(load_bidding); + }); + $(document).click(function() { + $('#bidding_popup').remove(); + }); +}; + +$(document).ready(function() { + setInterval(bind_bidding_links, 1000); +}); diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/src/bidding_data.py b/src/bidding_data.py new file mode 100644 index 0000000..ae789f1 --- /dev/null +++ b/src/bidding_data.py @@ -0,0 +1,303 @@ +import sys +import glob +import re +import pypyodbc + +from os import path +from bs4 import BeautifulSoup as bs4 + + +class JFRBidding: + + # alignment of the bidding table + __directions = ['W', 'N', 'E', 'S'] + + # converts BWS lineup data to + # {round}.{sector}_{table}.{pair numbers} structure + def __parse_lineup_data(self, sitting_data): + round_lineups = {} + for sitting in sitting_data[1:]: + round_no = sitting[2] + table_no = str(sitting[0]) + '_' + str(sitting[1]) + if round_no not in round_lineups: + round_lineups[round_no] = {} + round_lineups[round_no][table_no] = sorted([ + sitting[3], sitting[4]]) + return round_lineups + + # converts BWS bidding to the structure: + # {board}_{round}_{sector}_{table}.{sector}_{table}.{round} -> {bidding}[], + # including erased calls + def __parse_bidding_data(self, bidding_data): + bids = {} + for bid in bidding_data[1:]: + round_no = bid[3] + table_no = str(bid[1]) + '_' + str(bid[2]) + board_no = str(bid[4]) + '_' + str(round_no) + '_' + table_no + bid_counter = bid[5] + bid_erased = bid[10] + if board_no not in bids: + bids[board_no] = {} + if table_no not in bids[board_no]: + bids[board_no][table_no] = {} + if round_no not in bids[board_no][table_no]: + bids[board_no][table_no][round_no] = {} + if bid_erased == 1: + if bid_counter in bids[board_no][table_no][round_no]: + if bids[board_no][table_no][round_no][bid_counter][ + 'direction'] == bid[6]: + bids[board_no][table_no][round_no].pop( + bid_counter, None) + if len(bids[board_no][table_no][round_no]) == 0: + bids[board_no][table_no].pop(round_no, None) + else: + bids[board_no][table_no][round_no][bid_counter] = { + 'direction': bid[6], 'bid': bid[7]} + return bids + + # converts bidding data into HTML table + def __format_bidding(self, bidding): + bid_match = re.compile('(\d)([SHDCN])') + html_output = bs4('
', 'lxml') + header_row = html_output.new_tag('tr') + html_output.table.append(header_row) + for direction in self.__directions: + header_cell = html_output.new_tag('th') + header_cell.string = direction + header_row.append(header_cell) + for bid_round in bidding: + bidding_row = html_output.new_tag('tr') + html_output.table.append(bidding_row) + for bid in bid_round: + bid_cell = html_output.new_tag('td') + call_match = re.match(bid_match, bid) + if call_match: + bid_cell.append(call_match.group(1)) + bid_icon = html_output.new_tag( + 'img', src='images/' + call_match.group(2) + '.gif') + bid_cell.append(bid_icon) + else: + bid_cell.append(bid) + bidding_row.append(bid_cell) + return html_output.table.prettify() + + # returns file path for bidding HTML output + # {prefix}_bidding_{jfr_board_number}_{pair_numbers}.txt + def __get_bidding_file_output_path(self, + board_no, + round_no=None, + table_no=None, + pair_numbers=None): + return u'{0}_bidding_{1:03}_{2}.txt'.format( + self.__tournament_prefix, board_no, + '_'.join(map(str, + self.__round_lineups[round_no][table_no] + if pair_numbers is None # read numbers from lineup + else pair_numbers))) # or use provided numbers + + def __map_board_numbers(self): + self.__tournament_files = [ + f for f + in glob.glob(self.__tournament_prefix + '*.html') + if re.search(self.__tournament_files_match, f)] + for round_data in self.__lineup_data: + # 13th column has JFR number for the first board + if len(round_data) > 12: + jfr_number = round_data[12] + round_no = round_data[2] + sector_no = round_data[0] + table_no = round_data[1] + if jfr_number and round_no: + # 5th and 6th - actual board number + for board_number in range(int(round_data[5]), + int(round_data[6])+1): + board_string = '_'.join([ + str(board_number), + str(round_no), + str(sector_no), + str(table_no)]) + self.__board_number_mapping[ + board_string + ] = jfr_number + board_number - round_data[5] + # only include these board numbers from mapping + # which actually exist in JFR output + custom_files = [] + for b_number, jfr_number in self.__board_number_mapping.iteritems(): + board_files = [ + f for f + in self.__tournament_files + if f.endswith('{0:03}.html'.format(jfr_number))] + if len(board_files): + custom_files = custom_files + board_files + else: + self.__board_number_mapping[b_number] = None + self.__tournament_files = custom_files + + # sitting read from BWS + __round_lineups = {} + # bidding read from BWS + __bids = {} + + # full path + JFR prefix + __tournament_prefix = '' + # RegEx matching board HTML files + __tournament_files_match = None + # matched files, including board number mapping boundaries + __tournament_files = [] + + # BWS number -> JFR number mapping + __board_number_mapping = {} + + def __init__(self, bws_file, file_prefix): + with pypyodbc.win_connect_mdb(bws_file) as connection: + cursor = connection.cursor() + self.__lineup_data = cursor.execute( + 'SELECT * FROM RoundData').fetchall() + bid_data = cursor.execute('SELECT * FROM BiddingData').fetchall() + self.__round_lineups = self.__parse_lineup_data(self.__lineup_data) + self.__bids = self.__parse_bidding_data(bid_data) + self.__tournament_prefix = path.splitext( + path.realpath(file_prefix))[0] + self.__tournament_files_match = re.compile( + re.escape(self.__tournament_prefix) + '([0-9]{3})\.html') + self.__map_board_numbers() + + def write_bidding_tables(self): + for board_no, board_data in self.__bids.items(): + if board_no in self.__board_number_mapping: + for table_no, table_data in board_data.items(): + for round_no, round_data in table_data.items(): + if round_no in self.__round_lineups: + if table_no in self.__round_lineups[round_no]: + bidding = sorted(round_data) + dealer = round_data[bidding[0]]['direction'] + bidding_table = [[], [], [], []] + # compile bidding player-by-player + for bid_index in bidding: + bid = round_data[bid_index] + bidding_table[ + self.__directions.index( + bid['direction']) + ].append(bid['bid']) + last_bidder = bid['direction'] + # fill skipped calls for players before dealer + # in the first round of bidding + for pos in range( + 0, self.__directions.index(dealer)): + bidding_table[pos].insert(0, '') + # fill skipped calls for players after pass out + # (so that bidding table is a proper matrix) + for pos in range( + self.__directions.index(last_bidder), + len(self.__directions)): + bidding_table[pos].append('') + # transpose the bidding table + # aligning it row-by-row (bid round-by-round) + bidding_table = map(list, zip(*bidding_table)) + bidding_fpath = \ + self.__get_bidding_file_output_path( + self.__board_number_mapping[board_no], + round_no, table_no) + with file(bidding_fpath, 'w') as bidding_file: + bidding_file.write( + self.__format_bidding(bidding_table)) + + def write_bidding_scripts(self): + for tournament_file in self.__tournament_files: + with file(tournament_file, 'r+') as board_html: + board_content = bs4(board_html, 'lxml', from_encoding='utf-8') + header_scripts = board_content.select('head script') + # check for jQuery, append if necessary + jquery_scripts = [script for script in header_scripts + if script['src'] == 'javas/jquery.js'] + if not len(jquery_scripts): + jquery = board_content.new_tag( + 'script', src='javas/jquery.js', + type='text/javascript') + jquery_scripts.append(jquery) + board_content.head.append(jquery) + # check for bidding.js + bidding_scripts = [ + script for script in header_scripts + if script['src'] == 'javas/bidding.js'] + # and make sure bidding.js is appended after jQuery + for script in bidding_scripts: + script.extract() + bidding_script = board_content.new_tag( + 'script', src='javas/bidding.js', + type='text/javascript') + jquery_scripts[0].insert_after(bidding_script) + board_html.seek(0) + board_html.write(board_content.prettify( + 'utf-8', formatter='html')) + board_html.truncate() + + def write_bidding_links(self): + for tournament_file in self.__tournament_files: + file_number = re.match( + self.__tournament_files_match, + tournament_file).group(1) + board_text_path = path.splitext(tournament_file)[0] + '.txt' + with file(board_text_path, 'r+') as board_text: + board_text_content = bs4( + board_text, 'lxml', from_encoding='iso-8859-2') + for row in board_text_content.select('tr'): + cells = row.select('td') + # traveller table rows for specific score entries + # should have 11 cells + if len(cells) == 11: + try: + pair_numbers = sorted([ + int(cells[1].contents[0]), + int(cells[2].contents[0])]) + except ValueError: + continue + bidding_link = board_text_content.new_tag( + 'a', href='#', **{'class': 'biddingLink'}) + bidding_link.string = ' ' + bidding_link['data-bidding-link'] = path.basename( + self.__get_bidding_file_output_path( + int(file_number, 10), + pair_numbers=pair_numbers)) + # only append link if we've got bidding data + if path.isfile(path.join( + path.dirname(self.__tournament_prefix), + bidding_link['data-bidding-link'])): + # fourth cell is the contract + for link in cells[3].select('a.biddingLink'): + link.extract() + cells[3].append(bidding_link) + board_text.seek(0) + board_text.write(board_text_content.table.prettify( + 'iso-8859-2', formatter='html')) + board_text.truncate() + +if __name__ == '__main__': + import argparse + + argument_parser = argparse.ArgumentParser( + description='Display bidding data from BWS files on JFR Pary pages') + + def file_path(filepath): + filepath = unicode(filepath, sys.getfilesystemencoding()) + if path.isfile(filepath): + return filepath + else: + argument_parser.error('File %s does not exist' % filepath) + + argument_parser.add_argument('bws_file', metavar='BWS_FILE', + help='path to BWS file', + type=file_path) + argument_parser.add_argument('path', metavar='PATH', + help='tournament path (to PREFIX.html)', + type=file_path) + + arguments = argument_parser.parse_args() + + bidding_parser = JFRBidding( + bws_file=arguments.bws_file, + file_prefix=arguments.path, + ) + bidding_parser.write_bidding_tables() + bidding_parser.write_bidding_scripts() + bidding_parser.write_bidding_links() diff --git a/src/icon.ico b/src/icon.ico new file mode 100644 index 0000000..ffc9601 Binary files /dev/null and b/src/icon.ico differ diff --git a/src/icon.xcf b/src/icon.xcf new file mode 100644 index 0000000..b625556 Binary files /dev/null and b/src/icon.xcf differ diff --git a/src/version b/src/version new file mode 100644 index 0000000..03aad11 --- /dev/null +++ b/src/version @@ -0,0 +1,39 @@ +# UTF-8 +VSVersionInfo( + ffi=FixedFileInfo( + filevers=(1, 0, 0, 0), + prodvers=(1, 0, 0, 0), + # Contains a bitmask that specifies the valid bits 'flags' + mask=0x3f, + # Contains a bitmask that specifies the Boolean attributes of the file. + flags=0x0, + # The operating system for which this file was designed. + # 0x4 - NT and there is no need to change it. + OS=0x4, + # The general type of file. + # 0x1 - the file is an application. + fileType=0x1, + # The function of the file. + # 0x0 - the function is not defined for this fileType + subtype=0x0, + # Creation date and time stamp. + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904b0', # 0x0409 = 1033 = English, 0x04b0 = 1200 = UTF-8 + [StringStruct(u'CompanyName', u'emkael.info'), + StringStruct(u'ProductName', u'bidding_data'), + StringStruct(u'ProductVersion', u'1, 0, 0, 0'), + StringStruct(u'InternalName', u'bidding_data'), + StringStruct(u'OriginalFilename', u'bidding_data.exe'), + StringStruct(u'FileVersion', u'1, 0, 0, 0'), + StringStruct(u'FileDescription', u'Bidding data display for JFR Pary result pages'), + StringStruct(u'LegalCopyright', u'© 2015 mkl (Michał Klichowicz)'), + StringStruct(u'LegalTrademarks', u''),]) + ]), + VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) -- cgit v1.2.3