1   
   2   
   3   
   4   
   5   
   6   
   7   
   8   
   9   
  10   
  11   
  12   
  13   
  14   
  15   
  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 
  39   
  42           
  45   
  47          try: 
  48              self._observers.remove(observer) 
  49          except ValueError: 
  50              pass 
   51   
  53          for o in self._observers: 
  54              o.update(self, event, message) 
    55   
  58   
  60      """Searches for runs and/or runners.""" 
  61   
  62      _search_config = {'run'      : {'title' : u'Run', 
  63                                      'joins' : True,  
  64                                      'terms' : ((Run.id, 'int'), 
  65                                                 ), 
  66                                      }, 
  67                        'sicard'   : {'title' : u'SICard', 
  68                                      'joins' : 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   
 113   
 137   
 139          """Set the search string""" 
 140          self._term = unicode(term).split() 
 141          self._update_query() 
  142   
 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   
 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               
 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   
 200   
 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       
 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       
 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 
 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   
 269   
 271          return self._run.course is not None 
  272   
 274          return self._run is not None 
  275       
 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       
 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   
 311      runner_team = property(_get_runner_team) 
 312   
 314          try: 
 315              return unicode(self._run.sicard.id) 
 316          except AttributeError: 
 317              return '' 
  318      runner_sicard = property(_get_runner_sicard) 
 319   
 321          try: 
 322              return str(self._run.id) 
 323          except AttributeError: 
 324              return '' 
  325      run_id = property(_get_run_id) 
 326       
 328          try: 
 329              return self._run.course.code 
 330          except AttributeError: 
 331              return '' 
  332      run_course = property(_get_run_course) 
 333   
 340      run_validation = property(_get_run_validation) 
 341   
 347      run_score = property(_get_run_score) 
 348   
 358      run_override = property(_get_run_override) 
 359   
 361          try: 
 362              return self._run.complete 
 363          except AttributeError: 
 364              return False 
  365      run_complete = property(_get_run_complete) 
 366   
 383      team_validation = property(_get_team_validation) 
 384   
 390      team_score = property(_get_team_score) 
 391       
 407           
 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   
 457           
 463   
 469   
 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   
 513   
 544   
 554               
 562   
 570   
 585   
 596   
 611       
 621   
 627   
 631   
 633          if time == '': 
 634              return None 
 635   
 636           
 637          try: 
 638              return datetime.strptime(time, '%x %X') 
 639          except ValueError: 
 640              pass 
 641   
 642           
 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           
 651          try: 
 652              return datetime.strptime(time, '%Y-%m-%d %H:%M:%S') 
 653          except ValueError: 
 654              pass 
 655   
 656           
 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           
 665          return None 
  666   
 670   
 674       
 693           
 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): 
  728   
 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               
 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               
 752              self._run = self._store.add(Run(sicard)) 
 753   
 754          self.commit() 
 755          self._sireader.ack_sicard() 
  756   
 758          """ 
 759          Deletes the current run. 
 760          """ 
 761           
 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           
 784   
 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                   
 794                   
 795                   
 796                  self._run = None 
 797               
 798               
 799               
 800              self._clear_cache() 
 801              self._notify_observers('run') 
  802   
 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       
 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   
 824          return self._progress 
  826          if msg is None: 
 827               
 828              self._progress = (0, 'Waiting for SI-Card...') 
 829          else: 
 830               
 831              self._progress = (self._progress[0] + 1, msg) 
 832          self._notify_observers('progress') 
  833      progress = property(_get_progress, _set_progress) 
 834   
 836          if self._sireader is not None: 
 837              return self._sireader.sicard 
 838          else: 
 839              return None 
  840      sicard = property(_get_sicard) 
 841   
 857      sicard_runner = property(_get_sicard_runner) 
 858   
 860          f = ReportlabRunFormatter(self._run, self._event._header, self._event) 
 861          Popen(self._print_command, shell=True, stdin=PIPE).communicate(input=str(f)) 
  862   
 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               
 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           
 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                   
 901                  try: 
 902                      self._sireader.reconnect() 
 903                  except SIReaderException: 
 904                       
 905                      self._sireader = None 
 906                      self._notify_observers('reader') 
 907                      raise 
  908   
 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           
 916          self._run = None 
 917   
 918           
 919           
 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               
 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               
 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                   
 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               
 970              self._run.complete = True 
 971               
 972              if self._run.course is None: 
 973                  self.progress = 'Searching matching course ...' 
 974                   
 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               
 995              self.rollback() 
 996              self.progress = None 
 997           
 998          self.progress = None 
  999           
1000           
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               
1012              return False 
1013   
1014           
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           
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   
1029          self._print_command = command 
 1030   
1073   
1075   
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   
1094   
1101      validation = property(_get_validation) 
1102   
1104          try: 
1105              return unicode(self._event.score(self._team)['score']) 
1106          except (UnscoreableException, AttributeError): 
1107              return '' 
 1108      score = property(_get_score) 
1109   
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): 
  1128   
1130   
1131       
1132       
1133       
1134      _supported_reports = (('_open_runs',   'Open runs'  ), 
1135                            ('_orphan_runs', 'Orphan runs'), 
1136                            ) 
1137   
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   
1150   
1154   
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   
1166   
 1174