# dt_teamtrack.py -- defect tracking interface (TeamTrack). # Gareth Rees, Ravenbrook Limited, 2000-09-07. # # See "Replicator design" for the design of the # replicator; "Replicator classes in Python" # for the class organization of the replicator; "Replicator interface to # TeamTrack" for the design of # this module; "TeamTrack database schema extensions for integration with # Perforce" for the database schema # which this module depends on; and "Python interface to TeamTrack: design" # for the design of the teamtrack # module. # # Copyright 2000 Ravenbrook Limited. This document is provided "as is", # without any express or implied warranty. In no event will the authors # be held liable for any damages arising from the use of this document. # You may make and distribute copies and derivative works of this # document provided that (1) you do not charge a fee for this document or # for its distribution, and (2) you retain as they appear all copyright # and licence notices and document history entries, and (3) you append # descriptions of your modifications to the document history. import replicator import re import socket import string import teamtrack import types error = 'P4DTI TeamTrack interface error' # Field types in the VCACTIONS table. (See "TeamTrack database schema # extensions for integration with Perforce", 2.1.) vcactions_type_filespec = 1 vcactions_type_fix = 2 vcactions_type_changelist = 3 vcactions_type_config = 4 class teamtrack_case(replicator.defect_tracker_issue): dt = None # The defect tracker this case belongs to. case = None # The teamtrack_record object representing the case. def __init__(self, case, dt): self.case = case self.dt = dt def __getitem__(self, key): return self.case[key] def __repr__(self): return repr(self.case) def __setitem__(self, key, value): self.case[key] = value def action(self): return self['P4DTI_ACTION'] def add_filespec(self, filespec): filespec_record = self.dt.server.new_record(teamtrack.table['VCACTIONS']) filespec_record['TYPE'] = vcactions_type_filespec filespec_record['CHAR2'] = self.dt.sid filespec_record['CHAR1'] = self.rid() filespec_record['TABLEID'] = teamtrack.table['CASES'] filespec_record['RECID'] = self.case['ID'] filespec_record['TIME1'] = 0 filespec_record['TIME2'] = 0 filespec_record['FILENAME'] = repr({'filespec': filespec}) filespec_record.add() def add_fix(self, change, client, date, user, status): fix_record = self.dt.server.new_record(teamtrack.table['VCACTIONS']) fix_record['TYPE'] = vcactions_type_fix fix_record['CHAR2'] = self.dt.sid fix_record['CHAR1'] = self.rid() fix_record['TABLEID'] = teamtrack.table['CASES'] fix_record['RECID'] = self.case['ID'] fix_record['TIME2'] = 0 fix = self.dt.config['fix-class'](fix_record, self) fix.transform_from_p4(change, client, date, user, status) fix.add() def filespecs(self): query = "TS_TYPE=%d AND TS_RECID=%s" % (vcactions_type_filespec, self.id()) filespecs = [] for filespec in self.dt.server.query(teamtrack.table['VCACTIONS'], query): filespecs.append(self.dt.config['filespec-class'](filespec, self)) return filespecs # find_transition(old_state, new_state). Given an issue and the old and # new states, find a transition that corresponds to this state change, or # return None if there is no such transition. def find_transition(self, old_state, new_state): assert isinstance(old_state, types.IntType) assert isinstance(new_state, types.IntType) project = self['PROJECTID'] ss = (old_state, new_state) if not (self.dt.project_to_states_to_transition.has_key(project) and self.dt.project_to_states_to_transition[project].has_key(ss)): # The transitions may have changed since we last looked in the # database, so refresh our cache. self.dt.init_transitions() if (self.dt.project_to_states_to_transition.has_key(project) and self.dt.project_to_states_to_transition[project].has_key(ss)): return self.dt.project_to_states_to_transition[project][ss] else: # No appropriate transition found. return None def fixes(self): query = "TS_TYPE=%d AND TS_RECID=%s" % (vcactions_type_fix, self.id()) fixes = [] for fix in self.dt.server.query(teamtrack.table['VCACTIONS'], query): fixes.append(self.dt.config['fix-class'](fix, self)) return fixes def id(self): return str(self['ID']) def readable_name(self): readable_name = self['P4DTI_JOBNAME'] if readable_name: return readable_name elif self.dt.type_id_to_prefix.has_key(self['ISSUETYPE']): # For Mahi we're using only the ISSUETYPE and ISSUEID, even # though this is theoretically unsafe. GDR 2000-10-31. # There ought to be a flag in the configuration that selects # between the safe and nice behaviours. GDR 2000-10-31. return ('%s%s' % (self.dt.type_id_to_prefix[self['ISSUETYPE']], self['ISSUEID'])) else: return '%s' % (self['ISSUEID'],) def rid(self): return self['P4DTI_RID'] def setup_for_replication(self): self['P4DTI_RID'] = self.dt.rid self['P4DTI_SID'] = self.dt.sid self['P4DTI_JOBNAME'] = self.readable_name() self['P4DTI_ACTION'] = 'replicate' self.case.update() def update(self, user, changes = {}): # Work out a transition based on the old case state and the new case # state, if the state changed. Otherwise, update the case by using # transition 0 (this is a secret feature of the transition function - # see John McGinley's e-mail). transition = 0 if changes.has_key('STATE') and changes['STATE'] != self['STATE']: transition = self.find_transition(self['STATE'], changes['STATE']) if transition: self.dt.log("-- Transition: %d", (transition,)) else: # Don't change the state except through a transition. GDR # 2000-10-27. raise error, ("No transition found from state %d to state %d." % (old_state, changes['STATE'])) for key, value in changes.items(): self[key] = value user = self.dt.user_id_to_name[user] self.case.transition(user, transition) def update_action(self, action): if self['P4DTI_ACTION'] != action: self['P4DTI_ACTION'] = action self.case.update() class teamtrack_fix(replicator.defect_tracker_fix): case = None # The TeamTrack case to which the fix refers. fix = None # The teamtrack_record object representing the fix record. data = { 'status': '', 'client': '' } # The data that goes in the # TS_FILENAME field. def __init__(self, fix, case): self.fix = fix self.case = case if fix['FILENAME']: self.data = eval(fix['FILENAME']) def __getitem__(self, key): return self.fix[key] def __repr__(self): return repr(self.fix) def __setitem__(self, key, value): self.fix[key] = value if key == 'FILENAME': self.data = eval(value) def add(self): self.fix.add() def change(self): return self['INFO1'] def delete(self): self.case.dt.server.delete_record(teamtrack.table['VCACTIONS'], self.fix['ID']) def status(self): return self.data['status'] def transform_from_p4(self, change, client, date, status, user): self['INFO1'] = change self['TIME1'] = date self['AUTHOR1'] = user self['FILENAME'] = repr({ 'status': status, 'client': client }) def update(self, change, client, date, status, user): self.transform_from_p4(change, client, date, status, user) self.fix.update() class teamtrack_filespec(replicator.defect_tracker_filespec): case = None # The TeamTrack case to which the filespec refers. filespec = None # The teamtrack_record object representing the filespec. data = { 'filespec': '' } # The data that goes in the TS_FILENAME field. def __init__(self, filespec, case): self.filespec = filespec self.case = case if self['FILENAME']: self.data = eval(self['FILENAME']) def __getitem__(self, key): return self.filespec[key] def __setitem__(self, key, value): self.filespec[key] = value if key == 'FILENAME': data = eval(value) def delete(self): self.case.dt.server.delete_record(teamtrack.table['VCACTIONS'], self.filespec['ID']) def name(self): return self.data['filespec'] # The TeamTrack class implements a generic interface between the replicator and # TeamShare's "TeamTrack" defect tracker. Some configuration can be done by # passing a configuration hash to the constructor; for more advanced # configruation you should subclass this and replace some of the methods. The # configuration assumes that the teamTrack server is on the same host as the # replicator. class dt_teamtrack(replicator.defect_tracker): config = { 'server' : socket.gethostname(), 'user' : None, 'userid' : None, 'password' : '', 'case-class' : teamtrack_case, 'fix-class' : teamtrack_fix, 'filespec-class' : teamtrack_filespec, 'p4-server-description' : 'Perforce server', } rid = None server = None sid = None replicator = None # A map from the type of an issue to the prefix for that type (e.g., "BUG", # "ENH"). type_id_to_prefix = { } # A map from project and a pair of states to a transition that is available # to cases in that project and which transitions between the states. For # example, if transition 27 is available in project 5 and transitions # between state 6 and state 7, then # project_to_states_to_transition[5][(6,7)] == 27. project_to_states_to_transition = { } # A map from selection id to name. selection_id_to_name = { } # A map from field name to selection nam to the id for that selection. For # example, if selection called "Foo" in field "PROJECTS" has id 7, then # field_to_selection_to_id['PROJECTS']['Foo'] == 7. field_to_selection_to_id = { } # A map from TeamTrack project to a map from state name to state id. So # for example, if prject 7 has a state called "Open" with id 25, then # project_to_name_to_state[7]['Open'] == 25. project_to_name_to_state = { } # A map from TeamTrack state id to state name. state_id_to_name = { } # A map from TeamTrack user id (the ID field in the USERS table) to their # user name. user_id_to_name = { } # A map from TeamTrack user name to their user id (the ID field in the # USERS table). user_name_to_id = { } def __init__(self, rid, sid, config = {}): replicator.defect_tracker.__init__(self, rid, sid, config) if not self.config['user']: self.config['user'] = 'P4DTI-%s' % self.rid self.server = teamtrack.connect(self.config['user'], self.config['password'], self.config['server']) self.init_types() # Get the userid corresponding to the replicator's userid. This will # be used in queries to ignore records changed most recently by the # replicator. user = self.server.query(teamtrack.table['USERS'], "TS_LOGINID = '%s'" % self.config['user']) if len(user) != 1: raise error, ("No login id in TeamTrack's USERS table corresponds " "to replicator's login id '%s'." % self.config['user']) self.config['userid'] = user[0]['ID'] def all_issues(self): query = ("(TS_P4DTI_RID='%s' OR TS_P4DTI_RID='' OR " "TS_P4DTI_RID IS NULL)" % self.rid) cases = [] for c in self.server.query(teamtrack.table['CASES'], query): cases.append(self.config['case-class'](c, self)) return cases def changed_entities(self): # Get the last change record that was dealt with. query = ("TS_TYPE=%d AND TS_CHAR1='%s' AND TS_CHAR2='LAST_CHANGE'" % (vcactions_type_config, self.rid)) last_change = self.server.query(teamtrack.table['VCACTIONS'], query) if not last_change: raise error, "No LAST_CHANGE record for this replicator" # Get the list of changes to cases that haven't been dealt with yet. # Ignore changes made by the replicator: I believe that TeamTrack # record changes made using the TeamShare API as due to userid 0, but I # check for self.config['userid'] just to be sure. last_change_id = last_change[0]['INFO1'] query = ("TS_TABLEID = %d AND TS_ID > %d AND TS_USERID <> %d " "AND TS_USERID <> 0" % (teamtrack.table['CASES'], last_change_id, self.config['userid'])) changes = self.server.query(teamtrack.table['CHANGES'], query) # Work out the set of changed cases (since a changed case may appear # several times in the CHANGES table but we don't want to replicate it # more than once) and the last change id. case_ids = {} for c in changes: case_ids[c['CASEID']] = 1 if c['ID'] > last_change_id: last_change_id = c['ID'] # Identify changed fixes and filespecs; add affected cases to case_ids. query = ("TS_TYPE IN (%d,%d) AND TS_CHAR1='%s' AND TS_TIME1>TS_TIME2" % (vcactions_type_fix, vcactions_type_filespec, self.rid)) changed_assocs = self.server.query(teamtrack.table['VCACTIONS'], query) for f in changed_assocs: case_ids[f['RECID']] = 1 # Get the changed cases. changed_cases = [] if case_ids: # The IS NULL condition is there because the TeamShare API doesn't # reliably set a NULL field to the empty string when you assign the # empty string to the field and update the record. See e-mail to # Larry Fish, 2000-09-19. query = ("(TS_P4DTI_RID='%s' OR TS_P4DTI_RID='' OR " "TS_P4DTI_RID IS NULL) AND TS_ID IN (%s)" % (self.rid, repr(case_ids.keys())[1:-1])) for c in self.server.query(teamtrack.table['CASES'], query): changed_cases.append(self.config['case-class'](c, self)) if last_change[0]['INFO1'] != last_change_id: last_change[0]['INFO1'] = last_change_id # Note that there are no changed changelists. return changed_cases, [], last_change[0] else: # There were no new changes, so there must be no changed cases. # BUT this isn't correct. There might be changes to fixes and # filespecs that don't show up as changes to cases. So I've commented # out the next line. GDR 2000-10-24. # assert changed_cases == [] # Note that there are no changed changelists. return changed_cases, [], None def mark_changes_done(self, last_change): if last_change: last_change.update() def init(self): # Check that the TeamTrack database version is supported. supported_dbver = 23 system_info = self.server.read_record(teamtrack.table['SYSTEMINFO'], 1) if system_info['DBVER'] < supported_dbver: raise error, ('TeamTrack database version %d not supported ' 'by P4DTI. Minimum supported version is %d.' % (system_info['DBVER'], supported_dbver)) # Fields to add to the TS_CASES table. new_fields = [ { 'name': 'P4DTI_RID', 'type': teamtrack.field_type['TEXT'], 'length': 32, 'attributes': 1, # Fixed-width text. 'description': "P4DTI replicator identifier", 'value': '' }, { 'name': 'P4DTI_SID', 'type': teamtrack.field_type['TEXT'], 'length': 32, 'attributes': 1, # Fixed-width text. 'description': "P4DTI Perforce server identifier", 'value': '' }, { 'name': 'P4DTI_JOBNAME', 'type': teamtrack.field_type['TEXT'], 'length': 0, # Arbitrarily long. 'attributes': 0, # "Memo" = variable-width. 'description': "P4DTI Perforce jobname", 'value': '' }, { 'name': 'P4DTI_ACTION', 'type': teamtrack.field_type['TEXT'], 'length': 32, 'attributes': 1, # Fixed-width text. 'description': "P4DTI action", 'value': 'replicate' }, ] # Make a TS_CASES record so we can see if the new fields are already # present. case = self.server.new_record(teamtrack.table['CASES']) # Add each new field if not present. added_fields = [] for new_field in new_fields: if not case.has_key(new_field['name']): self.log("Installing field '%s' in the TS_CASES table.", new_field['name']) f = self.server.new_record(teamtrack.table['FIELDS']) f['TABLEID'] = teamtrack.table['CASES'] f['NAME'] = new_field['description'] f['DBNAME'] = new_field['name'] f['FLDTYPE'] = new_field['type'] f['LEN'] = new_field['length'] f['ATTRIBUTES'] = new_field['attributes'] f['STATUS'] = 0 # Active, not deleted. f['PROPERTY'] = 1 # Not editable. f['DESCRIPTION'] = new_field['description'] f.add_field() added_fields.append(new_field) if added_fields: # Previous installation was not up to date. Put default values in # the new fields. cases = self.server.query(teamtrack.table['CASES'], '') for case in cases: for added_field in added_fields: case[added_field['name']] = added_field['value'] case.update() if len(added_fields) != len(new_fields): self.log("Partially installed the new fields in the TS_CASES " "table. Previous installation was not up to date.") else: self.log("Installed all new fields in the TS_CASES table.") # These are the values that should appear in the Replicator's # configuration parameters table. Each entry in the config_params list # is a 4-tuple ( parameter name, field name, field value, force # update?) The LAST_CHANGE parameter is set to -1 the first time the # replicator is initialized, so that we'll examine all changes the # first time the replicator polls the defect tracker. config_params = [ ( 'LAST_CHANGE', 'INFO1', -1, 0 ), ( 'SERVER', 'FILENAME', repr({ 'sid': self.sid, 'description': self.config['p4-server-description']}), 1 ), ] # Get all the configuration parameters for this replicator; make a hash # by parameter name. query = ("TS_TYPE=%d AND TS_CHAR1='%s'" % (vcactions_type_config, self.rid)) params = {} for p in self.server.query(teamtrack.table['VCACTIONS'], query): params[p['CHAR2']] = p # Now add or update. for name, field, value, force_p in config_params: if not params.has_key(name): r = self.server.new_record(teamtrack.table['VCACTIONS']) r['TYPE'] = vcactions_type_config r['CHAR1'] = self.rid r['CHAR2'] = name r[field] = value r.add() self.log("Put '%s' parameter in replicator configuration " "with value '%s'", (name, repr(value))) elif force_p and params[name][field] != value: params[name][field] = value params[name].update() self.log("Updated '%s' parameter in replicator configuration " "to have value '%s'", (name, repr(value))) # init_selections(). Record mappings between selection name and id, so # that we can transform single-select fields. def init_selections(self): self.log("Reading FIELDS and SELECTIONS tables.") fields = self.server.query(teamtrack.table['FIELDS'],'') selections = self.server.query(teamtrack.table['SELECTIONS'],'') # sn_map is a map from selection id to selection name. sn_map = {} # From TeamTrack to p4 we need to map selection id to selection name. for s in selections: sn_map[s['ID']] = s['NAME'] # fn_map is a map from field id to field name, for fields in the # CASES table. fn_map = {} for f in fields: if f['TABLEID'] == teamtrack.table['CASES']: # Normalise the case of the database field name so that we can # rely on it being uppercase throughout the code. See defect # 14 of "Alpha test report for Quokka, 2000-11-01. GDR # 2000-11-01. fn_map[f['ID']] = string.upper(f['DBNAME']) # From p4 to TeamTrack we have selection name and field name and we # need selection id. fns_map = {} for s in selections: if fn_map.has_key(s['FLDID']): field_name = fn_map[s['FLDID']] if not fns_map.has_key(field_name): fns_map[field_name] = {} fns_map[field_name][s['NAME']] = s['ID'] # Store these maps for later use. self.field_to_selection_to_id = fns_map self.selection_id_to_name = sn_map # Determine a mapping from project id and transition name to the transition # id. Determine a mapping from project id and state name to state id. def init_states(self): self.log("Reading PROJECTS and STATES tables.") # Get all the projects and states from the TeamTrack database. projects = self.server.query(teamtrack.table['PROJECTS'], '') states = self.server.query(teamtrack.table['STATES'], '') # sn_map is a map from state id to state name. sn_map = { } for s in states: # Remove leading and trailing whitespace from state name # (workaround for job00020). sn_map[s['ID']] = string.strip(s['NAME']) # pns_map is a map from project id and state name to the state id # corresponding to that state in that project. pns_map = { } for p in projects: pid = p['ID'] if not pns_map.has_key(pid): pns_map[pid] = {} for s in self.server.read_state_list(pid, 1): # Remove leading and trailing whitespace from state name # (workaround for job00020). pns_map[pid][string.strip(s['NAME'])] = s['ID'] # Remember these maps for use later. self.project_to_name_to_state = pns_map self.state_id_to_name = sn_map # Determine a mapping from project id and transition name to the transition # id. Determine a mapping from project id and state name to state id. def init_transitions(self): self.log("Reading PROJECTS table to discover available transitions.") # Get all the projects and states from the TeamTrack database. projects = self.server.query(teamtrack.table['PROJECTS'], '') # psst_map is a map from project id and a pair of state ids to the # transition id in that project that takes a case from one state to # the other. psst_map = { } for p in projects: pid = p['ID'] if not psst_map.has_key(pid): psst_map[pid] = {} for t in self.server.read_transition_list(pid): psst_map[pid][(t['OLDSTATEID'], t['NEWSTATEID'])] = t['ID'] # Remember this map for use when choosing transitions. self.project_to_states_to_transition = psst_map # init_types(). Record the mapping between issue type and the prefix # for that type. def init_types(self): self.log("Reading SELECTIONS table to find type prefixes.") types = self.server.query(teamtrack.table['SELECTIONS'], '') for t in types: self.type_id_to_prefix[t['ID']] = t['PREFIX'] # init_users(). Record the mapping between userid and username (we'll use # this to map Perforce users to TeamTrack users under the assumption that # they have the same user name in both systems). def init_users(self): self.log("Reading USERS table.") users = self.server.query(teamtrack.table['USERS'], '') for u in users: self.user_name_to_id[u['LOGINID']] = u['ID'] self.user_id_to_name[u['ID']] = u['LOGINID'] def issue(self, case_id): try: case = self.server.read_record(teamtrack.table['CASES'], int(case_id)) return self.config['case-class'](case, self) except teamtrack.tsapi_error: return None def replicate_changelist(self, change, client, date, description, status, user): query = ("TS_TYPE=%d AND TS_CHAR1='%s' AND TS_INFO1=%d" % (vcactions_type_changelist, self.rid, change)) changelists = self.server.query(teamtrack.table['VCACTIONS'], query) if len(changelists) == 0: changelist = self.server.new_record(teamtrack.table['VCACTIONS']) self.translate_changelist(changelist, change, client, date, description, status, user) changelist.add() return 1 elif self.translate_changelist(changelists[0], change, client, date, description, status, user): changelists[0].update() return 1 else: return 0 # translate_changelist(tt_changelist, change, client, date, user, # description, status). Return the changes that were made to # tt_changelist. def translate_changelist(self, tt_changelist, change, client, date, description, status, user): changes = {} changes['TYPE'] = vcactions_type_changelist changes['CHAR1'] = self.rid changes['CHAR2'] = self.sid changes['INFO1'] = change changes['AUTHOR1'] = user changes['INFO2'] = (status == 'submitted') changes['FILENAME'] = repr({'description': description, 'client': client }) for key, value in changes.items(): if tt_changelist[key] != value: tt_changelist[key] = value else: del changes[key] return changes # The date_translator class translates date fields between defect trackers # TeamTrack (0) and Perforce (1). # Some Perforce dates are reported in the form 2000/01/01 00:00:00 (e.g., dates # in changeslists) and others are reported as seconds since 1970-01-01 00:00:00 # (e.g., dates in fixes). I don't know why this is, but I have to deal with it # by checking for both formats. class date_translator(replicator.translator): date_regexps = [re.compile("^([0-9][0-9][0-9][0-9])/([0-9][0-9])/([0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$"), re.compile("^[0-9]+$")] def translate_0_to_1(self, tt_date, issues = None): assert isinstance(tt_date, types.IntType) return time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(tt_date)) def translate_1_to_0(self, p4_date, issues = None): assert isinstance(p4_date, types.StringType) match = self.date_regexps[0].match(p4_date) if match: # Note that months are 1-12 in Python, unlike in C. return int(time.mktime((int(match.group(1)), int(match.group(1)), int(match.group(2)), int(match.group(3)), int(match.group(4)), int(match.group(5)), 0, 0, 0))) elif self.date_regexps[1].match(p4_date): return int(p4_date) else: raise error, ("Unexpected date format from Perforce: '%s'" % p4_date) # The single_select_translator class translates single select fields between # defect trackers TeamTrack (0) and Perforce (1). class single_select_translator(replicator.translator): # The field that this translator translates. field = None def __init__(self, dts, field): # Call the superclass method. replicator.translator.__init__(self, dts) self.field = field # Read the database. self.dts[0].init_selections() def translate_0_to_1(self, tt_selection, issues = None): assert isinstance(tt_selection, types.IntType) if not (self.dts[0].selection_id_to_name.has_key(tt_selection)): # The selection might have been added since we last looked in the # database, so refresh our cache. self.dts[0].init_selections() if (self.dts[0].selection_id_to_name.has_key(tt_selection)): # Selections are words in Perforce and arbitrary strings in # TeamTrack, so convert spaces to underscores. return string.replace(self.dts[0].selection_id_to_name[tt_selection], ' ', '_') else: raise error, ("No TeamTrack selection name for selection id '%d'." % tt_selection) def translate_1_to_0(self, p4_selection, issues = None): assert isinstance(p4_selection, types.StringType) # Selections are words in Perforce and arbitrary strings in TeamTrack, # so convert underscores to spaces. tt_selection = string.replace(p4_selection, '_', ' ') if p4_selection == '(None)': return 0 if not (self.dts[0].field_to_selection_to_id.has_key(self.field) and self.dts[0].field_to_selection_to_id[self.field].has_key(tt_selection)): # The selection might have been added since we last looked in the # database, so refresh our cache. self.dts[0].init_selections() if (self.dts[0].field_to_selection_to_id.has_key(self.field) and self.dts[0].field_to_selection_to_id[self.field] .has_key(tt_selection)): return self.dts[0].field_to_selection_to_id[self.field][tt_selection] else: raise error, ("No TeamTrack selection for field '%s' " "corresponding to Perforce selection '%s'." % (self.field, p4_selection)) # The state_translator class translates issue statuses between defcet trackers # TeamTrack (0) and Perforce (1). class state_translator(replicator.translator): # A map from TeamTrack state name to Perforce state name. state_tt_to_p4 = { } # A map from Perforce state name to TeamTrack state name (the reverse of # the above map). state_p4_to_tt = { } # The states argument is a list of pairs (TeamTrack state name, Perforce # state name). def __init__(self, dts, states): # Call the superclass method. replicator.translator.__init__(self, dts) # Get data from the database. self.dts[0].init_states() # Compute the maps. for tt_state, p4_state in states: assert isinstance(tt_state, types.StringType) assert isinstance(p4_state, types.StringType) self.state_tt_to_p4[tt_state] = p4_state self.state_p4_to_tt[p4_state] = tt_state def translate_0_to_1(self, tt_state, issues): assert isinstance(tt_state, types.IntType) assert isinstance(issues, types.TupleType) assert len(issues) == 2 assert isinstance(issues[0], replicator.defect_tracker_issue) if not self.dts[0].state_id_to_name.has_key(tt_state): # The workflows may have changed since we last looked in the # database, so refresh our cache. self.dts[0].init_states() if self.dts[0].state_id_to_name.has_key(tt_state): tt_name = self.dts[0].state_id_to_name[tt_state] if self.state_tt_to_p4.has_key(tt_name): return self.state_tt_to_p4[tt_name] else: raise error, ("No Perforce state in corresponding to " "TeamTrack state '%s'" % tt_name) else: raise error, ("No state name for TeamTrack state %d" % tt_state) def translate_1_to_0(self, p4_state, issues): assert isinstance(p4_state, types.StringType) assert isinstance(issues, types.TupleType) assert len(issues) == 2 assert isinstance(issues[0], replicator.defect_tracker_issue) if not self.state_p4_to_tt.has_key(p4_state): raise error, ("Perforce state '%s' is unknown." % p4_state) tt_state = self.state_p4_to_tt[p4_state] project = issues[0]['PROJECTID'] if not (self.dts[0].project_to_name_to_state.has_key(project) and self.dts[0].project_to_name_to_state[project].has_key(tt_state)): # The state might have been added or the workflows changed since we # last looked in the database, so refresh our cache. self.dts[0].init_states() if (self.dts[0].project_to_name_to_state.has_key(project) and self.dts[0].project_to_name_to_state[project].has_key(tt_state)): return self.dts[0].project_to_name_to_state[project][tt_state] else: raise error, ("No TeamTrack state in project '%s' " "corresponding to Perforce state '%s'" % (project, p4_state)) # The text_translator class translates multi-line text fields between deect # trackers TeamTrack (0) and Perforce (1). class text_translator(replicator.translator): # Transform TeamTrack memo field contents to Perforce text field contents # by converting line endings. See job000008 and job000009. def translate_0_to_1(self, tt_string, issues = None): assert isinstance(tt_string, types.StringType) # Replace \r\n with \n. string = re.sub('\r\n', '\n', tt_string) # Add final newline, unless the string is empty. if tt_string: tt_string = tt_string + '\n' return tt_string # Transform Perforce text field contents to TeamTrack memo field contents # by converting line endings. See job000008 and job000009. def translate_1_to_0(self, p4_string, issues = None): assert isinstance(p4_string, types.StringType) # Remove final newline (if any). if p4_string and p4_string[-1] == '\n': p4_string = p4_string[:-1] # Replace \n with \r\n. p4_string = re.sub('\n', '\r\n', p4_string) return p4_string # The user_translator class translates users between defect trackers TeamTrack # (0) and Perforce (1). # # User fields in TeamTrack can be empty (that is, userid is 0). We replicate # these to and from the dummy user "(None)" in Perforce, since that's how # non-existent users show up in TeamTrack. We considered requiring users in # TeamTrack to have licences for Perforce, and vice versa, but after discussion # with Perforce this requirement was dropped. class user_translator(replicator.translator): def __init__(self, dts): # Call the superclass method. replicator.translator.__init__(self, dts) # Get data from the database. self.dts[0].init_users() def translate_0_to_1(self, tt_user, issues = None): assert isinstance(tt_user, types.IntType) if tt_user == 0: return '(None)' if not self.dts[0].user_id_to_name.has_key(tt_user): # The user might have been added since we last looked in the # TS_USERS table, so refresh our cache. self.dts[0].init_users() if self.dts[0].user_id_to_name.has_key(tt_user): return self.dts[0].user_id_to_name[tt_user] else: return '(None)' def translate_1_to_0(self, p4_user, issues = None): assert isinstance(p4_user, types.StringType) if p4_user == '(None)': return 0 if not self.dts[0].user_name_to_id.has_key(p4_user): # The user might have been added since we last looked in the # TS_USERS table, so refresh our cache. self.dts[0].init_users() if self.dts[0].user_name_to_id.has_key(p4_user): return self.dts[0].user_name_to_id[p4_user] else: return 0