# 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()