1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 from csv import reader, writer, Sniffer, Error
21 from datetime import datetime, date
22 from time import sleep
23 from sys import exit, hexversion
24 if hexversion > 0x20500f0:
25 from xml.etree.ElementTree import parse
26 else:
27 from elementtree.ElementTree import parse
28 import re
29 from os import fsync
30
31 from storm.locals import *
32 from storm.exceptions import NotOneError, IntegrityError
33
34 from psycopg2 import DataError
35
36 from runner import Runner, Team, SICard, Category, Country, Club
37 from course import Control, Course, Course, SIStation
38 from run import Run, Punch
41 """Base class for all Importer classes to import event
42 data. This class provides the general interface for (GUI)
43 import frontends."""
44
47
49 """Import runner data into store. Creates all the necessary objects
50 and add them to the store, but don't commit the store."""
51 pass
52
54 """Import form a CSV file. The first line of the file contains
55 descriptive labels for the values. All further lines are read
56 into a list of dictionaries using these labels."""
57
58 - def __init__(self, fname, encoding, verbose = False):
59
60 self._verbose = verbose
61
62
63 self.data = []
64
65
66 fh = open(fname, 'rb')
67 try:
68 dialect = Sniffer().sniff(fh.read(1024))
69 fh.seek(0)
70 csv = reader(fh, dialect = dialect)
71 except Error:
72 fh.seek(0)
73 csv = reader(fh, delimiter="\t")
74
75
76 labels = [ v.strip() for v in csv.next() ]
77 self._fieldcount = len(labels)
78
79
80 for line in csv:
81 try:
82 if line[0].strip()[0] == '#':
83
84 continue
85 except IndexError:
86 pass
87 d = {}
88 for i,v in enumerate(line):
89 d[labels[i]] = v.decode(encoding).strip()
90
91 self.data.append(d)
92
94 """Import Runner data from CSV file.
95 This class currently only consists of helper functions for derived classes.
96 """
97
98 @staticmethod
100 """Parses the year of birth
101 @rtype: date or None
102 """
103 if yob is None:
104 return None
105 try:
106 return date(int(yob), 1, 1)
107 except ValueError:
108 return None
109
110 @staticmethod
127
128 @staticmethod
129 - def _add_sicard(runner, sicard, store, force = False):
130 """Add SI Card to runner if it is valid.
131 @return One of the SICARD_* constants above.
132 """
133
134 if sicard and sicard.runner == runner:
135
136 return
137 elif sicard and sicard.runner:
138 if force:
139 print ("Forcing reassignment of SI-Card %s from runner %s %s (%s) "
140 "to runner %s %s (%s)." %
141 (sicard.id, sicard.runner.given_name,sicard.runner.surname,
142 sicard.runner.number, runner.given_name, runner.surname,
143 runner.number))
144 sicard.runner = None
145 else:
146 raise AlreadyAssignedSICardException("SI-Card %s is already assigned to runner "
147 "%s %s (%s). Not assigning any card to "
148 "runner %s %s (%s)." %
149 (sicard.id, sicard.runner.given_name,
150 sicard.runner.surname,
151 sicard.runner.number, runner.given_name,
152 runner.surname, runner.number))
153
154 runner.sicards.add(sicard)
155
156 @staticmethod
158 if sex is None:
159 return None
160 sex = sex.lower()
161 return (sex == 'm' and 'male'
162 or sex == 'f' and 'female'
163 or None)
164
166 """Import runners from the SOLV runner database. Uses what the SOLV calls
167 a VELPOZ data file. This can also be used to import reduced files which do not
168 contain all the columns of the SOLV database.
169 Additionally to the fields of the SOLV database the fields "Angemeldete_Kategorie" and
170 "Bahn" are supported. They link the runner to the given Category and create an open run
171 for the given Course respectively.
172 """
173
175
176 for i, r in enumerate(self.data):
177 if self._verbose:
178 print "%i: Adding %s %s" % (i+1, r.get('Vorname', u''),
179 r.get('Name', u''))
180
181 try:
182
183 try:
184 sicard = RunnerImporter._get_sicard(r.get('SI_Karte', None), store)
185 except InvalidSICardException, e:
186 print ("Runner %s %s (%s): %s" %
187 (r.get('Vorname', u''), r.get('Name', u''), r.get('SOLV-Nr', u''), e.message))
188 sicard = None
189 except NoSICardException, e:
190 sicard = None
191 else:
192 if sicard.runner and not (sicard.runner.given_name == r.get('Vorname', None) and
193 sicard.runner.surname == r.get('Name', None)):
194
195
196 print ("SI-card %i already assigned to runner %s %s. Can't assign to "
197 "runner %s %s on line %i" %
198 (sicard.id, sicard.runner.given_name, sicard.runner.surname,
199 r.get('Vorname'), r.get('Name'), i+2))
200 sicard = None
201
202
203
204 solvnr = r.get('SOLV-Nr', None) or None
205 startnumber = r.get('Startnummer', None) or None
206 runner = runner_solv = runner_number = runner_sicard = None
207 if solvnr:
208 runner_solv = store.find(Runner, Runner.solvnr == solvnr).one()
209 if startnumber:
210 runner_number = store.find(Runner, Runner.number == startnumber).one()
211 if sicard:
212 runner_sicard = sicard.runner
213
214 if ((runner_solv or runner_number or runner_sicard)
215 and len(set((runner_solv, runner_number, runner_sicard)) - set((None, ))) > 1):
216
217
218 print ("SOLV Number %s, Start Number %s or SI-card %s are already in the "
219 "database and assigned to different runners. Skipping "
220 "entry for %s %s on line %i" %
221 (r.get('SOLV-Nr', u''), r.get('Startnummer', u''), r.get('SI-Karte', u''), r.get('Vorname', u''), r.get('Name', u''),
222 i+2))
223 continue
224
225 if runner_solv:
226 runner = runner_solv
227 print ("Runner %s %s with SOLV Number %s already exists. "
228 "Updating information." %
229 (runner.given_name, runner.surname, runner.solvnr)
230 )
231 elif runner_number:
232 runner = runner_number
233 print ("Runner %s %s with Start Number %s already exists. "
234 "Updating information." %
235 (runner.given_name, runner.surname, runner.number)
236 )
237 elif runner_sicard:
238 runner = runner_sicard
239 print ("Runner %s %s with SI-card %s already exists. "
240 "Updating information." %
241 (runner.given_name, runner.surname, sicard.id)
242 )
243 else:
244 runner = store.add(Runner(solvnr=solvnr, number=startnumber))
245
246 if sicard:
247 RunnerImporter._add_sicard(runner, sicard, store)
248
249 clubname = r.get('Verein', None)
250 if clubname:
251 club = store.find(Club, Club.name == clubname).one()
252 else:
253 club = None
254 if not club and clubname is not None:
255 club = Club(r.get('Verein', u''))
256
257 runner.given_name = r.get('Vorname', None)
258 runner.surname = r.get('Name', None)
259 runner.dateofbirth = RunnerImporter._parse_yob(r.get('Jahrgang', None))
260 runner.sex = RunnerImporter._parse_sex(r.get('Geschlecht', None))
261 nationname = r.get('Nation', None)
262 if nationname:
263 runner.nation = store.find(Country, Country.code3 == nationname).one()
264 runner.solvnr = solvnr
265 runner.club = club
266 runner.address1 = r.get('Adressz1', None)
267 runner.address2 = r.get('Adressz2', None)
268 runner.zipcode = r.get('PLZ', None)
269 runner.city = r.get('Ort', None)
270 countryname = r.get('Land', None)
271 if countryname:
272 runner.address_country = store.find(Country, Country.code2 == countryname).one()
273 runner.email = r.get('Email', None)
274 runner.preferred_category = r.get('Kategorie', None)
275 dop = r.get('Dop.Stat', None)
276 if dop is not None:
277 runner.doping_declaration = bool(int(dop))
278
279
280 categoryname = r.get('Angemeldete_Kategorie', None)
281 if categoryname:
282 category = store.find(Category, Category.name == categoryname).one()
283 if not category:
284 category = Category(categoryname)
285 runner.category = category
286
287
288 coursecode = r.get('Bahn', None)
289 if coursecode:
290 course = store.find(Course, Course.code == coursecode).one()
291 sicount = runner.sicards.count()
292 if sicount == 1 and course:
293 store.add(Run(runner.sicards.one(), course))
294 elif sicount != 1:
295 print (u"Can't add run for runner %s %s on line %i: %s." %
296 (r.get('Vorname', u''), r.get('Name', u''), i+2,
297 sicount == 0 and "No SI-card" or "More than one SI-card")
298 )
299 elif course is None:
300 print (u"Can't add run for runner %s %s on line %i: Course not found" %
301 (r.get('Vorname', u''), r.get('Name', u''), i+2)
302 )
303
304 store.flush()
305 except (DataError, IntegrityError), e:
306 print (u"Error importing runner %s %s on line %i: %s\n"
307 u"Import aborted." %
308 (r.get('Vorname', u''), r.get('Name', u''), i+2, e.message.decode('utf-8', 'replace'))
309 )
310 store.rollback()
311 return
312
314 """Import participant data for 24h event from CSV file."""
315
316 RUNNER_NUMBERS = ['A', 'B', 'C', 'D', 'E', 'F']
317 TEAM_NUMBER_FORMAT = u'%03d'
318 RUNNER_NUMBER_FORMAT = u'%(team)s%(runner)s'
319
321
322
323 cat_24h = Category(u'24h')
324 next_24h = 101
325 cat_12h = Category(u'12h')
326 next_12h = 201
327
328 for t in self.data:
329
330
331 if t['Kurz'] == '24h':
332 team = Team(Team24hImporter.TEAM_NUMBER_FORMAT % next_24h,
333 t['Teamname'], cat_24h)
334 next_24h += 1
335 elif t['Kurz'] == '12h':
336 team = Team(Team24hImporter.TEAM_NUMBER_FORMAT % next_12h,
337 t['Teamname'], cat_12h)
338 next_12h += 1
339
340
341 num = 0
342 i = 1
343 while num < int(t['NofMembers']):
344 if t['Memyear%s' % str(i)] == '':
345
346
347 i += 1
348 continue
349
350 runner = Runner(t['Memfamilyname%s' % str(i)],
351 t['Memfirstname%s' % str(i)])
352 if t['Memsex%s' % str(i)] == 'M':
353 runner.sex = 'male'
354 elif t['Memsex%s' % str(i)] == 'F':
355 runner.sex = 'female'
356 runner.dateofbirth = RunnerImporter._parse_yob(t['Memyear%s' % str(i)])
357 runner.number = Team24hImporter.RUNNER_NUMBER_FORMAT % \
358 {'team' : team.number,
359 'runner' : Team24hImporter.RUNNER_NUMBERS[num]}
360
361
362 try:
363 sicard = RunnerImporter._get_sicard(t['Memcardnr%s' % str(i)], store)
364 except NoSICardException, e:
365 print ("Runner %s %s of Team %s (%s) has no SI-card." %
366 (runner.given_name, runner.surname, team.name, team.number))
367 except InvalidSICardException, e:
368 print ("Runner %s %s of Team %s (%s) has an invalid SI-card: %s" %
369 (runner.given_name, runner.surname, team.name, team.number,
370 e.message))
371 else:
372 RunnerImporter._add_sicard(runner,sicard, store)
373
374
375 team.members.add(runner)
376
377 num += 1
378 i += 1
379
380
381 store.add(team)
382
384 """Import participant data for a Relay."""
385
386
387
388 RUNNER_NUMBERS = ['0', '1', '2', '3']
389 RUNNER_NUMBER_FORMAT = u'%(runner)i%(team)02i'
390
392
393 self._categories = {}
394 for line,t in enumerate(self.data):
395 if self._verbose:
396 print ("%i: Importing team %s (%s):" %
397 (line+1, t['Teamname'], t['AnmeldeNummer']))
398
399 try:
400
401 if t['Kategorie'] not in self._categories:
402 self._categories[t['Kategorie']] = Category(t['Kategorie'])
403
404
405 team = Team(t['AnmeldeNummer'],
406 t['Teamname'], self._categories[t['Kategorie']])
407
408
409 num = 0
410 i = 1
411 while num < (self._fieldcount-4)/6:
412 surname = t['Name%s' % str(i)]
413 given_name = t['Vorname%s' % str(i)]
414 number = TeamRelayImporter.RUNNER_NUMBER_FORMAT % \
415 {'team' : int(team.number),
416 'runner' : i}
417 if surname == u'' and given_name == u'':
418
419 i += 1
420 num += 1
421 continue
422
423 if self._verbose:
424 print (" * Adding runner %s %s (%s)." %
425 (given_name, surname, number))
426
427 runner = store.add(Runner(surname,given_name))
428 runner.sex = RunnerImporter._parse_sex(t['Geschlecht%s' % str(i)])
429 runner.dateofbirth = RunnerImporter._parse_yob(t['Jahrgang%s' % str(i)])
430 runner.number = number
431
432
433 try:
434 sicard = RunnerImporter._get_sicard(t['SI-Card%s' % str(i)], store)
435 except NoSICardException, e:
436 print ("Runner %s %s of Team %s (%s) has no SI-card." %
437 (runner.given_name, runner.surname, team.name, team.number))
438 except InvalidSICardException, e:
439 print ("Runner %s %s of Team %s (%s) has an invalid SI-card: %s" %
440 (runner.given_name, runner.surname, team.name, team.number,
441 e.message))
442 else:
443 RunnerImporter._add_sicard(runner,sicard, store)
444
445
446 try:
447 si = runner.sicards.one()
448 except NotOneError:
449 pass
450 else:
451 if si is not None:
452 r = store.add(Run(si))
453 r.set_coursecode(unicode(t['Bahn%s' % i]))
454
455
456 team.members.add(runner)
457
458 num += 1
459 i += 1
460
461
462 store.add(team)
463 except (DataError, IntegrityError), e:
464 print (u"Error importing team %s (%s) on line %i: %s\n"
465 u"Import aborted." %
466 (t['Teamname'], t['AnmeldeNummer'], line+2, e.message.decode('utf-8', 'replace'))
467 )
468 store.rollback()
469 return
470
473 """Import SICard readout data from a backup file.
474 File Format:
475 Course Code;SICard Number;ReadoutTime;StartTime;FinishTime;CheckTime;ClearTime;Control Code 1;Time1;Control Code 2;Time2;...
476 Time Format is YYYY-MM-DD HH:MM:SS.ssssss
477 Example:
478 SE1;345213;2008-02-20 12:32:54.000000;2008-02-20 12:14:00.000000;2008-02-20 13:27:06.080200;2008-02-20 12:10:21.000000;2008-02-20 12:10:07.002000;32;2008-02-20 12:19:23.000000;76;2008-02-20 12:20:57.300000;...
479 """
480
481 timestamp_re = re.compile('([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{6}))?')
482
483 TIMEFORMAT = '%Y-%m-%d %H:%M:%S'
484
485 COURSE = 0
486 CARDNR = 1
487 READOUT= 2
488 START = 3
489 FINISH = 4
490 CHECK = 5
491 CLEAR = 6
492 BASE = 7
493
494 - def __init__(self, fname, replay = False, interval = 10, encoding = 'utf-8',
495 verbose = False):
496 self._replay = replay
497 self._interval = interval
498 self._verbose = verbose
499 csv = reader(open(fname, 'rb'), delimiter=';')
500 self.__runs = []
501 for line in csv:
502 try:
503 if line[0].strip()[0] == '#':
504
505 continue
506 except IndexError:
507 pass
508 self.__runs.append([v.decode(encoding) for v in line])
509
510 @staticmethod
512 """Create a datetime object from a punchtime given as string in the
513 format YYYY-MM-DD HH:MM:SS.ssssss."""
514
515 if punchtime == '':
516 return None
517
518 match = SIRunImporter.timestamp_re.match(punchtime)
519 if match is None:
520 raise RunImportException('Invalid time format: %s' % punchtime)
521
522 (year, month, day, hour, minute, second, dummy, microsecond) = \
523 match.groups()
524 if microsecond is None:
525 microsecond = 0
526
527 return datetime(int(year), int(month), int(day), int(hour),
528 int(minute), int(second), int(microsecond))
529
531 """
532 Adds a punch to the list of punches.
533
534 """
535
536 time = self.__datetime(timestring)
537 if time is not None:
538 self._punches.append((station, time))
539 else:
540 raise RunImportException('Empty punchtime for station "%s".' % station)
541
543
544 for line in self.__runs:
545 course_code = line[SIRunImporter.COURSE]
546 cardnr = line[SIRunImporter.CARDNR]
547
548 self._punches = []
549 i = SIRunImporter.BASE
550 while i < len(line):
551 self.add_punch(int(line[i]), line[i+1])
552 i += 2
553
554 run = Run(int(cardnr),
555 course = course_code,
556 punches = self._punches,
557 card_start_time = self.__datetime(line[SIRunImporter.START]),
558 card_finish_time = self.__datetime(line[SIRunImporter.FINISH]),
559 check_time = self.__datetime(line[SIRunImporter.CHECK]),
560 clear_time = self.__datetime(line[SIRunImporter.CLEAR]),
561 readout_time = self.__datetime(line[SIRunImporter.READOUT]),
562 store = store)
563 run.complete = True
564 store.add(run)
565 if self._replay is True:
566 print "Commiting Run %s for SI-Card %s" % (course_code, cardnr)
567 store.commit()
568 sleep(self._interval)
569
571 """Export Run data to a backup file."""
572
573 - def __init__(self, fname, verbose = False):
574 self.__file = open(fname, 'ab')
575 self._verbose = verbose
576 self.__csv = writer(self.__file, delimiter=';')
577
578 @staticmethod
580 """Convert a punch to a (sistationnr, timestring) tuple. If punch is
581 None ('', '') is retruned.
582 @param punch: punch to convert
583 @type punch: object of class Punch
584 """
585 if punch is None:
586 return ('','')
587
588 return (str(punch.sistation.id),
589 '%s.%06i' % (punch.punchtime.strftime(SIRunImporter.TIMEFORMAT),
590 punch.punchtime.microsecond)
591 )
592
615
617 """Import Course Data from an OCAD XML File produced by OCAD 9."""
618
619
620 KNOWN_VERSIONS = ('2.0.3', )
621
622
623 KNOWN_ROOTTAGS = ('CourseData', )
624
625
626 CONTROL_PATHS = ('./StartPoint/StartPointCode',
627 './FinishPoint/FinishPointCode',
628 './Control/ControlCode',
629 )
630
631 - def __init__(self, fname, finish, start, verbose = False):
644
645 @staticmethod
647 if tag == 'total':
648 length_tag = 'CourseLength'
649 climb_tag = 'CourseClimb'
650 elif tag == 'control':
651 length_tag = 'LegLength'
652 climb_tag = 'LegClimb'
653 elif tag == 'finish':
654 length_tag = 'DistinceToFinish'
655 climb_tag = 'ClimbToFinish'
656 else:
657 return (None, None)
658
659 try:
660 length = int(node.findtext(length_tag))
661 except (TypeError,ValueError):
662 length = None
663 try:
664 climb = int(node.findtext(climb_tag))
665 except (TypeError,ValueError):
666 if length is not None:
667
668 climb = 0
669 else:
670 climb = None
671
672 return (length, climb)
673
674
676
677
678 if self._start:
679 station = store.get(SIStation, SIStation.START)
680 if station is None:
681 station = store.add(SIStation(SIStation.START))
682 if self._finish:
683 station = store.get(SIStation, SIStation.FINISH)
684 if station is None:
685 station = store.add(SIStation(SIStation.FINISH))
686
687
688 for path in OCADXMLCourseImporter.CONTROL_PATHS:
689 for code_el in self.__tree.findall(path):
690 code = code_el.text.strip() and unicode(code_el.text.strip()) or None
691 if not code:
692 raise FileFormatException('Empty Control Code in Control Definition')
693
694 control = store.find(Control, Control.code == code).one()
695 if control is None:
696
697 control = Control(code, store=store)
698
699
700 for c_el in self.__tree.findall('./Course'):
701 variations = c_el.findall('CourseVariation')
702 if len(variations) == 1:
703 var = variations[0]
704
705 course_code = unicode(c_el.findtext('CourseName').strip())
706 (length, climb) = OCADXMLCourseImporter.__length(var, 'total')
707 course = Course(course_code, length, climb)
708 store.add(course)
709
710
711 controls = {}
712 for control_el in var.findall('CourseControl'):
713 code = control_el.findtext('ControlCode').strip()
714 if not code:
715 raise FileFormatException("Empty control code in definition of course '%s'" % course_code)
716 (length, climb) = OCADXMLCourseImporter.__length(control_el, 'control')
717 seq = int(control_el.findtext('Sequence').strip())
718 if seq in controls:
719 raise DuplicateSequenceException("Duplicate control sequence number '%s' in course" % seq)
720 controls[seq] = (code, length, climb)
721
722
723 keys = controls.keys()
724 keys.sort()
725
726 for seq in keys:
727 (code, length, climb) = controls[seq]
728 control = store.find(Control, Control.code == unicode(code)).one()
729 if not control:
730 raise ControlNotFoundException("Control with code '%s' not found." % code)
731 course.append(control, length, climb)
732
733
734 elif len(variations) > 1:
735 raise CourseTypeException('Courses with variations are not yet supported.')
736 else:
737 raise CourseTypeException('Course has no variations (at least 1 needed).')
738
740 """
741 Import courses from a CSV file. The file format is:
742 code;length;climb;1;2;...
743 Coursecode1;courselength1;courseclimb1;control1;control2;...
744 Coursecode2;courselength2;courseclimb2;control1;control2;...
745
746 The first line is the header, all following lines are course definitions. All lengths are
747 in meters.
748 """
749
780
783
786
789
792
795
798
801
804
807
810