# 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 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, p4_fix): 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['TIME1'] = 0 fix_record['TIME2'] = 0 fix = self.dt.config['fix-class'](fix_record, self) fix.transform_from_p4(p4_fix) fix.add() def conflicting_p(self): return self['P4DTI_ACTION'] == 'wait' 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 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 jobname(self): jobname = self['P4DTI_JOBNAME'] if jobname: return jobname 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.jobname() self['P4DTI_ACTION'] = 'replicate' self.update() def transform_to_job(self, job): # Work out the set of proposed changes. changes = {} changes['Job'] = self.jobname() changes['P4DTI-rid'] = self.rid() changes['P4DTI-issue-id'] = self.id() for dt_field, p4_field, type in self.dt.config['replicated-fields']: changes[p4_field] = self.transform_dt_to_p4(self[dt_field], type, dt_field) # Apply the proposed changes over to the job where it differs; # delete proposed changes where the job is the same. for key, value in changes.items(): # [1] The key might not be in the job because "optional" fields in Perforce # don't show up when you go a p4 -G job -o to fetch the job. So check for # key existence first. See "TeamShare PSG alpha test report, 2000-10-23". # GDR 2000-10-24. if not job.has_key(key) or job[key] != value: job[key] = value else: del changes[key] # Return new job and the set of changes. return job, changes def transform_from_job(self, job, fix_diffs, filespec_diffs): # Work out the set of proposed changes to the case. changes = {} changes['P4DTI_RID'] = self.dt.rid changes['P4DTI_SID'] = self.dt.sid changes['P4DTI_JOBNAME'] = job['Job'] for dt_field, p4_field, type in self.dt.config['replicated-fields']: # See note [1] above. if job.has_key(p4_field): changes[dt_field] = self.transform_p4_to_dt(job[p4_field], type, dt_field) else: changes[dt_field] = self.transform_p4_to_dt('', type, dt_field) old_state = self['STATE'] # Apply the proposed changes over to the case where it differs; delete # proposed changes where the case is the same. for key, value in changes.items(): if self[key] != value: self[key] = value else: del changes[key] # Work out a transition based on the old case state and the new case # state, if the state changed. Otherwise, don't use a transition. transition = None if changes.has_key('STATE'): transition_id = self.dt.transition(self['PROJECTID'], old_state, changes['STATE']) if transition_id: userid = self.dt.transform_user_p4_to_dt(job['P4DTI-user']) user = self.dt.user_id_to_name[userid] transition = { 'user': user, 'id': transition_id } 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'])) # Return the set of changes and the proposed transition. return changes, transition def transform_dt_to_p4(self, value, type, dt_fieldname): if type == 'state': return self.dt.transform_state_dt_to_p4(value, self['PROJECTID']) elif type == 'text': return self.dt.transform_text_dt_to_p4(value) elif type == 'user': return self.dt.transform_user_dt_to_p4(value) elif type == 'single-select': return self.dt.transform_single_select_dt_to_p4(value) else: return value def transform_p4_to_dt(self, value, type, dt_fieldname): if type == 'state': return self.dt.transform_state_p4_to_dt(value, self['PROJECTID']) elif type == 'text': return self.dt.transform_text_p4_to_dt(value) elif type == 'user': return self.dt.transform_user_p4_to_dt(value) elif type == 'single-select': return self.dt.transform_single_select_p4_to_dt(value, dt_fieldname) else: return value def update(self, transition = None): if transition: self.case.transition(transition['user'], transition['id']) else: self.case.update() def update_action(self, action): if self['P4DTI_ACTION'] != action: self['P4DTI_ACTION'] = action self.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, p4_fix): self['INFO1'] = int(p4_fix['Change']) self['AUTHOR1'] = self.case.dt.transform_user_p4_to_dt(p4_fix['User']) self['FILENAME'] = repr({ 'status': p4_fix['Status'], 'client': p4_fix['Client'] }) def update(self, p4_fix): self.transform_from_p4(p4_fix) 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', # A list of fields that will be replicated between issues and # jobs. Each entry is a 3-tuple (TeamTrack field name, Perforce # field name, field type). 'replicated-fields' : [ ( 'OWNER', 'User', 'user' ), ( 'DESCRIPTION', 'Description', 'text' ), ( 'STATE', 'Status', 'state' ), ], # A map from TeamTrack state name to Perforce status name. 'state-dt-to-p4' : { 'Assigned': 'assigned', 'Closed': 'closed', 'Deferred': 'deferred', 'New': 'new', 'Open': 'assigned', 'Resolved': 'resolved', 'Verified': 'closed', }, # A map from Perforce status name to TeamTrack state name. 'state-p4-to-dt' : { 'assigned': 'Assigned', 'closed': 'Verified', 'deferred': 'Deferred', 'new': 'New', 'resolved': 'Resolved', }, } project_to_name_to_state = { } project_to_states_to_transition = { } rid = None selection_id_to_name = { } field_to_selection_to_id = { } server = None sid = None state_id_to_name = { } type_id_to_prefix = { } user_id_to_name = { } 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']) # Initialise various maps. self.init_types() self.init_users() # Get the userid corresponding to the replicator's userid. This will # be used in queries to ignore records changed most recently by the # replicator. if not self.user_name_to_id[self.config['user']]: raise error, ("No login id in TeamTrack's USERS table corresponds " "to replicator's login id '%s'." % self.config['user']) self.config['userid'] = self.user_name_to_id[self.config['user']] 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_issues(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 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 == [] return changed_cases, None def changed_issues_are_replicated(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): fields = self.server.query(teamtrack.table['FIELDS'],'') selections = self.server.query(teamtrack.table['SELECTIONS'],'') # From TeamTrack to p4 we need to map selection id to selection name. for s in selections: self.selection_id_to_name[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']: fn_map[f['ID']] = 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'] self.field_to_selection_to_id = fns_map # 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): 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'] # init_types(). Record the mapping between issue type and the prefix # for that type def init_types(self): types = self.server.query(teamtrack.table['SELECTIONS'], '') for t in types: self.type_id_to_prefix[t['ID']] = t['PREFIX'] # 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_workflows(self): # 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']) # 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 = { } # 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 psst_map.has_key(pid): psst_map[pid] = {} if not pns_map.has_key(pid): pns_map[pid] = {} for t in self.server.read_transition_list(pid): psst_map[pid][(t['OLDSTATEID'], t['NEWSTATEID'])] = t['ID'] 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 all these maps for use later (in transform_state_* and when # choosing transitions). self.project_to_name_to_state = pns_map self.project_to_states_to_transition = psst_map self.state_id_to_name = sn_map 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, p4_changelist): query = ("TS_CHAR1='%s' AND TS_INFO1=%d" % (self.rid, int(p4_changelist['Change']))) dt_changelists = self.server.query(teamtrack.table['VCACTIONS'], query) if len(dt_changelists) == 0: dt_changelist = self.server.new_record(teamtrack.table['VCACTIONS']) self.transform_changelist(dt_changelist, p4_changelist) dt_changelist.add() return 1 elif self.transform_changelist(dt_changelists[0], p4_changelist): dt_changelists[0].update() return 1 else: return 0 # transform_changelist(dt_changelist, p4_changelist). Return the changes # that were made to dt_changelist. def transform_changelist(self, dt_changelist, p4_changelist): changes = {} changes['TYPE'] = vcactions_type_changelist changes['CHAR1'] = self.rid changes['CHAR2'] = self.sid changes['INFO1'] = int(p4_changelist['Change']) # [2] We can't insist on every user who made a change in Perforce having a # corresponding userid in TeamTrack. So supply 0 for the raise_if_none # argument so that unknown users get converted to (None). changes['AUTHOR1'] =self.transform_user_p4_to_dt(p4_changelist['User'], raise_if_none = 0) changes['INFO2'] = (p4_changelist['Status'] == 'submitted') changes['FILENAME'] = repr({'description':p4_changelist['Description'], 'client': p4_changelist['Client'] }) for key, value in changes.items(): if dt_changelist[key] != value: dt_changelist[key] = value else: del changes[key] return changes # It would be better not to pass the dt field name to each call of this # function, but to instantiate different transformers for each kind of # selection field. def transform_single_select_p4_to_dt(self, p4_selection, field_name): # Selections are words in Perforce and arbitrary strings in TeamTrack, # so convert underscores to spaces. dt_selection = string.replace(p4_selection, '_', ' ') if p4_selection == '(None)': return 0 for i in range(2): if (self.field_to_selection_to_id.has_key(field_name) and self.field_to_selection_to_id[field_name].has_key(dt_selection)): return self.field_to_selection_to_id[field_name][dt_selection] if i > 0: raise error, ("No TeamShare selection for field '%s' " "corresponding to Perforce selection '%s'." % (field_name, p4_selection)) # The selection might have been added since we # last looked in the database, so refresh our cache. self.init_selections() def transform_single_select_dt_to_p4(self, dt_selection): for i in range(2): if (self.selection_id_to_name.has_key(dt_selection)): # Selections are words in Perforce and arbitrary strings in TeamTrack, # so convert spaces to underscores. return string.replace(self.selection_id_to_name[dt_selection], ' ', '_') if i > 0: raise error, ("No TeamShare selection name for selection id '%d'." % dt_selection) # The selection might have been added since we # last looked in the database, so refresh our cache. self.init_selections() def transform_state_p4_to_dt(self, p4_state, project_id): assert(self.config['state-p4-to-dt'].has_key(p4_state)) dt_state = self.config['state-p4-to-dt'][p4_state] for i in range(2): if (self.project_to_name_to_state.has_key(project_id) and self.project_to_name_to_state[project_id].has_key(dt_state)): return self.project_to_name_to_state[project_id][dt_state] if i > 0: raise error, ("No TeamShare state in project '%s' " "corresponding to Perforce state '%s'" % (project_id, p4_state)) # The state might have been added or the workflows changed since we # last looked in the database, so refresh our cache. self.init_workflows() def transform_state_dt_to_p4(self, dt_state, project_id): for i in range(2): if self.state_id_to_name.has_key(dt_state): name = self.state_id_to_name[dt_state] if self.config['state-dt-to-p4'].has_key(name): return self.config['state-dt-to-p4'][name] if i > 0: raise error, ("No Perforce state in corresponding to " "TeamTrack state '%s'" % name) if i > 0: raise error, ("No state name for TeamTrack state %d" % dt_state) # The workflows may have changed since we last looked in the # database, so refresh our cache. self.init_workflows() # Transform TeamTrack memo field contents to Perforce text field contents # by converting line endings. See job000008 and job000009. def transform_text_dt_to_p4(self, string): # Replace \r\n with \n. string = re.sub('\r\n', '\n', string) # Add final newline. string = string + '\n' return string # Transform Perforce text field contents to TeamTrack memo field contents # by converting line endings. See job000008 and job000009. def transform_text_p4_to_dt(self, string): # Remove final newline (if any). if string and string[-1] == '\n': string = string[:-1] # Replace \n with \r\n. string = re.sub('\n', '\r\n', string) return string def transform_user_p4_to_dt(self, p4_user, raise_if_none = 1): # [3] 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. Note that # this needs to be consistent with note [2] above. if p4_user == '(None)': return 0 for i in range(2): if self.user_name_to_id.has_key(p4_user): return self.user_name_to_id[p4_user] if i > 0: if raise_if_none: raise error, ("No TeamShare user corresponding to " "Perforce user '%s'" % p4_user) else: return 0 # The user might have been added since we last looked in the # TS_USERS table, so refresh our cache. self.init_users() def transform_user_dt_to_p4(self, dt_user): # See note [3] above. if dt_user == 0: return '(None)' for i in range(2): if self.user_id_to_name.has_key(dt_user): return self.user_id_to_name[dt_user] if i > 0: raise error, ("No Perforce user corresponding to TeamShare " "user '%s'" % dt_user) # The user might have been added since we last looked in the # TS_USERS table, so refresh our cache. self.init_users() def transition(self, project_id, old_state, new_state): ss = (old_state, new_state) for i in range(2): if (self.project_to_states_to_transition.has_key(project_id) and self.project_to_states_to_transition[project_id].has_key(ss)): return self.project_to_states_to_transition[project_id][ss] if i > 0: # No appropriate transition found. return None # The workflows may have changed since we last looked in the # database, so refresh our cache. self.init_workflows()