diff options
author | Michał Klichowicz <emkael@tlen.pl> | 2018-10-17 20:02:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-17 20:02:57 +0200 |
commit | 984540cd2dfba29c1dc9cbc43ab6fa4c85c7727b (patch) | |
tree | d03e49f564cec7c3eec7ce7c98fae530ec1544e0 | |
parent | 9a5f06ee9cddd38e11f49a2f934de202d34e63e2 (diff) | |
parent | 31fc51ce22e7c5197ed367cadd14d8a258f8fd65 (diff) |
Merge pull request #28 from emkael/develv1.2.0
v1.2
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .version | 8 | ||||
-rw-r--r-- | CONFIG.md | 20 | ||||
-rw-r--r-- | jfr_playoff/data.py | 69 | ||||
-rw-r--r-- | jfr_playoff/db.py | 9 | ||||
-rw-r--r-- | jfr_playoff/dto.py | 27 | ||||
-rw-r--r-- | jfr_playoff/filemanager.py | 23 | ||||
-rw-r--r-- | jfr_playoff/generator.py | 126 | ||||
-rw-r--r-- | jfr_playoff/i18n.py | 38 | ||||
-rw-r--r-- | jfr_playoff/matchinfo.py | 185 | ||||
-rw-r--r-- | jfr_playoff/remote.py | 6 | ||||
-rw-r--r-- | jfr_playoff/settings.py | 16 | ||||
-rw-r--r-- | jfr_playoff/template.py | 369 | ||||
-rw-r--r-- | jfr_playoff/tournamentinfo.py | 100 | ||||
-rw-r--r-- | playoff.py | 2 |
15 files changed, 694 insertions, 305 deletions
@@ -3,3 +3,4 @@ playoff-*.zip build/* dist/* +output/* @@ -1,8 +1,8 @@ # UTF-8 VSVersionInfo( ffi=FixedFileInfo( - filevers=(1, 1, 2, 0), - prodvers=(1, 1, 2, 0), + filevers=(1, 2, 0, 0), + prodvers=(1, 2, 0, 0), # Contains a bitmask that specifies the valid bits 'flags' mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -26,10 +26,10 @@ VSVersionInfo( u'040904b0', # 0x0409(1033)= English, 0x04b0(1200)= UTF-8 [StringStruct(u'CompanyName', u'emkael.info'), StringStruct(u'ProductName', u'teamy_playoff'), - StringStruct(u'ProductVersion', u'1, 1, 2, 0'), + StringStruct(u'ProductVersion', u'1, 2, 0, 0'), StringStruct(u'InternalName', u'teamy_playoff'), StringStruct(u'OriginalFilename', u'playoff.exe'), - StringStruct(u'FileVersion', u'1, 1, 2, 0'), + StringStruct(u'FileVersion', u'1, 2, 0, 0'), StringStruct( u'FileDescription', u'Play-off visualisation for JFR Teamy events'), @@ -25,6 +25,7 @@ Konfiguracja składa się, po kolei, z: + `"refresh"` - parametr odświeżania strony drabinki: `0` = wyłączone, liczba naturalna = interwał odświeżania, w sekundach + `"width"` i `"height"` - wymiary (w pikselach) miejsca rezerwowanego dla każdego meczu w widoku drabinki (`"width"` bezpośrednio wpływa na rozmieszczanie kolumn, wewnątrz każdej z kolumn mecze rozmieszczane są równomiernie, w zależnie od ich liczby) + `"margin"` - odstęp między w/w miejscem (minimalny - jak widać, w przypadku mniejszej liczby meczów w fazie, odstępy się dopasują) + + `"label_length_limit"` - maksymalna liczba znaków wyświetlanych jako skrócona nazwa drużyn(y) w schemacie (`0` lub brak wartości oznacza brak limitu) - sekcji `"canvas"`: ustawień rysowania linii + `"winner_h_offset"`, `"winner_v_offset"` - marginesy (poziomy i pionowy) rysowania linii zwycięzców (odpowiednio: pionowych i poziomych, względem środka obszaru) + `"loser_h_offset"`, `"loser_v_offset"` - analogiczne marginesy rysowania linii przegranych @@ -36,6 +37,25 @@ Ustawienia bazy danych nie są wymagane, program potrafi sobie poradzić bez nic Ustawienia Gońca nie są wymagane, domyślnie jest on wyłączony, a przy włączeniu - domyślnie komunikuje się z `localhost` na porcie `8090`. +Tłumaczenia tekstów +------------------- + +Program obsługuje możliwość ustawienia własnych łańcuchów znaków wyświetlanych w różnych miejscach wynikowej strony. Teksty te opcjonalnie określa sekcja `"i18n"` pliku konfiguracyjnego. + +Domyślna postać wszystkich obsługiwanych łańcuchów to: + +``` +{ + "SCORE": "wynik", + "FINAL_STANDINGS": "klasyfikacja końcowa", + "STANDINGS_PLACE": "miejsce", + "STANDINGS_TEAM": "drużyna", + "STANDINGS_CAPTIONS": "legenda", + "FOOTER_GENERATED": "strona wygenerowana", + "SWISS_DEFAULT_LABEL": "Turniej o %d. miejsce" +} +``` + Zdalne pliki konfiguracyjne --------------------------- diff --git a/jfr_playoff/data.py b/jfr_playoff/data.py index 78ef1e7..6adf00c 100644 --- a/jfr_playoff/data.py +++ b/jfr_playoff/data.py @@ -4,13 +4,14 @@ from jfr_playoff.db import PlayoffDB from jfr_playoff.dto import Phase from jfr_playoff.matchinfo import MatchInfo from jfr_playoff.tournamentinfo import TournamentInfo +from jfr_playoff.logger import PlayoffLogger class PlayoffData(object): def __init__(self, settings): self.database = PlayoffDB(settings.get('database')) if settings.has_section('database') else None if self.database is None: - print PlayoffDB.DATABASE_NOT_CONFIGURED_WARNING + PlayoffLogger.get('db').warning(PlayoffDB.DATABASE_NOT_CONFIGURED_WARNING) self.team_settings = settings.get('teams') self.phases = settings.get('phases') self.swiss = [] @@ -23,27 +24,31 @@ class PlayoffData(object): @cached_property def teams(self): if isinstance(self.team_settings, list): + PlayoffLogger.get('data').info( + 'team list pre-defined: %s', self.team_settings) return self.team_settings tournament_info = TournamentInfo(self.team_settings, self.database) - return tournament_info.get_tournament_results() + team_list = tournament_info.get_tournament_results() + if len(team_list) == 0: + PlayoffLogger.get('data').warning('team list is empty!') + return team_list def generate_phases(self): self.grid = [] for phase in self.phases: - phase_count = len(phase['matches']) - if 'dummies' in phase: - phase_count += len(phase['dummies']) + dummies = phase.get('dummies', []) + phase_count = len(phase['matches']) + len(dummies) phase_object = Phase() phase_object.title = phase['title'] - phase_object.link = phase['link'] if 'link' in phase else None + phase_object.link = phase.get('link', None) phase_object.matches = [None] * phase_count phase_pos = 0 for match in phase['matches']: - if 'dummies' in phase: - while phase_pos in phase['dummies']: - phase_pos += 1 + while phase_pos in dummies: + phase_pos += 1 phase_object.matches[phase_pos] = match['id'] phase_pos += 1 + PlayoffLogger.get('data').info('phase object: %s', phase_object) self.grid.append(phase_object) return self.grid @@ -59,14 +64,16 @@ class PlayoffData(object): for phase_obj in self.grid: if match['id'] in phase_obj.matches: phase_obj.running = True + PlayoffLogger.get('data').info( + 'match object: %s', self.match_info[match['id']]) return self.match_info def get_swiss_link(self, event): event_info = TournamentInfo(event, self.database) swiss_link = event_info.get_results_link() - if ('relative_path' in event) and ( - event['relative_path'] is not None): + if event.get('relative_path', None): swiss_link = '%s/%s' % (event['relative_path'], swiss_link) + PlayoffLogger.get('data').info('swiss link: %s', swiss_link) return swiss_link def prefill_leaderboard(self, teams): @@ -75,6 +82,7 @@ class PlayoffData(object): if len(team) > 3: self.leaderboard[team[3]-1] = team[0] self.fill_swiss_leaderboard(self.swiss, teams) + PlayoffLogger.get('data').info('leaderboard pre-filled: %s', self.leaderboard) return self.leaderboard def fill_swiss_leaderboard(self, swiss, teams): @@ -83,16 +91,8 @@ class PlayoffData(object): event['ties'] = teams event_info = TournamentInfo(event, self.database) if event_info.is_finished(): - swiss_position = ( - event['swiss_position'] - if 'swiss_position' in event - else 1 - ) - position_limit = ( - event['position_to'] - if 'position_to' in event - else 9999 - ) + swiss_position = event.get('swiss_position', 1) + position_limit = event.get('position_to', 9999) place = 1 swiss_results = event_info.get_tournament_results() for team in swiss_results: @@ -104,6 +104,8 @@ class PlayoffData(object): self.leaderboard[ target_position - 1] = team[0] place += 1 + PlayoffLogger.get('data').info( + 'leaderboard after %s swiss: %s', event, self.leaderboard) def fill_leaderboard(self): self.prefill_leaderboard(self.teams) @@ -124,30 +126,41 @@ class PlayoffData(object): self.match_info[match['id']].loser) for positions, position_teams in leaderboard_teams.iteritems(): positions = list(positions) + PlayoffLogger.get('data').info( + 'filling leaderboard positions %s with teams %s', + positions, position_teams) if len(positions) == len([ team for team in position_teams if team is not None]): for table_team in self.teams: if table_team[0] in position_teams: position = positions.pop(0) self.leaderboard[position-1] = table_team[0] + PlayoffLogger.get('data').info( + 'team %s in position %d', table_team[0], position) + PlayoffLogger.get('data').info( + 'leaderboard filled: %s', self.leaderboard) return self.leaderboard def get_swiss_info(self): - return [{ + swiss_info = [{ 'link': self.get_swiss_link(event), 'position': event['position'], - 'label': event['label'] if 'label' in event else None, + 'label': event.get('label', None), 'finished': TournamentInfo(event, self.database).is_finished() } for event in self.swiss] + PlayoffLogger.get('data').info('swiss info: %s', swiss_info) + return swiss_info def get_dimensions(self): - return ( + dimensions = ( len(self.phases), max([ - len(phase['matches']) + len(phase['dummies']) - if 'dummies' in phase - else len(phase['matches']) - for phase in self.phases])) + len(phase['matches']) + len(phase.get('dummies', [])) + for phase in self.phases + ]) + ) + PlayoffLogger.get('data').info('grid dimensions: %s', dimensions) + return dimensions def get_shortname(self, fullname): for team in self.teams: diff --git a/jfr_playoff/db.py b/jfr_playoff/db.py index 71f3dcb..bca05d2 100644 --- a/jfr_playoff/db.py +++ b/jfr_playoff/db.py @@ -1,9 +1,12 @@ import sys +from jfr_playoff.logger import PlayoffLogger + + class PlayoffDB(object): db_cursor = None - DATABASE_NOT_CONFIGURED_WARNING = 'WARNING: database not configured' + DATABASE_NOT_CONFIGURED_WARNING = 'database not configured' def __init__(self, settings): reload(sys) @@ -14,12 +17,14 @@ class PlayoffDB(object): password=settings['pass'], host=settings['host'], port=settings['port']) + PlayoffLogger.get('db').info('db settings: %s', settings) self.db_cursor = self.database.cursor(buffered=True) def get_cursor(self): return self.db_cursor def __execute_query(self, db_name, sql, params): + PlayoffLogger.get('db').info('query (%s): %s %s', db_name, sql.replace('\n', ' '), params) self.db_cursor.execute(sql.replace('#db#', db_name), params) def fetch(self, db_name, sql, params): @@ -29,6 +34,7 @@ class PlayoffDB(object): row = self.db_cursor.fetchone() return row except mysql.connector.Error as e: + PlayoffLogger.get('db').error(str(e)) raise IOError(e.errno, str(e), db_name) def fetch_all(self, db_name, sql, params): @@ -38,6 +44,7 @@ class PlayoffDB(object): results = self.db_cursor.fetchall() return results except mysql.connector.Error as e: + PlayoffLogger.get('db').error(str(e)) raise IOError( message=str(e), filename=db_name, errno=e.errno, strerror=str(e)) diff --git a/jfr_playoff/dto.py b/jfr_playoff/dto.py index f5e08ef..a88cd2b 100644 --- a/jfr_playoff/dto.py +++ b/jfr_playoff/dto.py @@ -1,7 +1,22 @@ +import sys + +def coalesce(*arg): + for el in arg: + if el is not None: + return el + return None + + class Team(object): name = '' score = 0.0 + def __unicode__(self): + return u'%s (%.1f)' % (coalesce(self.name, '<None>'), self.score) + + def __repr__(self): + return unicode(self).encode(sys.stdin.encoding) + class Match(object): id = None @@ -13,6 +28,13 @@ class Match(object): winner_matches = None loser_matches = None + def __repr__(self): + return (u'#%d (%s) %s [%s]' % ( + self.id, coalesce(self.link, '<None>'), [unicode(team) for team in self.teams], + u'finished' if self.running < 0 else ( + u'%d boards' % self.running)) + ).encode(sys.stdin.encoding) + class Phase(object): title = None @@ -20,4 +42,9 @@ class Phase(object): matches = [] running = False + def __repr__(self): + return u'%s (%s) <%d matches> [%srunning]' % ( + self.title, coalesce(self.link, '<None>'), + len(self.matches), '' if self.running else 'not ') + __all__ = ('Team', 'Match', 'Phase') diff --git a/jfr_playoff/filemanager.py b/jfr_playoff/filemanager.py index 97f9cb8..515845e 100644 --- a/jfr_playoff/filemanager.py +++ b/jfr_playoff/filemanager.py @@ -2,6 +2,8 @@ import os import shutil import socket +from jfr_playoff.logger import PlayoffLogger + import __main__ @@ -9,12 +11,15 @@ class PlayoffFileManager(object): def __init__(self, settings): self.goniec = settings.get('goniec') if settings.has_section('goniec') else None + PlayoffLogger.get('filemanager').info('goniec settings: %s', self.goniec) self.output_file = settings.get('output') + PlayoffLogger.get('filemanager').info('output file: %s', self.output_file) self.output_path = os.path.dirname( self.output_file ).strip(os.sep) if len(self.output_path) > 0: self.output_path += os.sep + PlayoffLogger.get('filemanager').info('output path: %s', self.output_path) self.files = set() def reset(self): @@ -22,14 +27,24 @@ class PlayoffFileManager(object): def register_file(self, path): if path.startswith(self.output_path): + PlayoffLogger.get('filemanager').info('registering file: %s', path) self.files.add(path.replace(self.output_path, '')) + else: + PlayoffLogger.get('filemanager').info( + 'file: %s outside of %s, not registering', path, self.output_path) def write_content(self, content): output_dir = os.path.dirname(self.output_file) if len(output_dir) > 0: if not os.path.exists(output_dir): + PlayoffLogger.get('filemanager').info( + 'output directory %s does not exist, creating', + output_dir) os.makedirs(output_dir) output = open(self.output_file, 'w') + PlayoffLogger.get('filemanager').info( + 'writing %d bytes into file %s', + len(content), self.output_file) output.write(content.encode('utf8')) output.close() self.register_file(self.output_file) @@ -40,7 +55,12 @@ class PlayoffFileManager(object): script_output_dir = os.path.dirname(script_output_path) if len(script_output_dir) > 0: if not os.path.exists(script_output_dir): + PlayoffLogger.get('filemanager').info( + 'output directory %s does not exist, creating', + script_output_dir) os.makedirs(script_output_dir) + PlayoffLogger.get('filemanager').info( + 'copying JS to %s', script_output_path) shutil.copy( unicode(os.path.join( os.path.dirname(__main__.__file__), 'playoff.js')), @@ -58,7 +78,8 @@ class PlayoffFileManager(object): content_lines = [self.output_path] + \ list(self.files) + \ ['bye', ''] - print '\n'.join(content_lines) + PlayoffLogger.get('goniec').info( + '\n'.join(content_lines)) goniec = socket.socket() goniec.connect((self.goniec['host'], self.goniec['port'])) goniec.sendall('\n'.join(content_lines)) diff --git a/jfr_playoff/generator.py b/jfr_playoff/generator.py index 2b5aaf0..d358d86 100644 --- a/jfr_playoff/generator.py +++ b/jfr_playoff/generator.py @@ -1,19 +1,28 @@ from datetime import datetime -import jfr_playoff.template as p_temp +from jfr_playoff.template import PlayoffTemplate from jfr_playoff.data import PlayoffData +from jfr_playoff.logger import PlayoffLogger class PlayoffGenerator(object): def __init__(self, settings): self.data = PlayoffData(settings) self.page = settings.get('page') + PlayoffLogger.get('generator').info( + 'page settings: %s', self.page) self.canvas = {} if settings.has_section('canvas'): self.canvas = settings.get('canvas') + PlayoffLogger.get('generator').info( + 'canvas settings: %s', self.canvas) self.leaderboard_classes = {} if settings.has_section('position_styles'): self.leaderboard_classes = settings.get('position_styles') + PlayoffLogger.get('generator').info( + 'leaderboard classes settings: %s', self.leaderboard_classes) + self.p_temp = PlayoffTemplate( + settings.get('i18n') if settings.has_section('i18n') else {}) def generate_content(self): match_grid = self.get_match_grid( @@ -21,67 +30,87 @@ class PlayoffGenerator(object): self.data.generate_phases(), self.data.fill_match_info()) leaderboard_table = self.get_leaderboard_table() - return p_temp.PAGE % ( - p_temp.PAGE_HEAD % ( - p_temp.PAGE_HEAD_REFRESH % ( - self.page['refresh']) + return self.p_temp.get( + 'PAGE', + self.p_temp.get( + 'PAGE_HEAD', + self.p_temp.get( + 'PAGE_HEAD_REFRESH', + self.page['refresh']) \ if self.page['refresh'] > 0 else '', self.page['title']), - p_temp.PAGE_BODY % ( + self.p_temp.get( + 'PAGE_BODY', self.page['logoh'], match_grid, self.get_swiss_links(), leaderboard_table, self.get_leaderboard_caption_table() if leaderboard_table else '', - p_temp.PAGE_BODY_FOOTER.decode('utf8') % ( + self.p_temp.get( + 'PAGE_BODY_FOOTER', datetime.now().strftime('%Y-%m-%d o %H:%M:%S')))) def get_match_table(self, match): rows = '' for team in match.teams: - score_html = p_temp.MATCH_SCORE % (team.score) + score_html = self.p_temp.get('MATCH_SCORE', team.score) team_label = ' / '.join([ self.data.get_shortname(name) for name in team.name.split('<br />')]) - team_html = p_temp.MATCH_TEAM_LINK % ( - match.link, team.name, team_label) if match.link is not None \ - else p_temp.MATCH_TEAM_NON_LINK % ( - team.name, team_label) - rows += p_temp.MATCH_TEAM_ROW % ( + label_max_length = self.page.get('label_length_limit', 0) + if label_max_length: + team_label = team_label[:label_max_length] + (team_label[label_max_length:] and '(...)') + team_html = self.p_temp.get( + 'MATCH_TEAM_LINK', + match.link, team.name, team_label) \ + if match.link is not None \ + else self.p_temp.get( + 'MATCH_TEAM_NON_LINK', + team.name, team_label) + rows += self.p_temp.get( + 'MATCH_TEAM_ROW', ' '.join([ 'winner' if team.name == match.winner else '', 'loser' if team.name == match.loser else '' ]).strip(), team_html, - p_temp.MATCH_LINK % (match.link, score_html) if match.link is not None else score_html) - html = p_temp.MATCH_TABLE.decode('utf8') % ( - int(self.page['width'] * 0.75), - int(self.page['width'] * 0.25), + self.p_temp.get( + 'MATCH_LINK', + match.link, score_html) \ + if match.link is not None else score_html) + html = self.p_temp.get( + 'MATCH_TABLE', + int(self.page['width'] * 0.7), + int(self.page['width'] * 0.2), rows) if match.running > 0: - running_html = p_temp.MATCH_RUNNING % (match.running) - html += p_temp.MATCH_LINK % (match.link, running_html) if match.link is not None else running_html + running_html = self.p_temp.get('MATCH_RUNNING', match.running) + html += self.p_temp.get('MATCH_LINK', match.link, running_html) if match.link is not None else running_html + PlayoffLogger.get('generator').info( + 'match table for #%d generated: %d bytes', match.id, len(html)) return html def get_phase_header(self, phase, position): - if phase.running: - grid_header = p_temp.MATCH_GRID_PHASE_RUNNING - else: - grid_header = p_temp.MATCH_GRID_PHASE - grid_header = grid_header % (phase.title) + grid_header = self.p_temp.get( + 'MATCH_GRID_PHASE_RUNNING' if phase.running \ + else 'MATCH_GRID_PHASE', + phase.title) if phase.link is not None: - return p_temp.MATCH_GRID_PHASE_LINK % ( + return self.p_temp.get( + 'MATCH_GRID_PHASE_LINK', phase.link, self.page['width'], position, grid_header) else: - return p_temp.MATCH_GRID_PHASE_NON_LINK % ( + return self.p_temp.get( + 'MATCH_GRID_PHASE_NON_LINK', self.page['width'], position, grid_header) def get_match_box(self, match, position): if match is not None: - return p_temp.MATCH_BOX % ( + return self.p_temp.get( + 'MATCH_BOX', position[0], position[1], match.id, ' '.join([ @@ -101,6 +130,8 @@ class PlayoffGenerator(object): dimensions[1] * ( self.page['height'] + self.page['margin'] ) - self.page['margin']) + PlayoffLogger.get('generator').info( + 'canvas size: %s', canvas_size) grid_boxes = '' col_no = 0 for phase in grid: @@ -109,14 +140,19 @@ class PlayoffGenerator(object): match_height = canvas_size[1] / len(phase.matches) row_no = 0 for match in phase.matches: - grid_y = int(row_no * match_height + + grid_y = self.page['margin'] / 2 if dimensions[1] == 1 else \ + int(row_no * match_height + 0.5 * (match_height - self.page['height'])) + PlayoffLogger.get('generator').info( + 'grid box (%d, %d) position: (%d, %d)', + col_no, row_no, grid_x, grid_y) grid_boxes += self.get_match_box( matches[match] if match is not None else None, (grid_x, grid_y)) row_no += 1 col_no += 1 - return p_temp.MATCH_GRID % ( + return self.p_temp.get( + 'MATCH_GRID', canvas_size[0], canvas_size[1], canvas_size[0], canvas_size[1], ' '.join(['data-%s="%s"' % ( @@ -136,9 +172,10 @@ class PlayoffGenerator(object): rows = '' for style in self.leaderboard_classes: if 'caption' in style: - rows += p_temp.LEADERBOARD_CAPTION_TABLE_ROW % ( + rows += self.p_temp.get( + 'LEADERBOARD_CAPTION_TABLE_ROW', style['class'], style['caption']) - return p_temp.LEADERBOARD_CAPTION_TABLE % (rows) if rows else '' + return self.p_temp.get('LEADERBOARD_CAPTION_TABLE', rows) if rows else '' def get_leaderboard_table(self): leaderboard = self.data.fill_leaderboard() @@ -147,24 +184,33 @@ class PlayoffGenerator(object): position = 1 rows = '' for team in leaderboard: - rows += p_temp.LEADERBOARD_ROW % ( + rows += self.p_temp.get( + 'LEADERBOARD_ROW', self.get_leaderboard_row_class(position), position, self.get_flag(team), team or '') position += 1 - html = p_temp.LEADERBOARD.decode('utf8') % (rows) + html = self.p_temp.get('LEADERBOARD', rows) + PlayoffLogger.get('generator').info( + 'leaderboard HTML generated: %d bytes', len(html)) return html def get_swiss_links(self): info = [] for event in self.data.get_swiss_info(): - event_label = p_temp.SWISS_DEFAULT_LABEL % (event['position']) - if 'label' in event and event['label'] is not None: + event_label = self.p_temp.get('SWISS_DEFAULT_LABEL', event['position']) + if event.get('label', None): event_label = event['label'] - info.append((p_temp.SWISS_LINK if event['finished'] else p_temp.SWISS_RUNNING_LINK) % ( - event['link'], event_label - )) - return '\n'.join(info) + info.append((self.p_temp.get('SWISS_LINK', + event['link'], event_label) \ + if event['finished'] \ + else self.p_temp.get( + 'SWISS_RUNNING_LINK', + event['link'], event_label))) + html = '\n'.join(info) + PlayoffLogger.get('generator').info( + 'swiss HTML generated: %d bytes', len(html)) + return html def get_flag(self, team): flag = self.data.get_team_image(team) - return '' if flag is None else p_temp.LEADERBOARD_ROW_FLAG % (flag) + return '' if flag is None else self.p_temp.get('LEADERBOARD_ROW_FLAG', flag) diff --git a/jfr_playoff/i18n.py b/jfr_playoff/i18n.py new file mode 100644 index 0000000..c4134a9 --- /dev/null +++ b/jfr_playoff/i18n.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +import re + +from jfr_playoff.logger import PlayoffLogger + +PLAYOFF_I18N_DEFAULTS = { + 'SCORE': 'wynik', + 'FINAL_STANDINGS': 'klasyfikacja końcowa', + 'STANDINGS_PLACE': 'miejsce', + 'STANDINGS_TEAM': 'drużyna', + 'STANDINGS_CAPTIONS': 'legenda', + 'FOOTER_GENERATED': 'strona wygenerowana', + 'SWISS_DEFAULT_LABEL': 'Turniej o %d. miejsce' +} + +class PlayoffI18N(object): + + def __init__(self, settings): + self.settings = settings + self.string_match = re.compile(r'{{(.*)}}') + + def localize(self, string): + return re.sub( + self.string_match, + lambda x: self.__get_translation(x.group(1)), + string) + + def __get_translation(self, string): + for dictionary in [self.settings, PLAYOFF_I18N_DEFAULTS]: + if string in dictionary: + translation = dictionary[string].decode('utf8') + PlayoffLogger.get('i18n').info( + 'translation for %s: %s', string, translation) + return translation + PlayoffLogger.get('i18n').info( + 'translation for %s not found', string) + return '{{%s}}' % (string) diff --git a/jfr_playoff/matchinfo.py b/jfr_playoff/matchinfo.py index 5924564..ab742d4 100644 --- a/jfr_playoff/matchinfo.py +++ b/jfr_playoff/matchinfo.py @@ -5,6 +5,7 @@ import jfr_playoff.sql as p_sql from jfr_playoff.dto import Match, Team from jfr_playoff.remote import RemoteUrl as p_remote from jfr_playoff.tournamentinfo import TournamentInfo +from jfr_playoff.logger import PlayoffLogger class MatchInfo: @@ -36,10 +37,15 @@ class MatchInfo: def __fetch_match_link(self): if 'link' in self.config: self.info.link = self.config['link'] + PlayoffLogger.get('matchinfo').info( + 'match #%d link pre-defined: %s', self.info.id, self.info.link) elif ('round' in self.config) and ('database' in self.config): event_info = TournamentInfo(self.config, self.database) self.info.link = event_info.get_results_link( 'runda%d.html' % (self.config['round'])) + PlayoffLogger.get('matchinfo').info( + 'match #%d link fetched: %s', self.info.id, self.info.link) + PlayoffLogger.get('matchinfo').info('match #%d link empty', self.info.id) def __get_predefined_scores(self): teams = [Team(), Team()] @@ -62,6 +68,9 @@ class MatchInfo: if i == 2: break scores_fetched = True + PlayoffLogger.get('matchinfo').info( + 'pre-defined scores for match #%d: %s', + self.info.id, teams) return scores_fetched, teams_fetched, teams def __get_db_teams(self, teams, fetch_scores): @@ -77,6 +86,8 @@ class MatchInfo: teams[0].score += row[2] else: teams[1].score -= row[2] + PlayoffLogger.get('matchinfo').info( + 'db scores for match #%d: %s', self.info.id, teams) return teams def __find_table_row(self, url): @@ -84,7 +95,13 @@ class MatchInfo: for row in html_content.select('tr tr'): for cell in row.select('td.t1'): if cell.text.strip() == str(self.config['table']): + PlayoffLogger.get('matchinfo.html').debug( + 'HTML row for table %d found: %s', + self.config['table'], row) return row + PlayoffLogger.get('matchinfo.html').debug( + 'HTML row for table %d not found', + self.config['table']) return None def __get_html_teams(self, teams, fetch_score): @@ -93,17 +110,48 @@ class MatchInfo: row = self.__find_table_row(self.info.link) if row is None: raise ValueError('table row not found') - score_cell = row.select('td.bdc')[-1] - scores = [ - float(text) for text - in score_cell.contents - if isinstance(text, unicode)] + try: + scores = [ + float(text) for text + in row.select('td.bdc')[-1].contents + if isinstance(text, unicode)] + except ValueError: + # single-segment match + try: + # running single-segment + scores = [ + float(text.strip()) for text + in row.select('td.bdcg a')[-1].contents + if isinstance(text, unicode)] + except IndexError: + try: + # static single-segment + scores = [ + float(text.strip()) for text + in row.select('td.bdc a')[-1].contents + if isinstance(text, unicode)] + except IndexError: + # toweled single-segment + scores = [0.0, 0.0] + # carry-over + carry_over = [ + float(text.strip()) if len(text.strip()) > 0 else 0.0 for text + in row.select('td.bdc')[0].contents + if isinstance(text, unicode)] + if len(carry_over) < 2: + # no carry-over, possibly no carry-over cells or empty + carry_over = [0.0, 0.0] + for i in range(0, 2): + scores[i] += carry_over[i] team_names = [[text for text in link.contents if isinstance(text, unicode)][0].strip(u'\xa0') for link in row.select('a[onmouseover]')] for i in range(0, 2): teams[i].name = team_names[i] teams[i].score = scores[i] + PlayoffLogger.get('matchinfo').info( + 'HTML scores for match #%d: %s', + self.info.id, teams) return teams def __get_config_teams(self, teams): @@ -133,15 +181,17 @@ class MatchInfo: else '??' for team in match_teams]) else: teams[i].name = '' + PlayoffLogger.get('matchinfo').info( + 'config scores for match #%d: %s', + self.info.id, teams) return teams def __fetch_teams_with_scores(self): (scores_fetched, teams_fetched, self.info.teams) = self.__get_predefined_scores() if scores_fetched: - if 'running' in self.config: - self.info.running = int(self.config['running']) - else: - self.info.running = -1 + PlayoffLogger.get('matchinfo').info( + 'pre-defined scores for match #%d fetched', self.info.id) + self.info.running = int(self.config.get('running', -1)) if not teams_fetched: try: try: @@ -151,10 +201,16 @@ class MatchInfo: raise KeyError('database not configured') self.info.teams = self.__get_db_teams( self.info.teams, not scores_fetched) - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('matchinfo').warning( + 'fetching DB scores for match #%d failed: %s(%s)', + self.info.id, type(e).__name__, str(e)) self.info.teams = self.__get_html_teams( self.info.teams, not scores_fetched) - except (TypeError, IndexError, KeyError, IOError, ValueError): + except (TypeError, IndexError, KeyError, IOError, ValueError) as e: + PlayoffLogger.get('matchinfo').warning( + 'fetching HTML scores for match #%d failed: %s(%s)', + self.info.id, type(e).__name__, str(e)) self.info.teams = self.__get_config_teams(self.info.teams) def __get_db_board_count(self): @@ -170,6 +226,9 @@ class MatchInfo: boards_played = max(int(row[1]), 0) if boards_to_play > 0: boards_played += int(towels[0]) + PlayoffLogger.get('matchinfo').info( + 'DB board count for match #%d: %d/%d', + self.info.id, boards_played, boards_to_play) return boards_played, boards_to_play def __has_segment_link(self, cell): @@ -183,6 +242,15 @@ class MatchInfo: def __get_html_running_boards(self, cell): return int(cell.contents[-1].strip()) + def __get_html_segment_board_count(self, segment_url): + segment_content = p_remote.fetch(segment_url) + board_rows = [row for row in segment_content.find_all('tr') if len(row.select('td.bdcc a.zb')) > 0] + board_count = len(board_rows) + played_boards = len([ + row for row in board_rows if len( + ''.join([cell.text.strip() for cell in row.select('td.bdc')])) > 0]) + return played_boards, board_count + def __get_finished_info(self, cell): segment_link = cell.select('a[href]') if len(segment_link) > 0: @@ -190,14 +258,15 @@ class MatchInfo: r'\.htm$', '.html', urljoin(self.info.link, segment_link[0]['href'])) try: - segment_content = p_remote.fetch(segment_url) - board_rows = [row for row in segment_content.find_all('tr') if len(row.select('td.bdcc a.zb')) > 0] - board_count = len(board_rows) - played_boards = len([ - row for row in board_rows if len( - ''.join([cell.text.strip() for cell in row.select('td.bdc')])) > 0]) + played_boards, board_count = self.__get_html_segment_board_count(segment_url) + PlayoffLogger.get('matchinfo').info( + 'HTML played boards count for segment: %d/%d', + played_boards, board_count) return board_count, played_boards >= board_count - except IOError: + except IOError as e: + PlayoffLogger.get('matchinfo').info( + 'cannot fetch HTML played boards count for segment: %s(%s)', + self.info.id, type(e).__name__, str(e)) return 0, False return 0, False @@ -207,14 +276,22 @@ class MatchInfo: row = self.__find_table_row(self.info.link) if row is None: raise ValueError('table row not found') - cells = row.select('td.bdc') - segments = [cell for cell in cells if self.__has_segment_link(cell)] - towels = [cell for cell in cells if self.__has_towel_image(cell)] - if len(segments) == 0: - if len(towels) > 0: - return 1, 1 # entire match is toweled, so mark as finished + for selector in ['td.bdc', 'td.bdcg']: + cells = row.select(selector) + segments = [cell for cell in cells if self.__has_segment_link(cell)] + towels = [cell for cell in cells if self.__has_towel_image(cell)] + if len(segments) == 0: + # in single-segment match, there are no td.bdc cells with segment links + # but maybe it's a multi-segment match with towels + if len(towels) > 0: + PlayoffLogger.get('matchinfo').info( + 'HTML board count for match #%d: all towels', self.info.id) + return 1, 1 # entire match is toweled, so mark as finished else: - raise ValueError('segments not found') + # not a single-segment match, no need to look for td.bdcg cells + break + if len(segments) == 0: + raise ValueError('segments not found') running_segments = row.select('td.bdca') running_boards = sum([self.__get_html_running_boards(segment) for segment in running_segments]) finished_segments = [] @@ -226,8 +303,18 @@ class MatchInfo: finished_segments.append(segment) if boards_in_segment is None and boards > 0: boards_in_segment = boards - total_boards = (len(segments) + len(towels) + len(running_segments)) * boards_in_segment + if 'bdcg' in segments[0]['class']: + # only a single-segment match will yield td.bdcg cells with segment scores + total_boards = boards_in_segment + else: + PlayoffLogger.get('matchinfo').info( + 'HTML board count for match #%d, found: %d finished segments, %d towels, %d boards per segment and %d boards in running segment', + self.info.id, len(finished_segments), len(towels), boards_in_segment, running_boards) + total_boards = (len(segments) + len(towels) + len(running_segments)) * boards_in_segment played_boards = (len(towels) + len(finished_segments)) * boards_in_segment + running_boards + PlayoffLogger.get('matchinfo').info( + 'HTML board count for match #%d: %d/%d', + self.info.id, played_boards, total_boards) return played_boards, total_boards def __fetch_board_count(self): @@ -237,10 +324,16 @@ class MatchInfo: if self.database is None: raise KeyError('database not configured') boards_played, boards_to_play = self.__get_db_board_count() - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('matchinfo').warning( + 'fetching board count from DB for match #%d failed: %s(%s)', + self.info.id, type(e).__name__, str(e)) try: boards_played, boards_to_play = self.__get_html_board_count() - except (TypeError, IndexError, KeyError, IOError, ValueError): + except (TypeError, IndexError, KeyError, IOError, ValueError) as e: + PlayoffLogger.get('matchinfo').warning( + 'fetching board count from HTML for match #%d failed: %s(%s)', + self.info.id, type(e).__name__, str(e)) pass if boards_played > 0: self.info.running = -1 \ @@ -260,6 +353,9 @@ class MatchInfo: current_segment = int( self.database.fetch( self.config['database'], p_sql.CURRENT_SEGMENT, ())[0]) + PlayoffLogger.get('matchinfo').info( + 'fetched running segment from DB for match #%d: %d', + self.info.id, current_segment) return '%s%st%d-%d.html' % ( prefix, round_no, self.config['table'], current_segment) @@ -270,11 +366,15 @@ class MatchInfo: running_link = row.select('td.bdcg a[href]') if len(running_link) == 0: raise ValueError('running link not found') + PlayoffLogger.get('matchinfo').info( + 'fetched running link from HTML for match #%d: %s', + self.info.id, running_link) return urljoin(self.info.link, running_link[0]['href']) def __determine_running_link(self): if self.info.link is None: return + match_link = self.info.link link_match = re.match(r'^(.*)runda(\d+)\.html$', self.info.link) if link_match: try: @@ -282,11 +382,31 @@ class MatchInfo: raise KeyError('database not configured') self.info.link = self.__get_db_running_link( link_match.group(1), link_match.group(2)) - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('matchinfo').warning( + 'cannot determine running link from DB for match #%d: %s(%s)', + self.info.id, type(e).__name__, str(e)) try: self.info.link = self.__get_html_running_link() - except (TypeError, IndexError, KeyError, IOError, ValueError): - pass + except (TypeError, IndexError, KeyError, IOError, ValueError) as e: + PlayoffLogger.get('matchinfo').warning( + 'cannot determine running link from HTML for match #%d: %s(%s)', + self.info.id, type(e).__name__, str(e)) + if self.info.link != match_link: + # we've detected a running segment link + # we should check if the segment's uploaded live + try: + boards_played, board_count = self.__get_html_segment_board_count(re.sub('\.htm$', '.html', self.info.link)) + except IOError as e: + PlayoffLogger.get('matchinfo').warning( + 'cannot determine running link (%s) board count for match #%d: %s(%s)', + self.info.link, self.info.id, type(e).__name__, str(e)) + boards_played = 0 + if not boards_played: + PlayoffLogger.get('matchinfo').warning( + 'running link (%s) for match #%d is not live, reverting to match link (%s)', + self.info.link, self.info.id, match_link) + self.info.link = match_link def set_phase_link(self, phase_link): if self.info.link is None: @@ -294,6 +414,9 @@ class MatchInfo: else: if self.info.link != '#': self.info.link = urljoin(phase_link, self.info.link) + PlayoffLogger.get('matchinfo').info( + 'applying phase link %s to match #%d: %s', + phase_link, self.info.id, self.info.link) def get_info(self): self.__fetch_teams_with_scores() diff --git a/jfr_playoff/remote.py b/jfr_playoff/remote.py index b687740..f0bf6c2 100644 --- a/jfr_playoff/remote.py +++ b/jfr_playoff/remote.py @@ -3,6 +3,7 @@ import re import requests from bs4 import BeautifulSoup as bs +from jfr_playoff.logger import PlayoffLogger class RemoteUrl: @@ -10,6 +11,8 @@ class RemoteUrl: @classmethod def fetch(cls, url): + PlayoffLogger.get('remote').info( + 'fetching content for: %s', url) if url not in cls.url_cache: request = requests.get(url) encoding_match = re.search( @@ -18,4 +21,7 @@ class RemoteUrl: if encoding_match: request.encoding = encoding_match.group(2) cls.url_cache[url] = request.text + PlayoffLogger.get('remote').info( + 'fetched %d bytes from remote location', + len(cls.url_cache[url])) return bs(cls.url_cache[url], 'lxml') diff --git a/jfr_playoff/settings.py b/jfr_playoff/settings.py index 021253f..58de66c 100644 --- a/jfr_playoff/settings.py +++ b/jfr_playoff/settings.py @@ -4,6 +4,8 @@ import readline import requests import sys +from jfr_playoff.logger import PlayoffLogger + def complete_filename(text, state): return (glob.glob(text+'*')+[None])[state] @@ -31,9 +33,9 @@ class PlayoffSettings(object): if (key not in base_config) or overwrite: base_config[key] = value except Exception as e: - print 'WARNING: unable to merge remote config: %s' % (str(e)) - if remote_url is not None: - print 'Offending URL: %s' % (remote_url) + PlayoffLogger.get('settings').warning( + 'unable to merge remote config %s: %s(%s)', + remote_url, type(e).__name__, str(e)) return base_config def load(self): @@ -45,16 +47,24 @@ class PlayoffSettings(object): 'JSON settings file: ').decode(sys.stdin.encoding) if self.settings is None: + PlayoffLogger.get('settings').info( + 'loading config file: %s', unicode(self.settings_file)) self.settings = json.loads( open(unicode(self.settings_file)).read().decode('utf-8-sig')) if self.has_section('remotes'): remote_config = {} for remote in self.get('remotes'): + PlayoffLogger.get('settings').info( + 'merging remote config: %s', remote) remote_config = self.__merge_config( remote_config, remote_url=remote) + PlayoffLogger.get('settings').debug( + 'remote config: %s', remote_config) self.settings = self.__merge_config( self.settings, new_config=remote_config, overwrite=False) + PlayoffLogger.get('settings').debug( + 'parsed config: %s', self.settings) def has_section(self, key): self.load() diff --git a/jfr_playoff/template.py b/jfr_playoff/template.py index b99c7c7..70c179b 100644 --- a/jfr_playoff/template.py +++ b/jfr_playoff/template.py @@ -1,180 +1,193 @@ # -*- coding: utf-8 -*- -MATCH_TABLE = ''' -<table border="0" cellspacing="0"> -<tr> -<td class="s12" width="%d"> </td> -<td class="bdcc2" width="%d"> wynik </td> -</tr> -%s -</table> -''' - -MATCH_LINK = ''' -<a href="%s" target="_top"> -%s -</a> -''' - -MATCH_SCORE = ''' - %.1f -''' - -MATCH_TEAM_LINK = ''' -<a href="%s" onmouseover="Tip('%s')" onmouseout="UnTip()">%s</a> -''' - -MATCH_TEAM_NON_LINK = ''' -<a onmouseover="Tip('%s')" onmouseout="UnTip()">%s</a> -''' - -MATCH_TEAM_ROW = ''' -<tr class="%s"> -<td class="bd1"> %s </td> -<td class="bdc"> -%s -</td> -</tr> -''' - -MATCH_RUNNING = ''' -<img src="images/A.gif" /> -<span style="font-size: 10pt">%d</span> -<img src="images/A.gif" /> -''' - -MATCH_GRID = ''' -<div style="position: relative; width: %dpx; height: %dpx; margin: 10px"> -<canvas width="%d" height="%d" id="playoff_canvas" %s></canvas> -%s -<script src="sklady/playoff.js" type="text/javascript"></script> -</div> -''' - -MATCH_GRID_PHASE_LINK = ''' -<a href="%s" target="_top" style="display: inline-block; width: %dpx; text-align: center; position: absolute; top: 0; left: %dpx"> -%s -</a> -''' - -MATCH_GRID_PHASE_NON_LINK = ''' -<span class="phase_header" style="display: inline-block; width: %dpx; text-align: center; position: absolute; top: 0; left: %dpx"> -<p style="margin: 0">%s</p> -</span> -''' - -MATCH_GRID_PHASE = ''' -<font size="4">%s</font> -''' - -MATCH_GRID_PHASE_RUNNING = ''' -<img src="images/A.gif" /> -<font size="4">%s</font> -<img src="images/A.gif" /> -''' - -MATCH_BOX = ''' -<div style="text-align: center; position: absolute; left: %dpx; top: %dpx" data-id="%d" data-winner="%s" data-loser="%s" class="playoff_matchbox"> -%s -</div> -''' - -LEADERBOARD = ''' -<table border="0" cellspacing="0"> -<tr> -<td class="bdnl12" colspan="2" align="center"><b> KLASYFIKACJA KOŃCOWA </b></td> -</tr> -<tr> -<td class="e" colspan="2"> </td> -</tr> -<tr> -<td class="bdcc12"> miejsce </td> -<td class="bdcc2"> drużyna </td> -</tr> -%s -</table> -''' - -LEADERBOARD_ROW = ''' -<tr class="%s"> -<td class="bdc1">%d</td> -<td class="bd"> - %s %s -</td> -</tr> -''' - -LEADERBOARD_ROW_FLAG = ''' -<img class="fl" src="images/%s" /> -''' - -LEADERBOARD_CAPTION_TABLE = ''' -<table class="caption_table" border="0" cellspacing="0"> -<tr><td class="e"> </td></tr> -<tr><td class="bdnl12" align="center"><b> LEGENDA </b></td></tr> -%s -</table> -''' - -LEADERBOARD_CAPTION_TABLE_ROW = ''' -<tr class="%s"> -<td class="bd1"> - %s -</td> -</tr> -''' - -PAGE_HEAD = ''' -<meta http-equiv="Pragma" content="no-cache" /> -<meta http-equiv="Cache-Control" content="no-cache" /> -<meta name="robots" content="noarchive" /> -<meta http-equiv="expires" content="0" /> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<meta name="Generator" content="PlayOff" /> -%s -<title>%s</title> -<link rel="stylesheet" type="text/css" href="css/kolorki.css" /> -<script type="text/javascript" src="sklady/myAjax.js"></script> -''' - -PAGE_HEAD_REFRESH = ''' -<meta http-equiv="Refresh" content="%d" /> -''' - -PAGE_BODY = ''' -<script type="text/javascript" src="sklady/wz_tooltip.js"></script> -%s -%s -<p> -%s -</p> -%s -%s -%s -''' - -PAGE_BODY_FOOTER = ''' -<p class="f"> Admin ©Jan Romański'2005, PlayOff ©Michał Klichowicz'2017-2018, strona wygenerowana %s</p> -''' - -PAGE = ''' -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> -<html> -<head> -%s -</head> -<body class="all"> -%s -</body> -</html> -''' - -SWISS_LINK = ''' -[<a href="%s" class="zb" target="_top"> %s </a>]<br /><br /> -''' - -SWISS_RUNNING_LINK = ''' -[<a href="%s" class="zb" target="_top"> <img src="images/A.gif" /> %s <img src="images/A.gif" /> </a>]<br /><br /> -''' - -SWISS_DEFAULT_LABEL = 'Turniej o %d. miejsce' +from jfr_playoff.i18n import PlayoffI18N + +class PlayoffTemplateStrings(object): + + MATCH_TABLE = ''' + <table border="0" cellspacing="0"> + <tr> + <td class="s12" width="%d"> </td> + <td class="bdcc2" width="%d"> {{SCORE}} </td> + </tr> + %s + </table> + ''' + + MATCH_LINK = ''' + <a href="%s" target="_top"> + %s + </a> + ''' + + MATCH_SCORE = ''' + %.1f + ''' + + MATCH_TEAM_LINK = ''' + <a href="%s" onmouseover="Tip('%s')" onmouseout="UnTip()">%s</a> + ''' + + MATCH_TEAM_NON_LINK = ''' + <a onmouseover="Tip('%s')" onmouseout="UnTip()">%s</a> + ''' + + MATCH_TEAM_ROW = ''' + <tr class="%s"> + <td class="bd1"> %s </td> + <td class="bdc"> + %s + </td> + </tr> + ''' + + MATCH_RUNNING = ''' + <img src="images/A.gif" /> + <span style="font-size: 10pt">%d</span> + <img src="images/A.gif" /> + ''' + + MATCH_GRID = ''' + <div style="position: relative; width: %dpx; height: %dpx; margin: 10px"> + <canvas width="%d" height="%d" id="playoff_canvas" %s></canvas> + %s + <script src="sklady/playoff.js" type="text/javascript"></script> + </div> + ''' + + MATCH_GRID_PHASE_LINK = ''' + <a href="%s" target="_top" style="display: inline-block; width: %dpx; text-align: center; position: absolute; top: 0; left: %dpx"> + %s + </a> + ''' + + MATCH_GRID_PHASE_NON_LINK = ''' + <span class="phase_header" style="display: inline-block; width: %dpx; text-align: center; position: absolute; top: 0; left: %dpx"> + <p style="margin: 0">%s</p> + </span> + ''' + + MATCH_GRID_PHASE = ''' + <font size="4">%s</font> + ''' + + MATCH_GRID_PHASE_RUNNING = ''' + <img src="images/A.gif" /> + <font size="4">%s</font> + <img src="images/A.gif" /> + ''' + + MATCH_BOX = ''' + <div style="text-align: center; position: absolute; left: %dpx; top: %dpx" data-id="%d" data-winner="%s" data-loser="%s" class="playoff_matchbox"> + %s + </div> + ''' + + LEADERBOARD = ''' + <table border="0" cellspacing="0"> + <tr> + <td class="bdnl12" colspan="2" align="center" style="text-transform: uppercase"><b> {{FINAL_STANDINGS}} </b></td> + </tr> + <tr> + <td class="e" colspan="2"> </td> + </tr> + <tr> + <td class="bdcc12"> {{STANDINGS_PLACE}} </td> + <td class="bdcc2"> {{STANDINGS_TEAM}} </td> + </tr> + %s + </table> + ''' + + LEADERBOARD_ROW = ''' + <tr class="%s"> + <td class="bdc1">%d</td> + <td class="bd"> + %s %s + </td> + </tr> + ''' + + LEADERBOARD_ROW_FLAG = ''' + <img class="fl" src="images/%s" /> + ''' + + LEADERBOARD_CAPTION_TABLE = ''' + <table class="caption_table" border="0" cellspacing="0"> + <tr><td class="e"> </td></tr> + <tr><td class="bdnl12" align="center" style="text-transform: uppercase"><b> {{STANDINGS_CAPTIONS}} </b></td></tr> + %s + </table> + ''' + + LEADERBOARD_CAPTION_TABLE_ROW = ''' + <tr class="%s"> + <td class="bd1"> + %s + </td> + </tr> + ''' + + PAGE_HEAD = ''' + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Cache-Control" content="no-cache" /> + <meta name="robots" content="noarchive" /> + <meta http-equiv="expires" content="0" /> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <meta name="Generator" content="PlayOff" /> + %s + <title>%s</title> + <link rel="stylesheet" type="text/css" href="css/kolorki.css" /> + <script type="text/javascript" src="sklady/myAjax.js"></script> + ''' + + PAGE_HEAD_REFRESH = ''' + <meta http-equiv="Refresh" content="%d" /> + ''' + + PAGE_BODY = ''' + <script type="text/javascript" src="sklady/wz_tooltip.js"></script> + %s + %s + <p> + %s + </p> + %s + %s + %s + ''' + + PAGE_BODY_FOOTER = ''' + <p class="f"> Admin ©Jan Romański'2005, PlayOff ©Michał Klichowicz'2017-2018, {{FOOTER_GENERATED}} %s</p> + ''' + + PAGE = ''' + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> + <html> + <head> + %s + </head> + <body class="all"> + %s + </body> + </html> + ''' + + SWISS_LINK = ''' + [<a href="%s" class="zb" target="_top"> %s </a>]<br /><br /> + ''' + + SWISS_RUNNING_LINK = ''' + [<a href="%s" class="zb" target="_top"> <img src="images/A.gif" /> %s <img src="images/A.gif" /> </a>]<br /><br /> + ''' + + SWISS_DEFAULT_LABEL = '{{SWISS_DEFAULT_LABEL}}' + +class PlayoffTemplate(object): + + def __init__(self, settings): + self.i18n = PlayoffI18N(settings) + + def get(self, string, *params): + return self.i18n.localize( + getattr(PlayoffTemplateStrings, string).decode('utf8')) % params diff --git a/jfr_playoff/tournamentinfo.py b/jfr_playoff/tournamentinfo.py index ee96214..45a7752 100644 --- a/jfr_playoff/tournamentinfo.py +++ b/jfr_playoff/tournamentinfo.py @@ -3,8 +3,9 @@ import re import jfr_playoff.sql as p_sql from jfr_playoff.remote import RemoteUrl as p_remote +from jfr_playoff.logger import PlayoffLogger -SWISS_TIE_WARNING = 'WARNING: tie detected in swiss %s.' + \ +SWISS_TIE_WARNING = 'tie detected in swiss %s.' + \ ' Make sure to resolve the tie by arranging teams' + \ ' in configuration file.' @@ -19,7 +20,9 @@ class TournamentInfo: if 'link' not in self.settings: raise KeyError('link not configured') if not self.settings['link'].endswith('leaderb.html'): - raise ValueError('unable to determine tournament results') + raise ValueError('invalid link to tournament results') + PlayoffLogger.get('tournamentinfo').info( + 'fetching tournament results from leaderboard URL: %s', self.settings['link']) leaderboard = p_remote.fetch(self.settings['link']) result_links = [row.select('a[onmouseover]') for row in leaderboard.find_all('tr') if len(row.select('a[onmouseover]')) > 0] results = [None] * (len(result_links) * max([len(links) for links in result_links])) @@ -39,15 +42,23 @@ class TournamentInfo: if team_image is not None: team_info.append(team_image['src'].replace('images/', '')) teams.append(team_info) + PlayoffLogger.get('tournamentinfo').info( + 'read tournament results from leaderboard: %s', teams) for table in range(1, int(ceil(len(teams)/2.0))+1): table_url = self.get_results_link('1t%d-1.html' % (table)) table_content = p_remote.fetch(table_url) + PlayoffLogger.get('tournamentinfo').info( + 'reading team shortnames from traveller: %s', table_url) for link in table_content.select('a.br'): if link['href'] in team_links: for team in teams: if team[0] == team_links[link['href']]: team[1] = link.text.strip(u'\xa0') + PlayoffLogger.get('tournamentinfo').info( + 'shortname for %s: %s', team[0], team[1]) break + PlayoffLogger.get('tournamentinfo').info( + 'tournament results from HTML: %s', teams) return teams def __get_db_results(self): @@ -66,23 +77,44 @@ class TournamentInfo: swiss_results = sorted( swiss_results, key=lambda t: t[1], reverse=True) swiss_results = sorted(swiss_results, key=lambda team: team[2]) + PlayoffLogger.get('tournamentinfo').info( + 'fetched tournament results from database %s: %s', + self.settings['database'], swiss_results) prev_result = None for team in swiss_results: if prev_result == team[1]: - print SWISS_TIE_WARNING % (self.settings['database']) + PlayoffLogger.get('tournamentinfo').warning( + SWISS_TIE_WARNING, self.settings['database']) prev_result = team[1] db_teams = [[team[0], team[3], team[4]] for team in swiss_results] + PlayoffLogger.get('tournamentinfo').info( + 'fetched team list from database %s: %s', + self.settings['database'], db_teams) return db_teams def __get_html_finished(self): if 'link' not in self.settings: raise KeyError('link not configured') if not self.settings['link'].endswith('leaderb.html'): - raise ValueError('unable to determine tournament status') + raise ValueError('invalid tournament leaderboard link') + PlayoffLogger.get('tournamentinfo').info( + 'fetching tournament finished status from HTML: %s', + self.settings['link']) leaderboard = p_remote.fetch(self.settings['link']) leaderb_heading = leaderboard.select('td.bdnl12')[0].text + contains_digits = any(char.isdigit() for char in leaderb_heading) + PlayoffLogger.get('tournamentinfo').info( + 'tournament header from HTML: %s, %s', + leaderb_heading, 'contains digits' if contains_digits else "doesn't contain digits") non_zero_scores = [imps.text for imps in leaderboard.select('td.bdc small') if imps.text != '0-0'] - return (not any(char.isdigit() for char in leaderb_heading)) and (len(non_zero_scores) > 0) + PlayoffLogger.get('tournamentinfo').info( + 'tournament leaderboard from HTML: has %d non-zero scores', + len(non_zero_scores)) + finished = (not contains_digits) and (len(non_zero_scores) > 0) + PlayoffLogger.get('tournamentinfo').info( + 'tournament leaderboard from HTML indicates finished: %s', + finished) + return finished def __get_db_finished(self): if self.database is None: @@ -91,14 +123,21 @@ class TournamentInfo: raise KeyError('database not configured') finished = self.database.fetch( self.settings['database'], p_sql.SWISS_ENDED, {}) + PlayoffLogger.get('tournamentinfo').info( + 'fetching tournament finished status from DB %s: %s', + self.settings['database'], finished) return (len(finished) > 0) and (finished[0] > 0) def __get_html_link(self, suffix='leaderb.html'): if 'link' not in self.settings: raise KeyError('link not configured') if not self.settings['link'].endswith('leaderb.html'): - raise ValueError('unable to determine html link') - return re.sub(r'leaderb.html$', suffix, self.settings['link']) + raise ValueError('invalid tournament leaderboard link') + link = re.sub(r'leaderb.html$', suffix, self.settings['link']) + PlayoffLogger.get('tournamentinfo').info( + 'generating tournament-specific link from leaderboard link %s: %s -> %s', + self.settings['link'], suffix, link) + return link def __get_db_link(self, suffix='leaderb.html'): if self.database is None: @@ -109,20 +148,33 @@ class TournamentInfo: self.settings['database'], p_sql.PREFIX, ()) if row is not None: if len(row) > 0: - return row[0] + suffix + link = row[0] + suffix + PlayoffLogger.get('tournamentinfo').info( + 'generating tournament-specific link from DB %s prefix: %s -> %s', + self.settings['database'], suffix, link) + return link raise ValueError('unable to fetch db link') def get_tournament_results(self): teams = [] try: teams = self.__get_db_results() - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament results from DB: %s(%s)', + type(e).__name__, str(e)) try: teams = self.__get_html_results() - except (TypeError, IndexError, KeyError, IOError, ValueError): - pass - if self.is_finished() and 'final_positions' in self.settings: - for position in self.settings['final_positions']: + except (TypeError, IndexError, KeyError, IOError, ValueError) as e: + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament results from HTML: %s(%s)', + type(e).__name__, str(e)) + if self.is_finished(): + final_positions = self.settings.get('final_positions', []) + PlayoffLogger.get('tournamentinfo').info( + 'setting final positions from tournament results: %s', + final_positions) + for position in final_positions: if len(teams) >= position: teams[position-1].append(position) return teams @@ -130,19 +182,31 @@ class TournamentInfo: def is_finished(self): try: return self.__get_db_finished() - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament finished status from DB: %s(%s)', + type(e).__name__, str(e)) try: return self.__get_html_finished() - except (TypeError, IndexError, KeyError, IOError, ValueError): - pass + except (TypeError, IndexError, KeyError, IOError, ValueError) as e: + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament finished status from HTML: %s(%s)', + type(e).__name__, str(e)) + PlayoffLogger.get('tournamentinfo').info( + 'assuming tournament is finished') return True def get_results_link(self, suffix='leaderb.html'): try: return self.__get_db_link(suffix) - except (IOError, TypeError, IndexError, KeyError): + except (IOError, TypeError, IndexError, KeyError) as e: + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament link from DB: %s(%s)', + type(e).__name__, str(e)) try: return self.__get_html_link(suffix) except (KeyError, ValueError): - pass + PlayoffLogger.get('tournamentinfo').warning( + 'cannot determine tournament link from HTML: %s(%s)', + type(e).__name__, str(e)) return None @@ -30,7 +30,7 @@ def main(): 'INFO' if arguments.verbose else ( 'DEBUG' if arguments.debug else 'WARNING'))) - PlayoffLogger.get().debug('started with arguments: %s', arguments) + PlayoffLogger.get().info('started with arguments: %s', arguments) settings = PlayoffSettings(arguments.config_file) interactive = settings.interactive |