From 20eaf94c7b883dc67ac7f607676e490943a0eb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81tienne=20Beaule=CC=81?= Date: Mon, 5 Aug 2019 03:39:50 -0300 Subject: [PATCH] Create Tabroom XML to Archive translator This commit adds a purpose of the repo to contain translators from different formats, now in the 'converters/' subdirectory. The first of these translators is for Tabroom, a tournament management system by the National Speech & Debate Association, an American high school debate association. This format is used for import/export onto the Tabroom site, as well as "Computer Assisted Tab," "Joy of Tournaments," and "TRPC." The schema used as a basis is available on GitHub: https://github.com/TimothyGu/tabroom/blob/master/ndt/DataFiles/TourneyData.xsd Further, some datafiles were found and used for testing. There seemed to be two ways to encode the rounds with results; using 9).lower() + }) + + if self.num_events > 1 and tr_round.find('EVENT') is not None: + round.set('division', self.ID_PREFIX['EVENT'] + tr_round.find('EVENT').text) + timeslot = tr_round.find('TIMESLOT') + if timeslot is not None and self.tr.find("TIMESLOT[ID='%s']" % timeslot.text) is not None: + round.set('start', _handle_datetime( + self.tr.find("TIMESLOT[ID='%s']/START" % timeslot.text).text, time=True).isoformat() + ) + + for panel in self.tr.findall("PANEL[ROUND='%s']" % tr_round.find('ID').text): + panel_id = panel.find('ID').text + entries = [e.text for e in self.tr.findall("BALLOT[PANEL='%s']/ENTRY" % panel_id)] + if (panel.find('BYE') is None or panel.find('BYE').text == "0") and self.UNEXISTANT_ID not in entries: + self.interpret_debate(round, panel) + else: + byes = set(entries) + for bye in byes: + if bye != self.UNEXISTANT_ID: + # ID included just for debugging purposes + bye_tag = ET.SubElement(round, 'bye') + bye_tag.text = self.ID_PREFIX['ENTRY'] + bye + + def interpret_debate(self, round, panel): + ballot_adjs = set() # Chairs don't seem to be defined + for adj in self.tr.findall("BALLOT[PANEL='%s']/JUDGE" % panel.find('ID').text): + ballot_adjs.add(self.ID_PREFIX['JUDGE'] + adj.text) + + debate = ET.SubElement(round, 'debate', { + 'id': self.ID_PREFIX['DEBATE'] + panel.find('ID').text, + }) + if self.UNEXISTANT_ID not in ballot_adjs: + debate.set('adjudicators', " ".join(ballot_adjs)) + if panel.find('ROOM').text != self.UNEXISTANT_ID: + debate.set('venue', self.ID_PREFIX['ROOM'] + panel.find('ROOM').text) + + entries_added = set() + for tr_ballot_side in sorted( + self.tr.findall("BALLOT[PANEL='%s']" % panel.find('ID').text), + key=lambda e: int(e.find('SIDE').text) + ): + entry = tr_ballot_side.find('ENTRY').text + entry_id = self.ID_PREFIX['ENTRY'] + entry + if entry_id in entries_added: + continue + side = ET.SubElement(debate, 'side', { + 'team': entry_id + }) + entries_added.add(entry_id) + self.interpret_side(side, panel.find('ID').text, entry) + + def interpret_side(self, side, panel_id, entry_id): + tr_ballots = self.tr.findall("BALLOT[PANEL='%s'][ENTRY='%s']" % (panel_id, entry_id)) + speaker_scores = {} + for tr_ballot in tr_ballots: + b_id = tr_ballot.find('ID').text + ballot_team_rank = self.tr.find("BALLOT_SCORE[BALLOT='%s'][SCORE_ID='%s']" % (b_id, self.BALLOT_TYPE_IDS['Ballot'])) + ballot_team_score_total = sum(float(e.find('SCORE').text) for e in + self.tr.findall("BALLOT_SCORE[BALLOT='%s'][SCORE_ID='%s']" % (b_id, self.BALLOT_TYPE_IDS['Speaker Points'])) + ) + + ballot = ET.SubElement(side, 'ballot') + adj_id = "" + if tr_ballot.find('JUDGE').text != self.UNEXISTANT_ID: + adj_id = self.ID_PREFIX['JUDGE'] + tr_ballot.find('JUDGE').text + ballot.set('adjudicators', adj_id) + if ballot_team_rank is not None: + max_rank = 4 if self.is_bp else 2 + rank = max_rank - int(ballot_team_rank.find('SCORE').text) + ballot.set('rank', str(rank)) + if ballot_team_score_total > 0: + ballot.text = str(round(ballot_team_score_total, 1)) + + for ss in self.tr.findall("BALLOT_SCORE[BALLOT='%s']" % b_id): + rec = ss.find('RECIPIENT').text + if rec == entry_id: + continue + if rec not in speaker_scores: + speaker_scores[rec] = {} + if adj_id not in speaker_scores[rec]: + speaker_scores[rec][adj_id] = {} + speaker_scores[rec][adj_id][ss.find('SCORE_ID').text] = ss.find('SCORE').text + + for speaker, adj_s in speaker_scores.items(): + speech = ET.SubElement(side, 'speech', {'speaker': self.ID_PREFIX['STUDENT'] + speaker}) + for adj, ss in adj_s.items(): + ballot = ET.SubElement(speech, 'ballot') + if adj is not '': + ballot.set('adjudicators', adj) + for t, s in ss.items(): + attr = self.BALLOT_TYPE_ATTR[self.BALLOT_TYPE_NAMES[t]] + if attr is None: + ballot.text = s + else: + ballot.set(attr, s) + + def parse_rounds_by_roundresult(self): + for round_result in self.tr.findall('ROUNDRESULT'): + round = ET.SubElement(self.dml, 'round', { + 'name': round_result.get('RoundName'), + 'elimination': str(round_result.get('RoundType') == 'Elim').lower() + }) + if self.num_events > 1: + round.set('division', self.ID_PREFIX['EVENT'] + round_result.get('EventID')) + + for panel_id, tr_ballots_gen in groupby(round_result.findall('BALLOT'), key=lambda l: l.get('Panel')): + tr_ballots = list(tr_ballots_gen) + + if tr_ballots[0].get('JudgeID') == self.UNEXISTANT_ID: + for team in tr_ballots[0].findall("SCORE[@ScoreFor='Team']"): + if team.get('Recipient') == self.UNEXISTANT_ID: + continue + bye = ET.SubElement(round, 'bye') + bye.text = self.ID_PREFIX['ENTRY'] + team.get('Recipient') + break + + debate = ET.SubElement(round, 'debate', { + 'id': self.ID_PREFIX['DEBATE'] + panel_id, + 'adjudicators': " ".join([ + self.ID_PREFIX['JUDGE'] + b.get('JudgeID') for b in tr_ballots if b.get('JudgeID') is not None + ]) + }) + if tr_ballots[0].get('RoomID') is not None: + debate.set('venue', self.ID_PREFIX['ROOM'] + tr_ballots[0].get('RoomID')) + + # Get sides + sides = {} + for tr_score in sorted(tr_ballots[0].findall("SCORE[@ScoreFor='Team']"), key=lambda l: int(l.get('Side', 0))): + side = ET.SubElement(debate, 'side', {'team': self.ID_PREFIX['ENTRY'] + tr_score.get('Recipient')}) + sides[tr_score.get('Recipient')] = side + + # Get speakers if available + speakers = {} + cur_team = "" + for tr_score in tr_ballots[0].findall("SCORE"): + if tr_score.get('ScoreFor') == 'Team': + cur_team = tr_score.get('Recipient') + elif tr_score.get('Recipient') not in speakers: + speaker = ET.SubElement(sides[cur_team], 'speech', { + 'speaker': self.ID_PREFIX['STUDENT'] + tr_score.get('Recipient') + }) + speakers[tr_score.get('Recipient')] = speaker + + for tr_ballot in tr_ballots: + adj_id = tr_ballot.get('JudgeID') + + for p_type in [sides, speakers]: + for p_id, tag in p_type.items(): + adj_decision = {} + for tr_score in tr_ballot.findall("SCORE[@Recipient='%s']" % p_id): + adj_decision[tr_score.get('SCORE_NAME')] = tr_score.text + ballot = ET.SubElement(tag, 'ballot', { + 'adjudicators': self.ID_PREFIX['JUDGE'] + adj_id + }) + for score_type, value in adj_decision.items(): + attr = self.BALLOT_TYPE_ATTR[score_type] + if score_type == 'Ballot': + max_rank = len(sides.keys()) + ballot.set('rank', str(max_rank - int(value))) + elif attr is not None: + ballot.set(attr, value) + else: + ballot.text = value + + def parse_participants(self): + participants = ET.SubElement(self.dml, 'participants') + + self.parse_entries(participants) + self.parse_judges(participants) + + def parse_entries(self, participants): + for entry in self.tr.findall('ENTRY'): + team = ET.SubElement(participants, 'team', { + 'id': self.ID_PREFIX['ENTRY'] + entry.find('ID').text + }) + + # Names are optional apparently + if entry.find('FULLNAME') is not None: + team.set('name', entry.find('FULLNAME').text) + elif entry.find('CODE') is not None: + team.set('name', entry.find('CODE').text) + else: + team.set('name', 'UNKNOWN') + + if entry.find('CODE') is not None: + team.set('code', entry.find('CODE').text) + + if self.num_events > 1: + team.set('division', self.ID_PREFIX['EVENT'] + entry.find('EVENT').text) + + # Add speakers + inst = "" + if entry.find('SCHOOL') is not None and entry.find('SCHOOL') != self.UNAFFILATED_ID: + inst = self.ID_PREFIX['SCHOOL'] + entry.find('SCHOOL').text + + for student in self.tr.findall("ENTRY_STUDENT[ENTRY='%s']" % entry.find('ID').text): + speaker = ET.SubElement(team, 'speaker', { + 'id': self.ID_PREFIX['STUDENT'] + student.find('ID').text + }) + speaker.text = student.find('FIRST').text + " " + student.find('LAST').text + + if student.find('SCHOOL') is not None and student.find('SCHOOL') != self.UNAFFILATED_ID: + other_inst = self.ID_PREFIX['SCHOOL'] + student.find('SCHOOL').text + if inst == "": + speaker.set('institutions', other_inst) + elif other_inst != inst: + speaker.set('institutions', inst + " " + other_inst) + else: + speaker.set('institutions', inst) + elif inst != "": + speaker.set('institutions', inst) + + def parse_judges(self, participants): + for judge in self.tr.findall('JUDGE'): + adj = ET.SubElement(participants, 'adjudicator', { + 'id': self.ID_PREFIX['JUDGE'] + judge.find('ID').text + }) + if judge.find('TABRATING') is not None and judge.find('TABRATING').text is not None: + adj.set('score', judge.find('TABRATING').text) + if judge.find('SCHOOL') is not None and judge.find('SCHOOL').text != self.UNAFFILATED_ID: + adj.set('institutions', self.ID_PREFIX['SCHOOL'] + judge.find('SCHOOL').text) + adj.text = judge.find('FIRST').text + " " + judge.find('LAST').text + + def parse_schools(self): + for school in self.tr.findall('SCHOOL'): + if school.find('ID').text == self.UNAFFILATED_ID: + continue + institution = ET.SubElement(self.dml, 'institution', { + 'id': self.ID_PREFIX['SCHOOL'] + school.find('ID').text + }) + if school.find('CODE') is not None and school.find('CODE').text is not None: + institution.set('reference', school.find('CODE').text) + if school.find('REGION') is not None and school.find('REGION').text is not None: + institution.set('region', school.find('REGION').text) + institution.text = school.find('SCHOOLNAME').text + + def parse_rooms(self): + for room in self.tr.findall('ROOM'): + venue = ET.SubElement(self.dml, 'venue', { + 'id': self.ID_PREFIX['ROOM'] + room.find('ID').text, + 'score': room.find('QUALITY').text + }) + venue.text = room.find('ROOMNAME').text + + def parse_events(self): + if self.num_events == 1: + return + for event in self.tr.findall('EVENT'): + division = ET.SubElement(self.dml, 'division', { + 'id': self.ID_PREFIX['EVENT'] + event.find('ID').text + }) + division.text = event.find('EVENTNAME').text + + +if __name__ == "__main__": + input_xml = ET.fromstring(sys.stdin.read()) + tc = TabroomImporter(input_xml) + output_xml = ET.tostring(tc.run()) + sys.stdout.buffer.write(output_xml) + sys.stdout.write("\n") # XML doesn't add newline