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

Source Code for Module bosco.importer

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  #    import_teams.py - Import Data from go2ol.ch 
  4  # 
  5  #    Copyright (C) 2008  Gaudenz Steinlin <gaudenz@soziologie.ch> 
  6  # 
  7  #    This program is free software: you can redistribute it and/or modify 
  8  #    it under the terms of the GNU General Public License as published by 
  9  #    the Free Software Foundation, either version 3 of the License, or 
 10  #    (at your option) any later version. 
 11  # 
 12  #    This program is distributed in the hope that it will be useful, 
 13  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15  #    GNU General Public License for more details. 
 16  # 
 17  #    You should have received a copy of the GNU General Public License 
 18  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 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 
39 40 -class Importer:
41 """Base class for all Importer classes to import event 42 data. This class provides the general interface for (GUI) 43 import frontends.""" 44
45 - def __init__(self, fname, verbose):
46 pass
47
48 - def import_data(self, store):
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
53 -class CSVImporter(Importer):
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 # List of dicts 63 self.data = [] 64 65 # Set up CSV reader 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 # Read labels 76 labels = [ v.strip() for v in csv.next() ] 77 self._fieldcount = len(labels) 78 79 # Read values 80 for line in csv: 81 try: 82 if line[0].strip()[0] == '#': 83 # skip comment lines 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
93 -class RunnerImporter(CSVImporter):
94 """Import Runner data from CSV file. 95 This class currently only consists of helper functions for derived classes. 96 """ 97 98 @staticmethod
99 - def _parse_yob(yob):
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
111 - def _get_sicard(si_str, store):
112 try: 113 si_int = int(si_str) 114 except ValueError: 115 if not si_str == '': 116 raise InvalidSICardException("Invalid SI Card number '%s'" % si_str) 117 else: 118 raise NoSICardException() 119 if si_int == 0: 120 raise NoSICardException() 121 122 sicard = store.get(SICard, si_int) 123 if sicard is None: 124 sicard = SICard(si_int) 125 126 return sicard
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 # this card is already assigned to this runner 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
157 - def _parse_sex(sex):
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
165 -class SOLVDBImporter(RunnerImporter):
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
174 - def import_data(self, store):
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 # check if we already know this SI-Card 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 # This sicard is already assigned and the names do not match. Don't 195 # assign an sicard to this runner 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 # check if we already know this runner 203 # get solvnr and startnumber, treat empty values (eg. '') as None 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 # we have matching runners for solvnr, number or sicard 217 # and they are not all the same 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 # Add category if present 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 # Add run if course code is present 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
313 -class Team24hImporter(RunnerImporter):
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
320 - def import_data(self, store):
321 322 # Create categories 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 # Create the team 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 # Create individual runners 341 num = 0 342 i = 1 343 while num < int(t['NofMembers']): 344 if t['Memyear%s' % str(i)] == '': 345 # discard runners with an empty year of 346 # birth 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 # Add SI Card if valid 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 # Add runner to team 375 team.members.add(runner) 376 377 num += 1 378 i += 1 379 380 # Add team to store 381 store.add(team)
382
383 -class TeamRelayImporter(RunnerImporter):
384 """Import participant data for a Relay.""" 385 386 # 0 leg needs 0 not '' otherwise the numbers do 387 # not sort correctly as they are strings! 388 RUNNER_NUMBERS = ['0', '1', '2', '3'] 389 RUNNER_NUMBER_FORMAT = u'%(runner)i%(team)02i' 390
391 - def import_data(self, store):
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 # Create category 401 if t['Kategorie'] not in self._categories: 402 self._categories[t['Kategorie']] = Category(t['Kategorie']) 403 404 # Create the team 405 team = Team(t['AnmeldeNummer'], 406 t['Teamname'], self._categories[t['Kategorie']]) 407 408 # Create individual runners 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 # don't add runner without any name 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 # Add SI Card if valid 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 # Add open run if SICard 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 # Add runner to team 456 team.members.add(runner) 457 458 num += 1 459 i += 1 460 461 # Add team to store 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
471 472 -class SIRunImporter(Importer):
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 # skip comment lines 505 continue 506 except IndexError: 507 pass 508 self.__runs.append([v.decode(encoding) for v in line])
509 510 @staticmethod
511 - def __datetime(punchtime):
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
530 - def add_punch(self, station, timestring):
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
542 - def import_data(self, store):
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
570 -class SIRunExporter(SIRunImporter):
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
579 - def __punch2string(punch):
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
593 - def export_run(self, run):
594 595 line = [''] * SIRunImporter.BASE 596 line[SIRunImporter.COURSE] = run.course.code 597 line[SIRunImporter.CARDNR] = run.sicard.id 598 line[SIRunImporter.READOUT] = run.readout_time and run.readout_time.strftime(SIRunImporter.TIMEFORMAT) or '' 599 line[SIRunImporter.START] = SIRunExporter.__punch2string(run.punches.find(Punch.sistation == SIStation.START).one())[1] 600 line[SIRunImporter.CHECK] = SIRunExporter.__punch2string(run.punches.find(Punch.sistation == SIStation.CHECK).one())[1] 601 line[SIRunImporter.CLEAR] = SIRunExporter.__punch2string(run.punches.find(Punch.sistation == SIStation.CLEAR).one())[1] 602 line[SIRunImporter.FINISH] = SIRunExporter.__punch2string(run.punches.find(Punch.sistation == SIStation.FINISH).one())[1] 603 for punch in run.punches.find(Not(Or(Punch.sistation == SIStation.START, 604 Punch.sistation == SIStation.CLEAR, 605 Punch.sistation == SIStation.CHECK, 606 Punch.sistation == SIStation.FINISH 607 ))): 608 punch_string = SIRunExporter.__punch2string(punch) 609 line.append(punch_string[0]) 610 line.append(punch_string[1]) 611 612 self.__csv.writerow(line) 613 self.__file.flush() 614 fsync(self.__file)
615
616 -class OCADXMLCourseImporter(Importer):
617 """Import Course Data from an OCAD XML File produced by OCAD 9.""" 618 619 # Known IOF Data Format versions 620 KNOWN_VERSIONS = ('2.0.3', ) 621 622 # Known Root Tags 623 KNOWN_ROOTTAGS = ('CourseData', ) 624 625 # XPaths to control point codes 626 CONTROL_PATHS = ('./StartPoint/StartPointCode', 627 './FinishPoint/FinishPointCode', 628 './Control/ControlCode', 629 ) 630
631 - def __init__(self, fname, finish, start, verbose = False):
632 self.__tree = parse(fname) 633 self._start = start 634 self._finish = finish 635 self._verbose = False 636 637 version = self.__tree.find('./IOFVersion').attrib['version'] 638 if not version in OCADXMLCourseImporter.KNOWN_VERSIONS: 639 raise FileFormatException("Unknown IOFVersion '%s'" % version) 640 641 roottag = self.__tree.getroot().tag 642 if not roottag in OCADXMLCourseImporter.KNOWN_ROOTTAGS: 643 raise FileFormatException("Wrong root tag: '%s'" % roottag)
644 645 @staticmethod
646 - def __length(node, tag = 'total'):
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 # Set climb to 0 if length is given 668 climb = 0 669 else: 670 climb = None 671 672 return (length, climb)
673 674
675 - def import_data(self, store):
676 677 # create SI Stations for start and finish if requested 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 # read control codes 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 # search for control in Store 694 control = store.find(Control, Control.code == code).one() 695 if control is None: 696 # Create new control 697 control = Control(code, store=store) 698 699 # Read courses 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 # Get Course properties 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 # read control codes and sequence numbers into dict 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 # sort controls by sequence number 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
739 -class CSVCourseImporter(CSVImporter):
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
750 - def import_data(self, store):
751 752 for c in self.data: 753 754 if self._verbose: 755 print "Importing course %s." % c['code'] 756 757 # create course 758 course = store.find(Course, Course.code == c['code']).one() 759 if course: 760 print "A course with code %s already exists. Updating course." % c['code'] 761 for s in course.sequence: 762 store.remove(s) 763 store.remove(course) 764 course = store.add(Course(c['code'], int(c['length']), int(c['climb']))) 765 766 # add controls 767 for i in range(len(c)-3): 768 code = c[str(i+1)] 769 770 if code == u'': 771 # end of course 772 break 773 774 control = store.find(Control, Control.code == code).one() 775 if control is None: 776 # Create new control 777 control = Control(code, store=store) 778 779 course.append(control)
780
781 -class SICardException(Exception):
782 pass
783
784 -class InvalidSICardException(SICardException):
785 pass
786
787 -class NoSICardException(SICardException):
788 pass
789
790 -class AlreadyAssignedSICardException(SICardException):
791 pass
792
793 -class RunImportException(Exception):
794 pass
795
796 -class InvalidStationNumberException(Exception):
797 pass
798
799 -class FileFormatException(Exception):
800 pass
801
802 -class CourseTypeException(Exception):
803 pass
804
805 -class ControlNotFoundException(Exception):
806 pass
807
808 -class DuplicateSequenceException(Exception):
809 pass
810