# 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