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

Source Code for Module bosco.editor

   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  editor.py - High level editing classes. 
  18  """ 
  19  import sys 
  20   
  21  from datetime import datetime, date 
  22  from time import sleep 
  23  from subprocess import Popen, PIPE 
  24  from traceback import print_exc 
  25   
  26  from storm.exceptions import NotOneError, LostObjectError 
  27  from storm.expr import Func 
  28  from storm.locals import * 
  29   
  30  from sireader import SIReaderReadout, SIReaderException, SIReader 
  31   
  32  from runner import Team, Runner, SICard, Category, Club 
  33  from run import Run, Punch, RunException 
  34  from course import Control, SIStation, Course 
  35  from formatter import AbstractFormatter, ReportlabRunFormatter 
  36  from ranking import ValidationError, UnscoreableException, Validator, OpenRuns 
37 38 -class Observable(object):
39
40 - def __init__(self):
41 self._observers = []
42
43 - def add_observer(self, observer):
44 self._observers.append(observer)
45
46 - def remove_observer(self, observer):
47 try: 48 self._observers.remove(observer) 49 except ValueError: 50 pass
51
52 - def _notify_observers(self, event = None, message = None):
53 for o in self._observers: 54 o.update(self, event, message)
55
56 -class RunFinderException(Exception):
57 pass
58
59 -class RunFinder(Observable):
60 """Searches for runs and/or runners.""" 61 62 _search_config = {'run' : {'title' : u'Run', 63 'joins' : True, # no tables to join, always true 64 'terms' : ((Run.id, 'int'), 65 ), 66 }, 67 'sicard' : {'title' : u'SICard', 68 'joins' : True, # no tables to join, alway true 69 'terms' : ((Run.sicard, 'int'), 70 ), 71 }, 72 'runner' : {'title': u'Runner', 73 'joins' : And(Run.sicard == SICard.id, 74 SICard.runner == Runner.id), 75 'terms' : ((Runner.given_name, 'partial_string'), 76 (Runner.surname, 'partial_string'), 77 (Runner.number, 'exact_string'), 78 (Runner.solvnr, 'exact_string'), 79 ), 80 }, 81 'team' : {'title': u'Team', 82 'joins' : And(Run.sicard == SICard.id, 83 SICard.runner == Runner.id, 84 Runner.team == Team.id), 85 'terms' : ((Team.name, 'partial_string'), 86 (Team.number, 'exact_string'), 87 ), 88 }, 89 'category' : {'title': u'Category', 90 'joins' : And(Run.sicard == SICard.id, 91 SICard.runner == Runner.id, 92 Or(Runner.category == Category.id, 93 And(Runner.team == Team.id, 94 Team.category == Category.id) 95 ) 96 ), 97 'terms' : ((Category.name, 'exact_string'), 98 ), 99 }, 100 'course' : {'title': u'Course', 101 'joins' : And(Run.course == Course.id), 102 'terms' : ((Course.code, 'exact_string'), 103 ), 104 }, 105 } 106
107 - def __init__(self, store):
108 super(RunFinder, self).__init__() 109 self._store = store 110 self._term = '' 111 self._query = None 112 self.set_search_domain('runner')
113
114 - def get_results(self, limit = False, start = 0):
115 """Returns the results of the current search.""" 116 if self._query is None: 117 return [] 118 119 results = [] 120 for r in self._query.order_by(Run.id): 121 runner = r.sicard.runner 122 team = runner and runner.team or None 123 club = runner and runner.club and runner.club.name or None 124 results.append((r.id, 125 r.course and r.course.code and unicode(r.course.code) 126 or 'unknown', 127 r.readout_time and RunEditor._format_time(r.readout_time) or 'unknown', 128 runner and runner.number and unicode(runner.number) 129 or 'unknown', 130 runner and unicode(runner) or 'unknown', 131 team and unicode(team) or club and unicode(club) or 'unknown', 132 team and unicode(team.category) or runner and unicode(runner.category) 133 or 'unkown' 134 )) 135 136 return results
137
138 - def set_search_term(self, term):
139 """Set the search string""" 140 self._term = unicode(term).split() 141 self._update_query()
142
143 - def get_search_domains(self):
144 """ 145 @return List of (key, description) pairs of valid search domains. 146 """ 147 return [(k, v['title']) for k,v in self._search_config.items() ]
148
149 - def set_search_domain(self, domain):
150 """ 151 @param domain: set of search domains. Valid search domains are those 152 defined in _search_config. 153 """ 154 if domain in self._search_config.keys(): 155 self._domain = domain 156 self._update_query() 157 else: 158 raise RunFinderException("Search domain %s is invalid." % domain)
159
160 - def _update_query(self):
161 """Updates the internal search query.""" 162 term_condition = [] 163 for t in self._term: 164 condition_parts = [] 165 for column, col_type in self._search_config[self._domain]['terms']: 166 if col_type == 'int': 167 try: 168 condition_parts.append(column == int(t)) 169 except (ValueError, TypeError): 170 pass 171 elif col_type == 'partial_string': 172 try: 173 condition_parts.append(column.lower().like("%%%s%%" % unicode(t).lower())) 174 except (ValueError, TypeError): 175 pass 176 elif col_type == 'exact_string': 177 try: 178 condition_parts.append(column.lower() == unicode(t).lower()) 179 except (ValueError, TypeError): 180 pass 181 182 if len(condition_parts) > 0: 183 term_condition.append(Or(*condition_parts)) 184 185 if len(term_condition) == 0: 186 # An empty list of conditions should return no results 187 term_condition.append(False) 188 189 self._query = self._store.find(Run, 190 And(self._search_config[self._domain]['joins'], 191 *term_condition 192 ) 193 ) 194 self._notify_observers()
195
196 197 198 -class RunEditorException(Exception):
199 pass
200
201 -class RunEditor(Observable):
202 """High level run editor. This class is intended as a model for a 203 (graphical) editing front-end. 204 The editor only edits one run at a time. Runs can be loaded from the 205 database or read from an SI Reader station. If polling the reader is 206 enabled, the edited run may change at any time! 207 """ 208 209 _station_codes = {SIStation.START: 'start', 210 SIStation.FINISH: 'finish', 211 SIStation.CHECK: 'check', 212 SIStation.CLEAR: 'clear'} 213 max_progress = 7 214 215 # singleton instance 216 __single = None 217 __initialized = False 218
219 - def __new__(classtype, *args, **kwargs):
220 """ 221 Override class creation to ensure that only one RunEditor is instantiated. 222 """ 223 224 if classtype != type(classtype.__single): 225 classtype.__single = object.__new__(classtype, *args, **kwargs) 226 return classtype.__single
227
228 - def __init__(self, store, event):
229 """ 230 @param store: Storm store of the runs 231 @param event: object of class (or subclass of) Event. This is used for 232 run and team validation 233 @note: Later "instantiations" of this singleton discard all arguments. 234 """ 235 if self.__initialized == True: 236 return 237 238 Observable.__init__(self) 239 self._store = store 240 self._event = event 241 242 self._run = None 243 self.progress = None 244 self._is_changed = False 245 246 self._sireader = None 247 248 self._print_command = "lp -o media=A5" 249 250 self.__initialized = True
251 252 @staticmethod
253 - def _format_time(time, date = True):
254 format = date and '%x %X' or '%X' 255 return time.strftime(format)
256 257 run_readout_time = property(lambda obj: obj._run and obj._run.readout_time and RunEditor._format_time(obj._run.readout_time) or 'unknown') 258 run_clear_time = property(lambda obj: obj._run and obj._run.clear_time and RunEditor._format_time(obj._run.clear_time) or 'unknown') 259 run_check_time = property(lambda obj: obj._run and obj._run.check_time and RunEditor._format_time(obj._run.check_time) or 'unknown') 260 run_card_start_time = property(lambda obj: obj._run and obj._run.card_start_time and RunEditor._format_time(obj._run.card_start_time) or 'unknown') 261 run_manual_start_time = property(lambda obj: obj._run and obj._run.manual_start_time and RunEditor._format_time(obj._run.manual_start_time) or '') 262 run_start_time = property(lambda obj: obj._run and obj._run.start_time and RunEditor._format_time(obj._run.start_time) or 'unknown') 263 run_card_finish_time = property(lambda obj: obj._run and obj._run.card_finish_time and RunEditor._format_time(obj._run.card_finish_time) or 'unknown') 264 run_manual_finish_time = property(lambda obj: obj._run and obj._run.manual_finish_time and RunEditor._format_time(obj._run.manual_finish_time) or '') 265 run_finish_time = property(lambda obj: obj._run and obj._run.finish_time and RunEditor._format_time(obj._run.finish_time) or 'unknown') 266
267 - def has_runner(self):
268 return self.has_run() and self._run.sicard.runner is not None
269
270 - def has_course(self):
271 return self._run.course is not None
272
273 - def has_run(self):
274 return self._run is not None
275
276 - def _get_runner_name(self):
277 try: 278 return unicode(self._run.sicard.runner or '') 279 except AttributeError: 280 return ''
281 runner_name = property(_get_runner_name) 282 283 runner_given_name = property(lambda obj: obj._run and obj._run.sicard.runner and 284 obj._run.sicard.runner.given_name or '') 285 runner_surname = property(lambda obj: obj._run and obj._run.sicard.runner and 286 obj._run.sicard.runner.surname or '') 287 runner_dateofbirth = property(lambda obj: obj._run and obj._run.sicard.runner and 288 obj._run.sicard.runner.dateofbirth and 289 obj._run.sicard.runner.dateofbirth.strftime('%x') or '') 290 291 runner_club = property(lambda obj: obj._run and obj._run.sicard.runner and 292 obj._run.sicard.runner.club and 293 obj._run.sicard.runner.club.name or '') 294
295 - def _get_runner_number(self):
296 try: 297 return self._run.sicard.runner.number or '' 298 except AttributeError: 299 return ''
300 runner_number = property(_get_runner_number) 301 302 runner_category = property(lambda obj: obj._run and obj._run.sicard.runner and 303 obj._run.sicard.runner.category and 304 obj._run.sicard.runner.category.name or '') 305
306 - def _get_runner_team(self):
307 try: 308 return u'%3s: %s' % (self._run.sicard.runner.team.number, self._run.sicard.runner.team.name) 309 except AttributeError: 310 return ''
311 runner_team = property(_get_runner_team) 312
313 - def _get_runner_sicard(self):
314 try: 315 return unicode(self._run.sicard.id) 316 except AttributeError: 317 return ''
318 runner_sicard = property(_get_runner_sicard) 319
320 - def _get_run_id(self):
321 try: 322 return str(self._run.id) 323 except AttributeError: 324 return ''
325 run_id = property(_get_run_id) 326
327 - def _get_run_course(self):
328 try: 329 return self._run.course.code 330 except AttributeError: 331 return ''
332 run_course = property(_get_run_course) 333
334 - def _get_run_validation(self):
335 try: 336 validation = self._event.validate(self._run) 337 except ValidationError: 338 return '' 339 return AbstractFormatter.validation_codes[validation['status']]
340 run_validation = property(_get_run_validation) 341
342 - def _get_run_score(self):
343 try: 344 return unicode(self._event.score(self._run)['score']) 345 except UnscoreableException: 346 return ''
347 run_score = property(_get_run_score) 348
349 - def _get_run_override(self):
350 try: 351 override = self._run.override 352 except AttributeError: 353 return 0 354 if override is None: 355 return 0 356 else: 357 return override
358 run_override = property(_get_run_override) 359
360 - def _get_run_complete(self):
361 try: 362 return self._run.complete 363 except AttributeError: 364 return False
365 run_complete = property(_get_run_complete) 366
367 - def _get_team_validation(self):
368 na = 'NA' 369 if self._run is None: 370 return '' 371 372 try: 373 team = self._run.sicard.runner.team 374 except AttributeError: 375 return na 376 if team is None: 377 return na 378 try: 379 validation = self._event.validate(team) 380 except ValidationError: 381 return '' 382 return AbstractFormatter.validation_codes[validation['status']]
383 team_validation = property(_get_team_validation) 384
385 - def _get_team_score(self):
386 try: 387 return unicode(self._event.score(self._run.sicard.runner.team)['score']) 388 except (UnscoreableException, AttributeError): 389 return ''
390 team_score = property(_get_team_score) 391
392 - def _raw_punchlist(self):
393 try: 394 punchlist = self._event.validate(self._run)['punchlist'] 395 except ValidationError: 396 if self._run is None: 397 return [] 398 399 # create pseudo validation result 400 punchlist = [ ('ignored', p) for p in self._run.punches.order_by(Func('COALESCE', Punch.manual_punchtime, Punch.card_punchtime))] 401 402 # add finish punch if it does not have one 403 if self._run.finish_time is None: 404 punchlist.append(('missing', 405 self._store.get(SIStation, SIStation.FINISH))) 406 return punchlist
407
408 - def _get_punchlist(self):
409 410 def StationCode(si): 411 if si <= SIStation.SPECIAL_MAX: 412 return RunEditor._station_codes[si] 413 else: 414 return str(si)
415 416 punchlist = [] 417 for code, p in self._raw_punchlist(): 418 if type(p) == Punch: 419 punchlist.append((p.sequence and str(p.sequence) or '', 420 p.sistation.control and p.sistation.control.code or '', 421 StationCode(p.sistation.id), 422 p.card_punchtime and RunEditor._format_time(p.card_punchtime) or '', 423 p.manual_punchtime and RunEditor._format_time(p.manual_punchtime) or '', 424 str(int(p.ignore)), 425 str(code))) 426 elif type(p) == Control: 427 punchlist.append(('', 428 p.code, 429 '', 430 '', 431 '', 432 str(int(False)), 433 code)) 434 elif type(p) == SIStation: 435 punchlist.append(('', 436 '', 437 StationCode(p.id), 438 '', 439 '', 440 str(int(False)), 441 code)) 442 return punchlist
443 punchlist = property(_get_punchlist) 444 445 print_command = property(lambda x:x._print_command) 446
447 - def _clear_cache(self):
448 try: 449 self._event.clear_cache(self._run) 450 except KeyError: 451 pass 452 try: 453 if self._run.sicard.runner.team is not None: 454 self._event.clear_cache(self._run.sicard.runner.team) 455 except (AttributeError, KeyError): 456 pass
457
458 - def get_runnerlist(self):
459 runners = [(None, '')] 460 for r in self._store.find(Runner).order_by('number'): 461 runners.append((r.id, u'%s: %s' % (r.number, r))) 462 return runners
463
464 - def get_teamlist(self):
465 teams = [(None, '')] 466 for t in self._store.find(Team).order_by('number'): 467 teams.append((t.id, u'%3s: %s' % (t.number, t.name))) 468 return teams
469
470 - def _create_virtual_sicard(self):
471 """ 472 Creates a virtual SI-Card used when no real card number is 473 available. 474 """ 475 min_id = self._store.find(SICard).min(SICard.id) - 1 476 if min_id > -1: 477 min_id = -1 478 return SICard(min_id)
479
480 - def set_runner(self, runner):
481 """ 482 Changes the runner of the current run. If no runner is found 483 the run is disconnected from the current runner. If the current 484 runner has multiple runs, a "virtual" sicard is created. 485 @param runner: ID of the new runner 486 """ 487 488 if self._run is None: 489 return 490 491 runner = self._store.get(Runner, runner) 492 493 if runner == self._run.sicard.runner: 494 # return and don't commit if runner did not change 495 return 496 497 if runner is None: 498 # connect to a virtual sicard not belonging to any runner 499 # leaves the sicard with the runner so to not reconnect this 500 # run if another run with this sicard is created 501 self._run.sicard = self._create_virtual_sicard() 502 elif self._run.sicard.runner is None or self._run.sicard.runs.count() == 1: 503 self._run.sicard.runner = runner 504 else: 505 # run already belongs to another runner 506 # and there are other runs connected to this sicard 507 # create a new 'virtual' sicard 508 si = self._create_virtual_sicard() 509 self._run.sicard = si 510 runner.sicards.add(si) 511 512 self.commit()
513
514 - def set_runner_number(self, n, force = False):
515 516 # unset number? 517 if n == '': 518 if self._run.sicard.runner is not None: 519 self._run.sicard.runner.number = None 520 self.commit() 521 return 522 523 # numbers must be unique. See if there is already another runner 524 # with this number 525 prev_runner = self._store.find(Runner, Runner.number == n).one() 526 527 # Attention: check for prev_runnner first, None is None == True ! 528 if prev_runner and prev_runner is self._run.sicard.runner: 529 # same runner as current runner, nothing to be done 530 return 531 532 if prev_runner is not None and force is False: 533 raise RunEditorException(u'Runner %s (%s) already has this number.' % (unicode(prev_runner), prev_runner.number)) 534 535 if not self.has_runner(): 536 self._run.sicard.runner = Runner() 537 538 if prev_runner is not None: 539 # unset number on previous runner, force is True by now 540 prev_runner.number = None 541 542 self._run.sicard.runner.number = n 543 self.commit()
544
545 - def set_runner_category(self, cat_name):
546 547 if not self.has_runner(): 548 self._run.sicard.runner = Runner() 549 550 cat = self._store.find(Category, Category.name == cat_name).one() 551 self._run.sicard.runner.category = cat 552 self.set_course(cat_name) 553 self.commit()
554
555 - def set_runner_given_name(self, n):
556 try: 557 self._run.sicard.runner.given_name = n 558 except AttributeError: 559 self._run.sicard.runner = Runner(given_name = n) 560 561 self.commit()
562
563 - def set_runner_surname(self, n):
564 try: 565 self._run.sicard.runner.surname = n 566 except AttributeError: 567 self._run.sicard.runner = Runner(surname = n) 568 569 self.commit()
570
571 - def set_runner_dateofbirth(self, d):
572 573 # First convert date string to a date object. If this fails 574 # it raises an Exception which should be handeld by the caller. 575 if d == '': 576 d = None 577 else: 578 d = datetime.strptime(d, '%x').date() 579 580 if self._run.sicard.runner is None: 581 self._run.sicard.runner = Runner() 582 583 self._run.sicard.runner.dateofbirth = d 584 self.commit()
585
586 - def set_runner_team(self, team):
587 588 if team is not None: 589 team = self._store.get(Team, team) 590 591 if self._run.sicard.runner is None: 592 self._run.sicard.runner = Runner() 593 594 self._run.sicard.runner.team = team 595 self.commit()
596
597 - def set_runner_club(self, clubname):
598 599 if clubname != '': 600 club = self._store.find(Club, Club.name == clubname).one() 601 if club is None: 602 club = Club(clubname) 603 else: 604 club = None 605 606 if self._run.sicard.runner is None: 607 self._run.sicard.runner = Runner() 608 609 self._run.sicard.runner.club = club 610 self.commit()
611
612 - def set_course(self, course):
613 if course == '': 614 course = None 615 try: 616 self._run.set_coursecode(course) 617 except RunException: 618 pass 619 else: 620 self.commit()
621
622 - def set_override(self, override):
623 if override == 0: 624 override = None 625 self._run.override = override 626 self.commit()
627
628 - def set_complete(self, complete):
629 self._run.complete = complete 630 self.commit()
631
632 - def parse_time(self, time):
633 if time == '': 634 return None 635 636 # try date and time in current locale 637 try: 638 return datetime.strptime(time, '%x %X') 639 except ValueError: 640 pass 641 642 # try only time in current locale 643 try: 644 t = datetime.strptime(time, '%X') 645 today = date.today() 646 return t.replace(year=today.year, month=today.month, day=today.day) 647 except ValueError: 648 pass 649 650 # try YYYY-mm-dd hh:mm:ss representation 651 try: 652 return datetime.strptime(time, '%Y-%m-%d %H:%M:%S') 653 except ValueError: 654 pass 655 656 # try hh:mm:ss 657 try: 658 t = datetime.strptime(time, '%H:%M:%S') 659 today = date.today() 660 return t.replace(year=today.year, month=today.month, day=today.day) 661 except ValueError: 662 pass 663 664 # we just don't know how to interpret this time 665 return None
666
667 - def set_manual_start_time(self, time):
668 self._run.manual_start_time = self.parse_time(time) 669 self.commit()
670
671 - def set_manual_finish_time(self, time):
672 self._run.manual_finish_time = self.parse_time(time) 673 self.commit()
674
675 - def set_punchtime(self, punch, time):
676 677 punch = self._raw_punchlist()[punch] 678 if punch[0] == 'missing': 679 # Create new manual punch with this time 680 if type(punch[1]) == Control: 681 si = punch[1].sistations.any() 682 elif type(punch[1]) == SIStation: 683 si = punch[1] 684 self._run.punches.add(Punch(si, 685 manual_punchtime = self.parse_time(time))) 686 elif punch[1].card_punchtime is None and time == '': 687 # remove punch if both times would become None 688 self._store.remove(punch[1]) 689 else: 690 punch[1].manual_punchtime = self.parse_time(time) 691 692 self.commit()
693
694 - def set_ignore(self, punch, ignore):
695 punch = self._raw_punchlist()[punch][1] 696 if ignore == '' or ignore == '0': 697 punch.ignore = False 698 elif ignore == '1': 699 punch.ignore = True 700 701 self.commit()
702
703 - def load(self, run):
704 """ 705 Loads a run into the editor. All uncommited changes to the previously 706 loaded run will be lost! 707 @param run: run id 708 """ 709 self.rollback() 710 self._run = self._store.get(Run, run) 711 self._clear_cache() 712 self._notify_observers('run')
713
714 - def new(self, si_nr = None):
715 """ 716 Create a new empty run. This rolls back any uncommited changes! 717 @param si_nr: number of the SI-Card 718 """ 719 self.rollback() 720 if si_nr is None: 721 sicard = self._create_virtual_sicard() 722 else: 723 sicard = self._store.get(SICard, si_nr) 724 if sicard is None: 725 sicard = SICard(si_nr) 726 self._run = self._store.add(Run(sicard)) 727 self.commit()
728
729 - def new_from_reader(self):
730 """ 731 Creates a new empty run for the card currently inserted into the reader. 732 If an open run for this card already exists, this run is loaded and no 733 new run created. 734 """ 735 si_nr = self._sireader.sicard 736 if not si_nr: 737 raise RunEditorException("Could not read SI-Card.") 738 739 self.rollback() 740 self._run = None 741 sicard = self._store.get(SICard, si_nr) 742 if sicard is None: 743 sicard = SICard(si_nr) 744 else: 745 # try to load existing open run 746 self._run = self._store.find(Run, 747 Run.sicard == si_nr, 748 Run.complete == False).order_by(Run.id).last() 749 750 if self._run is None: 751 # Create new run 752 self._run = self._store.add(Run(sicard)) 753 754 self.commit() 755 self._sireader.ack_sicard()
756
757 - def delete(self):
758 """ 759 Deletes the current run. 760 """ 761 # first remove all punches 762 for p in self._run.punches: 763 self._store.remove(p) 764 self._store.remove(self._run) 765 self._run = None 766 self.commit()
767
768 - def commit(self):
769 """Commit changes to the database.""" 770 try: 771 self._store.commit() 772 # some errors (notably LostObjectError) only occur when the object 773 # is accessed again, clear cache triggers these 774 self._clear_cache() 775 except LostObjectError, e: 776 # run got removed during the transaction 777 self._run = None 778 self._notify_observers('error', str(e)) 779 except Exception, e: 780 print_exc(file=sys.stderr) 781 self._notify_observers('error', str(e)) 782 783 self._notify_observers('run')
784
785 - def rollback(self):
786 """Rollback all changes.""" 787 self._store.rollback() 788 789 if self._run is not None: 790 try: 791 tmp = self._run.sicard 792 except LostObjectError: 793 # Clear run if it got lost by the rollback action 794 # Store.of(self._run) still returns the store because it 795 # may reference objects in the store. Use this hack instead. 796 self._run = None 797 798 # But notify observers that the object may have changed and clear 799 # the cache 800 self._clear_cache() 801 self._notify_observers('run')
802
803 - def _get_port(self):
804 if self._sireader is None: 805 return 'not connected' 806 else: 807 return '%s (at %s baud)' % (self._sireader.port, 808 self._sireader.baudrate)
809 port = property(_get_port) 810
811 - def _get_status(self):
812 if self._sireader is None: 813 return '' 814 elif self.sicard is None: 815 return 'No SI-Card inserted.' 816 elif self.sicard_runner is not None: 817 return 'SI-Card %s of runner %s inserted.' % (self.sicard, 818 self.sicard_runner) 819 else: 820 return 'SI-Card %s inserted.' % self.sicard
821 status = property(_get_status) 822
823 - def _get_progress(self):
824 return self._progress
825 - def _set_progress(self, msg):
826 if msg is None: 827 # reset progress 828 self._progress = (0, 'Waiting for SI-Card...') 829 else: 830 # increase by one 831 self._progress = (self._progress[0] + 1, msg) 832 self._notify_observers('progress')
833 progress = property(_get_progress, _set_progress) 834
835 - def _get_sicard(self):
836 if self._sireader is not None: 837 return self._sireader.sicard 838 else: 839 return None
840 sicard = property(_get_sicard) 841
842 - def _get_sicard_runner(self):
843 if self.sicard is not None: 844 runner = self._store.find(Runner, 845 SICard.id == self.sicard, 846 SICard.runner == Runner.id).one() 847 if runner is None: 848 return None 849 else: 850 string = '%s: %s %s' % (runner.number, 851 runner.given_name, 852 runner.surname) 853 if runner.team is None: 854 return string 855 else: 856 return '%s (%s)' % (string, runner.team.name)
857 sicard_runner = property(_get_sicard_runner) 858
859 - def print_run(self):
860 f = ReportlabRunFormatter(self._run, self._event._header, self._event) 861 Popen(self._print_command, shell=True, stdin=PIPE).communicate(input=str(f))
862
863 - def connect_reader(self, port = None):
864 """ 865 Connect an SI-Reader 866 @param port: serial port name, default autodetected 867 """ 868 fail_reasons = [] 869 try: 870 self._sireader = SIReaderReadout(port) 871 except SIReaderException, e: 872 fail_reasons.append(e.message) 873 else: 874 875 # check for correct reader configuration 876 fail_template = "Wrong SI-Reader configuration: %s" 877 config = self._sireader.proto_config 878 if config['auto_send'] == True: 879 fail_reasons.append(fail_template % "Autosend is enabled.") 880 if config['ext_proto'] == False: 881 fail_reasons.append(fail_template % "Exended protocol is not enabled.") 882 if config['mode'] != SIReader.M_READOUT: 883 fail_reasons.append(fail_template % "Station is not in readout mode.") 884 885 if len(fail_reasons) > 0: 886 self._sireader = None 887 888 self._notify_observers('reader') 889 890 if len(fail_reasons) > 0: 891 raise RunEditorException("\n".join(fail_reasons))
892
893 - def poll_reader(self):
894 """Polls the sireader for changes.""" 895 if self._sireader is not None: 896 try: 897 if self._sireader.poll_sicard(): 898 self._notify_observers('reader') 899 except IOError: 900 # try to reconnect on error 901 try: 902 self._sireader.reconnect() 903 except SIReaderException: 904 # reconnection failed, disconnect reader and reraise 905 self._sireader = None 906 self._notify_observers('reader') 907 raise
908
909 - def load_run_from_card(self):
910 """Read out card data and create or load a run based on this data.""" 911 912 if self.sicard is None: 913 return 914 915 # clear current run 916 self._run = None 917 918 # wrap everything in a try - except block to be able to roll back 919 # on error 920 try: 921 self.rollback() 922 self.progress = None 923 self.progress = 'Reading card data...' 924 925 card_data = self._sireader.read_sicard() 926 927 # find complete runs with this sicard 928 self.progress = 'Searching for matching run...' 929 runs = self._store.find(Run, 930 Run.sicard == card_data['card_number'], 931 Run.complete == True) 932 933 for r in runs: 934 if self._compare_run(r, card_data): 935 self._run = r 936 self._sireader.ack_sicard() 937 return 938 939 # search for incomplete run with this sicard 940 self.progress = 'Searching for open run...' 941 try: 942 self._run = self._store.find(Run, 943 Run.sicard == card_data['card_number'], 944 Run.complete == False).one() 945 except NotOneError: 946 self._run = None 947 948 if self._run is None: 949 # Create new run 950 self.progress = 'Creating new run and adding punches...' 951 self._run = Run(card_data['card_number'], 952 punches = card_data['punches'], 953 card_start_time = card_data['start'], 954 check_time = card_data['check'], 955 clear_time = card_data['clear'], 956 card_finish_time = card_data['finish'], 957 readout_time = datetime.now(), 958 store = self._store) 959 else: 960 self.progress = 'Adding punches to existing run...' 961 self._run.card_start_time = card_data['start'] 962 self._run.card_finish_time = card_data['finish'] 963 self._run.card_check_time = card_data['check'] 964 self._run.card_clear_time = card_data['clear'] 965 if not self._run.readout_time: 966 self._run.readout_time = datetime.now() 967 self._run.add_punchlist(card_data['punches']) 968 969 # mark run as complete 970 self._run.complete = True 971 972 if self._run.course is None: 973 self.progress = 'Searching matching course ...' 974 # Search Course for this run 975 courses = self._store.find(Course) 976 self._clear_cache() 977 for c in courses: 978 self._run.course = c 979 valid = self._event.validate(self._run) 980 if valid['status'] == Validator.OK: 981 break 982 else: 983 self._run.course = None 984 self._clear_cache() 985 else: 986 self.progress = 'Course already set.' 987 988 self.progress = 'Updating validation...' 989 self.progress = 'Commiting run to database...' 990 self.commit() 991 self._sireader.ack_sicard() 992 993 finally: 994 # roll back and re-raise the exception 995 self.rollback() 996 self.progress = None 997 998 self.progress = None
999 1000
1001 - def _compare_run(self, run, card_data):
1002 """ 1003 Compares run to card_data 1004 @return: True if card_data matches run, False otherwise 1005 """ 1006 if run.sicard.id != card_data['card_number']: 1007 return False 1008 1009 punches = run.punches.find(Not(Punch.card_punchtime == None)) 1010 if punches.count() != len(card_data['punches']): 1011 # different punch count 1012 return False 1013 1014 # compare punches 1015 card_data['punches'].sort(key = lambda x: x[1]) 1016 for i,p in enumerate(punches.order_by('card_punchtime')): 1017 if not p.card_punchtime == card_data['punches'][i][1]: 1018 return False 1019 1020 # compare start and finish time 1021 if not run.card_start_time == card_data['start']: 1022 return False 1023 if not run.card_finish_time == card_data['finish']: 1024 return False 1025 1026 return True
1027
1028 - def set_print_command(self, command):
1029 self._print_command = command
1030
1031 -class RunListFormatter(object):
1032
1033 - def format_run(self, run):
1034 1035 def format_runner(runner): 1036 if run.sicard.runner is None: 1037 return '' 1038 elif run.sicard.runner.number is None: 1039 return unicode(runner) 1040 else: 1041 return unicode('%3s: %s' % (run.sicard.runner.number, 1042 run.sicard.runner))
1043 1044 # run validation and score 1045 try: 1046 code = self._event.validate(run)['status'] 1047 validation = AbstractFormatter.validation_codes[code] 1048 except ValidationError: 1049 validation = '' 1050 try: 1051 score = self._event.score(run) 1052 except UnscoreableException: 1053 score = {'start':'', 'finish':'', 'score':''} 1054 1055 # team validation and score 1056 team = run.sicard.runner and run.sicard.runner.team or None 1057 try: 1058 code = self._event.validate(team)['status'] 1059 team_validation = AbstractFormatter.validation_codes[code] 1060 except ValidationError: 1061 team_validation = '' 1062 1063 return (str(run.id), 1064 run.course and run.course.code or '', 1065 format_runner(run.sicard.runner), 1066 str(run.sicard.id), 1067 team and team.name or '', 1068 str(score.has_key('start') and score['start'] or ''), 1069 str(score.has_key('finish') and score['finish'] or ''), 1070 validation, 1071 team_validation, 1072 str(score['score']))
1073
1074 -class TeamEditor(Observable, RunListFormatter):
1075
1076 - def __init__(self, store, event):
1077 """ 1078 @param store: Storm store of the runs 1079 @param event: object of class (or subclass of) Event. This is used for 1080 run and team validation 1081 """ 1082 Observable.__init__(self) 1083 self._store = store 1084 self._event = event 1085 1086 self._team = None
1087
1088 - def get_teamlist(self):
1089 teams = [] 1090 for t in self._store.find(Team).order_by('number'): 1091 teams.append((t.id, '%3s: %s' % (t.number, t.name))) 1092 1093 return teams
1094
1095 - def _get_validation(self):
1096 try: 1097 validation = self._event.validate(self._team) 1098 except (ValidationError, AttributeError): 1099 return '' 1100 return AbstractFormatter.validation_codes[validation['status']]
1101 validation = property(_get_validation) 1102
1103 - def _get_score(self):
1104 try: 1105 return unicode(self._event.score(self._team)['score']) 1106 except (UnscoreableException, AttributeError): 1107 return ''
1108 score = property(_get_score) 1109
1110 - def _get_runs(self):
1111 1112 if self._team is None: 1113 return [] 1114 1115 runs = self._team.runs 1116 runs.sort(key = lambda x: x.finish_time or datetime.max) 1117 result = [] 1118 for r in runs: 1119 result.append(self.format_run(r)) 1120 1121 return result
1122 runs = property(_get_runs) 1123
1124 - def load(self, team):
1125 self._store.rollback() 1126 self._team = self._store.get(Team, team) 1127 self._notify_observers()
1128
1129 -class Reports(RunListFormatter):
1130 1131 # list of supported reports 1132 # these are (method, descriptive name) tuples 1133 # the method should return a list of Run objects 1134 _supported_reports = (('_open_runs', 'Open runs' ), 1135 ('_orphan_runs', 'Orphan runs'), 1136 ) 1137
1138 - def __init__(self, store, event):
1139 """ 1140 @param store: Storm store of the runs 1141 @param event: object of class (or subclass of) Event. This is used for 1142 run and team validation 1143 """ 1144 self._store = store 1145 self._event = event 1146 self._report = None
1147
1148 - def list_reports(self):
1150
1151 - def set_report(self, report):
1152 if report in [r[0] for r in Reports._supported_reports]: 1153 self._report = report
1154
1155 - def _get_runs(self):
1156 runs = getattr(self, self._report)() 1157 result = [] 1158 for r in runs: 1159 result.append(self.format_run(r)) 1160 return result
1161 runs = property(_get_runs) 1162
1163 - def _open_runs(self):
1164 """Lists all runs that are not yet completed.""" 1165 return OpenRuns(self._store).members
1166
1167 - def _orphan_runs(self):
1168 """Lists all runs that don't belong to any runner.""" 1169 return self._store.find(Run, 1170 And(Run.sicard == SICard.id, 1171 SICard.runner == None, 1172 ) 1173 )
1174