Package bosco :: Module formatter
[hide private]
[frames] | no frames]

Source Code for Module bosco.formatter

  1  # 
  2  #    Copyright (C) 2008  Gaudenz Steinlin <gaudenz@soziologie.ch> 
  3  # 
  4  #    This program is free software: you can redistribute it and/or modify 
  5  #    it under the terms of the GNU General Public License as published by 
  6  #    the Free Software Foundation, either version 3 of the License, or 
  7  #    (at your option) any later version. 
  8  # 
  9  #    This program is distributed in the hope that it will be useful, 
 10  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 12  #    GNU General Public License for more details. 
 13  # 
 14  #    You should have received a copy of the GNU General Public License 
 15  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 16  """ 
 17  formatter.py - Classes to to format rankings 
 18  """ 
 19   
 20  import pkg_resources 
 21  import json 
 22   
 23  from mako.lookup import TemplateLookup 
 24   
 25  from reportlab.lib import colors, pagesizes 
 26  from reportlab.platypus import * 
 27  from reportlab.lib.styles import getSampleStyleSheet 
 28   
 29  from datetime import datetime 
 30  from StringIO import StringIO 
 31  from csv import writer 
 32   
 33  from ranking import Validator, ValidationError, UnscoreableException 
 34  from course import SIStation, Control 
 35  from run import Punch, Run 
 36   
37 -def format_timedelta(delta):
38 (hours, seconds) = divmod(delta.seconds, 3600) 39 (minutes, seconds) = divmod(seconds, 60) 40 if hours > 0: 41 return "%i:%02i:%02i" % (hours, minutes, seconds) 42 else: 43 return "%i:%02i" % (minutes, seconds)
44
45 -class AbstractFormatter(object):
46 47 validation_codes = {Validator.OK : 'OK', 48 Validator.NOT_COMPLETED : 'not yet finished', 49 Validator.MISSING_CONTROLS : 'missing controls', 50 Validator.DID_NOT_FINISH : 'did not finish', 51 Validator.DISQUALIFIED : 'disqualified', 52 Validator.DID_NOT_START : 'did not start'}
53
54 -class AbstractRankingFormatter(AbstractFormatter):
55 """Formats a ranking. str(rankingRormatter) returns the formatted ranking.""" 56
57 - def __init__(self, rankings):
58 """ 59 @param ranking: the ranking to format 60 @type ranking: generator or list as returned by L{Rankable}s ranking method. 61 """ 62 63 self.rankings = rankings
64
65 - def __str__(self):
66 """ 67 @return: Formatted Ranking 68 """ 69 pass
70
71 -class MakoRankingFormatter(AbstractRankingFormatter):
72 """Uses the Mako Templating Engine to format a ranking as HTML.""" 73
74 - def __init__(self, rankings, header, template_file, template_dir):
75 """ 76 @type rankings: list of dicts with keys 'ranking' and 'info' 77 the value of the 'ranking' key is an object of 78 class Ranking 79 @param template_file: File name for the template 80 @param template_dir: template directory (inside the bosco module) 81 @param header: gerneral information for the ranking header 82 @type header: dict 83 """ 84 super(type(self), self).__init__(rankings) 85 lookup = TemplateLookup(directories=[pkg_resources.resource_filename('bosco', template_dir)]) 86 self._template = lookup.get_template(template_file) 87 self._header = header
88
89 - def __str__(self):
90 91 return self._template.render_unicode(header = self._header, 92 validation_codes = self.validation_codes, 93 now = datetime.now().strftime('%c'), 94 rankings = self.rankings)
95
96 -class AbstractSOLVRankingFormatter(AbstractRankingFormatter):
97 """Formats the Ranking for exporting to the SOLV result site.""" 98 99 validation_codes = {Validator.OK : 'OK', 100 Validator.NOT_COMPLETED : '', 101 Validator.MISSING_CONTROLS : 'fehl', 102 Validator.DID_NOT_FINISH : 'aufg', 103 Validator.DISQUALIFIED : 'disq', 104 Validator.DID_NOT_START : 'n. gest'} 105
106 - def __init__(self, ranking, reftime, encoding = 'utf-8', control_replacements = None, control_exclude = None, 107 lineterminator = '\n'):
108 """ 109 @reftime: reference time for the event (usually first starttime) 110 """ 111 AbstractRankingFormatter.__init__(self, ranking) 112 self._reftime = reftime 113 self._encoding = encoding 114 self._control_replacements = control_replacements if control_replacements is not None else {} 115 self._control_exclude = control_exclude if control_exclude is not None else () 116 self._lineterminator = lineterminator
117
118 - def _print_score(self, r):
119 if r['validation']['status'] == Validator.OK: 120 return str(r['scoreing']['score']) 121 else: 122 return self.validation_codes[r['validation']['status']]
123
124 - def _encode(self, s):
125 return unicode(s).encode(self._encoding, 'xmlcharrefreplace')
126
127 - def _control_code(self, control):
128 try: 129 return self._control_replacements[control.code] 130 except KeyError: 131 return control.code
132
133 - def _writer(self):
134 self._outstr = StringIO() 135 return writer(self._outstr, delimiter=';', 136 lineterminator=self._lineterminator)
137
138 - def _output(self):
139 return self._outstr.getvalue()
140
141 - def __str__(self):
142 raise SOLVRankingFormatterException('Use a subclass and overrwrite this method.')
143 144
145 -class CourseSOLVRankingFormatter(AbstractSOLVRankingFormatter):
146 """ 147 Formatting a course ranking to be uploaded to the SOLV website 148 Format: Rank;Name;Firstname;YearOfBirth;SexMF;FedNr;Zip;Town;Club;NationIOF;Start Nr;eCardNr;RunTime;StartTime;FinishTime;CtrlCode;SplitTime 149 """
150 - def __str__(self):
151 152 output = self._writer() 153 for ranking in self.rankings: 154 output.writerow([str(ranking.rankable), 155 ranking.rankable.length, 156 ranking.rankable.climb, 157 ranking.rankable.controlcount() 158 ]) 159 for r in ranking: 160 line = [r['rank'] or '', 161 self._encode(r['item'].sicard.runner.surname), 162 self._encode(r['item'].sicard.runner.given_name), 163 r['item'].sicard.runner.dateofbirth and r['item'].sicard.runner.dateofbirth.strftime('%y') or '', 164 r['item'].sicard.runner.sex, 165 '', # FedNR (?) 166 '', # Zip 167 '', # Town 168 r['item'].sicard.runner.team and self._encode(r['item'].sicard.runner.team.name) or '', # Club 169 '', # NationIOF 170 '', # Start Nr 171 '', # eCardNr 172 self._print_score(r), 173 ] 174 try: 175 line.append(r['scoreing']['start'] - self._reftime) 176 except (TypeError, KeyError): 177 line.append('') 178 try: 179 line.append(r['scoreing']['finish'] - self._reftime) 180 except (TypeError, KeyError): 181 line.append('') 182 183 try: 184 punchlist = r['validation']['reordered_punchlist'] 185 except KeyError: 186 punchlist = r['validation']['punchlist'] 187 for status, p in punchlist: 188 if status == 'missing' and not p.code in self._control_exclude: 189 # p is an object of class Control 190 line.extend([self._encode(self._control_code(p)), '']) 191 if not status == 'ok': 192 # ignore additional punches 193 continue 194 if (not p.sistation.control is None 195 and p.sistation.id > SIStation.SPECIAL_MAX 196 and not p.sistation.control.code in self._control_exclude): 197 try: 198 line.extend([self._encode(self._control_code(p.sistation.control)), 199 p.punchtime - r['scoreing']['start']]) 200 except (TypeError, KeyError): 201 line.extend([self._encode(self._control_code(p.sistation.control)), 202 '']) 203 204 output.writerow(line) 205 206 return self._output()
207
208 -class CategorySOLVRankingFormatter(AbstractSOLVRankingFormatter):
209
210 - def __str__(self):
211 212 output = self._writer() 213 214 for ranking in self.rankings: 215 output.writerow([str(ranking.rankable)]) 216 217 for r in ranking: 218 line = [r['rank'] or '', 219 self._encode(r['item']), 220 self._print_score(r), 221 ] 222 223 output.writerow(line) 224 225 return self._output()
226
227 -class RelayCategorySOLVRankingFormatter(AbstractSOLVRankingFormatter):
228 """ 229 As there is no real documenation for the SOLV ranking format this is modeled 230 after the file for "Osterstaffel 2012" made with ORWare. 231 """ 232
233 - def __str__(self):
234 235 output = self._writer() 236 for ranking in self.rankings: 237 output.writerow([str(ranking.rankable)]) 238 239 for r in ranking: 240 line = [r['rank'] or '', 241 self._encode(r['item'].number), 242 self._encode(r['item']), 243 '', # TODO: put nation of team into database 244 self._print_score(r), 245 r['scoreing']['behind'] or '', 246 '', # RelayAltStart, whatever that could be... 247 len(r['runs']), 248 ] 249 for leg in range(len(r['runs'])): 250 leg_run = r['runs'][leg] 251 leg_split = r['splits'][leg] 252 if leg_run is not None: 253 run = leg_run['item'] 254 runner = run.sicard.runner 255 line.extend([self._encode(runner.surname) or '', 256 self._encode(runner.given_name) or '', 257 runner.dateofbirth and runner.dateofbirth.year or '', 258 self._encode(run.course.code), 259 leg_run['rank'] or '', 260 self._print_score(leg_run), 261 leg_run['scoreing']['behind'] or '', 262 ]) 263 else: 264 # No valid run on this leg 265 line.extend([''] * 7) 266 line.extend(['', # LegAltStart, whatever that could be... 267 leg_split['rank'] or '', 268 self._print_score(leg_split), 269 leg_split['scoreing']['behind'] or '', 270 ]) 271 output.writerow(line) 272 273 return self._output()
274
275 -class RoundCountRankingFormatter(AbstractSOLVRankingFormatter):
276
277 - def __str__(self):
278 279 output = self._writer() 280 281 for ranking in self.rankings: 282 lines = [] 283 for r in ranking: 284 if type(r['item']) == Run: 285 runner = r['item'].sicard.runner 286 run = r['item'] 287 else: 288 runner = r['item'] 289 run = r['item'].run 290 number = runner and runner.number or 0 291 lines.append([r['rank'] or '', 292 run.sicard.id, 293 self._encode(runner and runner.category or u''), 294 self._encode(number), # change index below if position of this element changes 295 self._encode(runner and runner.given_name or u''), 296 self._encode(runner and runner.surname or u''), 297 298 self._print_score(r), 299 ]) 300 301 # reorder by number instead of rank 302 lines.sort(key=lambda x: int(x[3])) 303 output.writerow([str(ranking.rankable)]) 304 output.writerows(lines) 305 306 return self._output()
307 308
309 -class OlanaRankingFormatter(AbstractSOLVRankingFormatter):
310
311 - def __str__(self):
312 results = { 313 'name': '', 314 'map': '', 315 'date': '', 316 'startTime': str(self._reftime), 317 'categories': [], 318 } 319 320 for ranking in self.rankings: 321 cat = { 322 'name': str(ranking.rankable), 323 'distance': ranking.rankable.length, 324 'ascent': ranking.rankable.climb, 325 'controls': ranking.rankable.controlcount(), 326 'runners': [], 327 } 328 329 for r in ranking: 330 run_dict = { 331 'fullName': '%s %s' % (r['item'].sicard.runner.given_name, r['item'].sicard.runner.surname), 332 'yearOfBirth': r['item'].sicard.runner.dateofbirth and r['item'].sicard.runner.dateofbirth.strftime('%y') or '', 333 'sex': r['item'].sicard.runner.sex, 334 'club': r['item'].sicard.runner.team and r['item'].sicard.runner.team.name or '', 335 'city': '', 336 'nation': '', 337 'time': self._print_score(r), 338 'ecard': r['item'].sicard.id, 339 } 340 341 try: 342 run_dict['startTime'] = str(r['scoreing']['start'] - self._reftime) 343 except (TypeError, KeyError): 344 run_dict['startTime'] = '' 345 346 try: 347 punchlist = r['validation']['reordered_punchlist'] 348 except KeyError: 349 punchlist = r['validation']['punchlist'] 350 splits = [] 351 for status, p in punchlist: 352 if status == 'missing' and not p.code in self._control_exclude: 353 # p is an object of class Control 354 splits.append([self._control_code(p), '']) 355 if not status == 'ok': 356 # ignore additional punches 357 continue 358 if (not p.sistation.control is None 359 and p.sistation.id > SIStation.SPECIAL_MAX 360 and not p.sistation.control.code in self._control_exclude): 361 try: 362 splits.append([self._control_code(p.sistation.control), 363 str(p.punchtime - r['scoreing']['start'])]) 364 except (TypeError, KeyError): 365 splits.append([self._control_code(p.sistation.control), '']) 366 run_dict['splits'] = splits 367 run_dict['course'] = ','.join([s[0] for s in splits]) 368 369 cat['runners'].append(run_dict) 370 371 results['categories'].append(cat) 372 373 return json.dumps(results)
374 375
376 -class AbstractRunFormatter(AbstractFormatter):
377 """Formats a Run.""" 378
379 - def __init__(self, run, header, event):
380 """ 381 @param run run to format 382 @param header header information 383 @type header dict 384 @param event event this run belongs to 385 """ 386 self._run = run 387 self._header = header 388 self._event = event
389
390 - def __str__(self):
391 """ 392 @return formatted run 393 """ 394 pass
395
396 - def _raw_punchlist(self):
397 try: 398 punchlist = self._event.validate(self._run)['punchlist'] 399 except ValidationError: 400 # create pseudo validation result 401 punchlist = [ ('ignored', p) for p in self._run.punches ] 402 403 return punchlist
404
405 - def _punchlist(self, with_finish = False):
406 407 raw_punchlist = self._raw_punchlist() 408 punchlist = [] 409 try: 410 lastpunch = start = self._event.score(self._run)['start'] 411 except UnscoreableException: 412 lastpunch = start = self._run.start_time or raw_punchlist[0][1].punchtime 413 for code, p in raw_punchlist: 414 if type(p) == Punch: 415 punchtime = p.manual_punchtime or p.card_punchtime 416 punchlist.append((p.sequence and str(p.sequence) or '', 417 p.sistation.control and p.sistation.control.code or '', 418 str(p.sistation.id), 419 p.card_punchtime and str(p.card_punchtime) or '', 420 p.manual_punchtime and str(p.manual_punchtime) or '', 421 punchtime and format_timedelta(punchtime - start) or '', 422 punchtime and format_timedelta(punchtime - lastpunch) or '', 423 str(int(p.ignore)), 424 str(code))) 425 if code == 'ok': 426 lastpunch = punchtime 427 elif type(p) == Control: 428 punchlist.append(('', 429 p.code, 430 '', 431 '', 432 '', 433 '', 434 '', 435 str(int(False)), 436 code)) 437 elif type(p) == SIStation: 438 punchlist.append(('', 439 '', 440 str(p.id), 441 '', 442 '', 443 '', 444 '', 445 str(int(False)), 446 code)) 447 if with_finish: 448 punchtime = self._run.manual_finish_time or self._run.card_finish_time 449 punchlist.append(('', 450 'Finish', 451 '', 452 self._run.card_finish_time and str(self._run.card_finish_time) or '', 453 self._run.manual_finish_time and str(self._run.manual_finish_time) or '', 454 punchtime and format_timedelta(punchtime - start) or '', 455 punchtime and format_timedelta(punchtime - lastpunch) or '', 456 str(int(False)), 457 'finish')) 458 return punchlist
459
460 -class ReportlabRunFormatter(AbstractRunFormatter):
461
462 - def __str__(self):
463 try: 464 validation = self._event.validate(self._run) 465 except ValidationError, validation_error: 466 validation = None 467 try: 468 score = self._event.score(self._run) 469 except UnscoreableException: 470 score = None 471 472 io = StringIO() 473 doc = SimpleDocTemplate(io, 474 pagesize=pagesizes.landscape(pagesizes.A5), 475 leftMargin = 20, rightMargin = 20, topMargin = 20, bottomMargin = 20) 476 477 styles = getSampleStyleSheet() 478 elements = [] 479 480 elements.append(Paragraph(("%(event)s / %(map)s / %(place)s / %(date)s / %(organiser)s" % 481 self._header), 482 styles['Normal'])) 483 elements.append(Spacer(0,10)) 484 485 runner = self._run.sicard.runner 486 elements.append(Paragraph("%s %s" % (unicode(runner), runner.number and 487 ("(%s)" % runner.number) or ''), 488 styles['Heading1'])) 489 elements.append(Paragraph("SI-Card: %s" % str(self._run.sicard.id), 490 styles['Normal'])) 491 course = self._run.course 492 elements.append(Paragraph("<b>%s</b>" % (course and course.code or 'unknown course'), 493 styles['Normal'])) 494 elements.append(Spacer(0,10)) 495 496 if validation and validation['status'] == Validator.OK: 497 elements.append(Paragraph('<b>Laufzeit %s</b>' % score['score'], styles['Normal'])) 498 elif validation: 499 elements.append(Paragraph('<b>%s</b>' % AbstractFormatter.validation_codes[validation['status']], 500 styles['Normal'])) 501 else: 502 elements.append(Paragraph('<b>Validation error: %s</b>' % validation_error.message, 503 styles['Normal'])) 504 elements.append(Spacer(0,10)) 505 506 (punchtable, tablestyles) = self._format_punchlist() 507 508 tablestyles = [('ALIGN', (0,0), (-1,-1), 'RIGHT'), 509 ('FONT', (0,0), (-1,-1), 'Helvetica'), 510 ('TOPPADDING', (0,0), (-1,-1), 0), 511 ('BOTTOMPADDING', (0,0), (-1,-1), 0), 512 ] + tablestyles 513 t = Table(punchtable, ) 514 t.setStyle(TableStyle(tablestyles)) 515 t.hAlign = 'LEFT' 516 elements.append(t) 517 elements.append(Spacer(0,20)) 518 519 elements.append(Paragraph("printed by Bosco, Free Orienteering Software, " 520 "http://bosco.durcheinandertal.ch", styles['Normal'])) 521 522 doc.build(elements) 523 524 return io.getvalue()
525
526 - def _format_punchlist(self, cols=10):
527 punchlist = self._punchlist(with_finish = True) 528 529 # format into triples (control, time, time_to_last) 530 punch_triples = [] 531 ctrl_nr = 1 532 for i,p in enumerate(punchlist): 533 if p[8] in ('ok', 'missing'): 534 control = "%i (%s)" % (ctrl_nr, p[1]) 535 ctrl_nr += 1 536 elif p[8] == 'finish': 537 control = "Finish" 538 elif p[8] in ('additional', 'ignored'): 539 control = "+ (%s)" % (p[1] != '' and p[1] or p[2]) 540 punch_triples.append((control, p[5] or 'missing', p[6])) 541 542 # rearrange punch_triples by row for the output table 543 i = 0 544 table = [] 545 styles = [] 546 row_items = ([], [], []) 547 while i < len(punchlist): 548 if i > 0 and i % cols == 0: 549 # new row 550 table.extend(row_items) 551 row_items = ([],[],[]) 552 rowcount = len(table) 553 styles.append(('TOPPADDING', (0,rowcount), (-1,rowcount), 20)) 554 555 for j in range(3): 556 row_items[j].append(punch_triples[i][j]) 557 i += 1 558 # add last row 559 table.extend(row_items) 560 return (table, styles)
561