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