# 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 time
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
# sql_escape: string -> string. Return the input string, escaped for inclusion
# in a SQL query by doubling single quotes. This works for Microsoft Access,
# but I'm not sure that it's correct ANSI SQL. See job000031.
def sql_escape(s):
return string.replace(s, "'", "''")
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):
assert isinstance(key, types.StringType)
return self.case[key]
def __repr__(self):
return repr(self.case)
def __setitem__(self, key, value):
assert isinstance(key, types.StringType)
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, status, user):
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, status, user)
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.read_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 = {}):
assert isinstance(user, types.IntType)
assert isinstance(changes, types.DictType)
# 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 not transition:
# Don't change the state except through a transition. GDR
# 2000-10-27.
raise error, ("No transition from state '%s' to state '%s'."
% (self.dt.state_id_to_name[self['STATE']],
self.dt.state_id_to_name[changes['STATE']]))
for key, value in changes.items():
self[key] = value
user = self.dt.user_id_to_name[user]
if transition:
self.dt.log("-- Transition: %d; User: %s", (transition, user))
self.case.transition(user, transition)
def update_action(self, action):
assert action in ['keep','discard','replicate','wait']
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):
assert isinstance(case, teamtrack_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):
assert isinstance(key, types.StringType)
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):
assert isinstance(change, types.IntType)
assert isinstance(client, types.StringType)
assert isinstance(date, types.IntType)
assert isinstance(status, types.StringType)
assert isinstance(user, types.IntType)
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):
assert isinstance(key, types.StringType)
return self.filespec[key]
def __setitem__(self, key, value):
assert isinstance(key, types.StringType)
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',
'state-pairs' : [],
'changelist-url' : None,
}
rid = None
server = None
sid = None
# A map from auxiliary table ID to the ID of the entities in that table to
# the name for that entity. For example, if entity 7 in the PROJECTS table
# (table 8) has the name "Image Builder", then table_to_id_to_name[8][7] ==
# "Image Builder".
table_to_id_to_name = { }
# A map from auxiliary table ID to the ID of the entities in that table to
# the name for that entity. For example, if entity 7 in the PROJECTS table
# (table 8) has the name "Image Builder", then
# table_to_name_to_id[8]["Image Builder"] == 7.
table_to_name_to_id = { }
# A map from field name to selection name 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 lowercased state name to state
# id. So for example, if project 7 has a state called "Open" with id 25,
# then project_to_name_to_state[7]['open'] == 25. The reason why the state
# names are lowercase here is because they are lowercase in Perforce for
# usability, to avoid having two states which differ only in case.
project_to_name_to_state = { }
# 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 TeamTrack state id to state name.
state_id_to_name = { }
# 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 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.
user_name_to_id = { }
# A map from TeamTrack user id to their e-mail address.
user_id_to_email = { }
# A map from TeamTrack user's e-mail address to their user id.
user_email_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.read_types()
self.read_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.
user = self.server.query(teamtrack.table['USERS'], "TS_LOGINID = '%s'"
% sql_escape(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']
# 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.")
# config_params gives 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?) See the TeamTrack schema extensions document for the
# meaning of these configuration parameters.
# Look up the LASTID field in the TABLES table for the CHANGES table;
# this is the highest value for the ID used in the CHANGES table.
# We'll use this for the initial value of the LAST_CHANGE parameter
# (unless there's a LAST_CHANGE parameter there already). See
# job000047 and the TeamTrack schema documentation.
last_change = self.server.query(teamtrack.table['TABLES'], 'TS_ID = %d' % teamtrack.table['CHANGES'])[0]['LASTID']
# Build a string of status values separated by / for the STATUS_VALUES
# keyword.
status_values = string.join(map(lambda p: p[1],
self.config['state-pairs']),'/')
config_params = [ ( 'LAST_CHANGE', 'INFO1', last_change, 0 ),
( 'SERVER', 'FILENAME',
repr({ 'sid': self.sid, 'description':
self.config['p4-server-description']}), 1 ),
( 'STATUS_VALUES', 'FILENAME',
repr({ 'sid': self.sid, 'description':
status_values }), 1 ),
( 'CHANGELIST_URL', 'FILENAME',
repr({ 'sid': self.sid, 'description':
self.config['changelist-url']}), 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)))
def issue(self, case_id):
assert isinstance(case_id, types.StringType)
try:
case = self.server.read_record(teamtrack.table['CASES'],
int(case_id))
return self.config['case-class'](case, self)
except ValueError:
# case_id was not a number and int() failed.
return None
except teamtrack.tsapi_error:
# No such issue.
return None
# read_auxiliary_table(table). Record mappings between id and name
# for a given auxiliary table (the tables PROJECTS, COMPANIES, PRODUCTS,
# SERVICEAGREEMENTS are suitable for this method).
def read_auxiliary_table(self, table):
assert isinstance(table, types.IntType)
self.log("Reading table %d.", (table,))
records = self.server.query(table, '')
self.table_to_id_to_name[table] = { }
self.table_to_name_to_id[table] = { }
for r in records:
self.table_to_id_to_name[table][r['ID']] = r['NAME']
if self.table_to_name_to_id[table].has_key(r['NAME']):
self.log("Warning: table '%s' has two entries called '%s'.",
(table_name, r['NAME']))
self.table_to_name_to_id[table][r['NAME']] = r['ID']
# read_selections(). Record mappings between selection name and id, so
# that we can transform single-select fields.
def read_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 read_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:
# Normalize the case of the state name. See "Case of state names"
# design decisions [RB 2000-11-28].
sn_map[s['ID']] = string.lower(s['NAME'])
# pns_map is a map from project id and lower-cased 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):
# Normalize the case of the state name. See "Case of state
# names" design decisions [RB 2000-11-28].
pns_map[pid][string.lower(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 read_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
# read_types(). Record the mapping between issue type and the prefix
# for that type.
def read_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']
# read_users(). Record the mapping between userid and username; and
# between userid and e-mail address (we'll use the latter to map Perforce
# users to TeamTrack users under the assumption that they have the same
# e-mail address in both systems).
def read_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']
self.user_email_to_id[u['EMAIL']] = u['ID']
self.user_id_to_email[u['ID']] = u['EMAIL']
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, description,
# status, user). Return the changes that were made to tt_changelist.
def translate_changelist(self, tt_changelist, change, client, date, description, status, user):
assert isinstance(change, types.IntType)
assert isinstance(client, types.StringType)
assert isinstance(date, types.IntType)
assert isinstance(description, types.StringType)
assert isinstance(status, types.StringType)
assert isinstance(user, types.IntType)
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['TIME1'] = date
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, tt, p4, case = None, job = None):
assert isinstance(tt_date, types.IntType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
return time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(tt_date))
def translate_1_to_0(self, p4_date, tt, p4, case = None, job = None):
assert isinstance(p4_date, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
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(2)),
int(match.group(3)), int(match.group(4)),
int(match.group(5)), int(match.group(6)),
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 auxiliary_translator class translates fields that cross-reference an
# auxiliary table (e.g. PROJECTS) between defect trackers TeamTrack (0) and
# Perforce (1).
class auxiliary_translator(replicator.translator):
table_name = None
table = None
def __init__(self, table_name):
if not teamtrack.table.has_key(table_name):
raise error, ("No such table: %s" % table_name)
self.table_name = table_name
self.table = teamtrack.table[table_name]
def translate_0_to_1(self, tt_value, tt, p4, case = None, job = None):
assert isinstance(tt_value, types.IntType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
if tt_value == 0:
return '(None)'
if not (tt.table_to_id_to_name.has_key(self.table) and
tt.table_to_id_to_name[self.table].has_key(tt_value)):
# The entity might have been added since we last looked in the
# database, so refresh our cache.
tt.read_auxiliary_table(self.table)
if (tt.table_to_id_to_name.has_key(self.table) and
tt.table_to_id_to_name[self.table].has_key(tt_value)):
return tt.table_to_id_to_name[self.table][tt_value]
else:
raise error, ("No TeamTrack entity in table '%s' with id %d."
% (self.table_name, tt_selection))
def translate_1_to_0(self, p4_value, tt, p4, case = None, job = None):
assert isinstance(p4_value, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
if p4_value == '(None)':
return 0
if not (tt.table_to_name_to_id.has_key(self.table) and
tt.table_to_name_to_id[self.table].has_key(p4_value)):
# The entity might have been added since we last looked in the
# database, so refresh our cache.
tt.read_auxiliary_table(self.table)
if (tt.table_to_name_to_id.has_key(self.table) and
tt.table_to_name_to_id[self.table].has_key(p4_value)):
return tt.table_to_name_to_id[self.table][p4_value]
else:
raise error, ("No TeamTrack entity in table '%s' with name '%s'."
% (self.table_name, p4_value))
# translate_keyword_tt_to_p4: string -> string. Translates a TeamTrack name to
# a Perforce keyword. TeamTrack names can have arbitrary characters in them,
# but Perforce keywords can't have spaces or slashes and possibly can't have
# various other characters in them, so we'll play safe and translate certain
# non-alphanumeric characters to %xx (xx being the hexadecimal character code
# for the offending character). In order to make the translated keywords more
# readable, we translate space to underscore.
#
# Note that this map is reversible so that we can translate keywords in the
# other direction (see translate_keyword_p4_to_tt below).
#
# These two functions are provided separately because they may be used by
# several translators.
def translate_keyword_tt_to_p4(tt_keyword):
assert isinstance(tt_keyword, types.StringType)
def translate_match(match):
if match.group(0) == ' ':
return '_'
else:
return '%%%02x' % ord(match.group(0))
return re.sub('[^\w(),.?!-]', translate_match, tt_keyword)
# translate_keyword_p4_to_tt: string -> string. Translates a Perforce keyword
# to a TeamTrack name. Reverse of the translation above.
def translate_keyword_p4_to_tt(p4_keyword):
assert isinstance(p4_keyword, types.StringType)
def translate_match(match):
if match.group(0) == '_':
return ' '
else:
return chr(string.atoi(match.group(1), 0x10))
return re.sub('%([\da-fA-F]{2})|_', translate_match, p4_keyword)
# 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, field):
self.field = field
def translate_0_to_1(self, tt_selection, tt, p4, case = None, job = None):
assert isinstance(tt_selection, types.IntType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
if not (tt.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.
tt.read_selections()
if (tt.selection_id_to_name.has_key(tt_selection)):
# Selections are 'word's in Perforce and arbitrary strings in
# TeamTrack, so translate it.
return translate_keyword_tt_to_p4(tt.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, tt, p4, case = None, job = None):
assert isinstance(p4_selection, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
# Selections are 'word's in Perforce and arbitrary strings in
# TeamTrack, so translate it.
tt_selection = translate_keyword_p4_to_tt(p4_selection)
if p4_selection == '(None)':
return 0
if not (tt.field_to_selection_to_id.has_key(self.field)
and tt.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.
tt.read_selections()
if (tt.field_to_selection_to_id.has_key(self.field)
and tt.field_to_selection_to_id[self.field].has_key(tt_selection)):
return tt.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, 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, tt, p4, case = None, job = None):
assert isinstance(tt_state, types.IntType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
if not tt.state_id_to_name.has_key(tt_state):
# The workflows may have changed since we last looked in the
# database, so refresh our cache.
tt.read_states()
if tt.state_id_to_name.has_key(tt_state):
tt_name = tt.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, tt, p4, case, job = None):
assert isinstance(p4_state, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert isinstance(case, teamtrack_case)
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 = case['PROJECTID']
if not (tt.project_to_name_to_state.has_key(project)
and tt.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.
tt.read_states()
if (tt.project_to_name_to_state.has_key(project)
and tt.project_to_name_to_state[project].has_key(tt_state)):
return tt.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, tt, p4, case = None, job = None):
assert isinstance(tt_string, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
# Replace \r\n with \n.
tt_string = string.replace(tt_string, '\r\n', '\n')
# 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, tt, p4, case = None, job = None):
assert isinstance(p4_string, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.defect_tracker)
assert case == None or isinstance(case, teamtrack_case)
# 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 = string.replace(p4_string, '\n', '\r\n')
return p4_string
# The user_translator class translates users between defect trackers TeamTrack
# (0) and Perforce (1).
#
# The user_translator needs to cope three two special cases.
#
# First, 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.
#
# Second, we don't insist that all users in Perforce have licences in
# TeamTrack. For example, a user who made a changelist long ago and has left
# the company. These users are mapped to the TeamTrack userid 0. (But note
# that people shouldn't be able to *change* issues in TeamTrack unless they
# have a licence -- this was agreed with TeamShare [ref?]. It isn't the
# responsibility of the user_translator to worry about this. That's up to
# the teamtrack_case.update method). See job000087.
#
# Third, we don't insist that all users in TeamTrack have licences in Perforce
# (we are discussing this with Perforce and the requirement may be dropped
# [ref?]). For these users we simply put their TeamTrack user name in Perforce
# -- this works because Perforce doesn't check user names.
class user_translator(replicator.translator):
# Have the maps been build yet? (Each instance of this class only runs
# match_users once.)
matched_users = 0
# A map from TeamTrack user id to Perforce username (for users where we can
# work out a correspondence by e-mail address).
user_tt_to_p4 = { }
# A map from Perforce username to TeamTrack user id (for users where we can
# work out a correspondence by e-mail address).
user_p4_to_tt = { }
# Obtain a dictionary email -> Perforce id. (This method copied from
# dt_bugzilla.user_translator; it probably belongs in the dt_perforce class
# so it can be shared between user translators for various defect tracking
# systems.)
def p4_user_dict(self, p4) :
p4_users = p4.p4.run("users")
dict = {}
for user in p4_users :
dict[user['Email']] = user['User']
return dict
# Build the user_tt_to_p4 and user_p4_to_tt maps.
def match_users(self, tt, p4):
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.dt_perforce)
if self.matched_users:
return
# Read TeamTrack users and e-mail addresses.
tt.read_users()
# Read Perforce users and e-mail addresses.
p4_email_to_user = self.p4_user_dict(p4)
# Clear the maps.
self.user_tt_to_p4 = {}
self.user_p4_to_tt = {}
# Pair up users by e-mail address.
for user, email in tt.user_id_to_email.items():
if p4_email_to_user.has_key(email):
tt.log("Matched TeamTrack user '%s' with Perforce user '%s' by e-mail address '%s'.", (tt.user_id_to_name[user], p4_email_to_user[email], email))
self.user_tt_to_p4[user] = p4_email_to_user[email]
self.user_p4_to_tt[p4_email_to_user[email]] = user
self.matched_users = 1
def translate_0_to_1(self, tt_user, tt, p4, case = None, job = None):
assert isinstance(tt_user, types.IntType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.dt_perforce)
assert case == None or isinstance(case, teamtrack_case)
if tt_user == 0:
return '(None)'
self.match_users(tt, p4)
# Is the user in our map by e-mail address?
if self.user_tt_to_p4.has_key(tt_user):
return self.user_tt_to_p4[tt_user]
# They are not, so use the TeamTrack username as the Perforce username.
if not tt.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.
tt.read_users()
if tt.user_id_to_name.has_key(tt_user):
return tt.user_id_to_name[tt_user]
else:
return '(None)'
def translate_1_to_0(self, p4_user, tt, p4, case = None, job = None):
assert isinstance(p4_user, types.StringType)
assert isinstance(tt, dt_teamtrack)
assert isinstance(p4, replicator.dt_perforce)
assert case == None or isinstance(case, teamtrack_case)
if p4_user == '(None)':
return 0
self.match_users(tt, p4)
# Is the user in our map by e-mail address?
if self.user_p4_to_tt.has_key(p4_user):
return self.user_p4_to_tt[p4_user]
# They are not, so use the Perforce username as the TeamTrack username.
if not tt.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.
tt.read_users()
if tt.user_name_to_id.has_key(p4_user):
return tt.user_name_to_id[p4_user]
else:
return 0
# B. Document History
#
# 2000-12-05 GDR Starts replicating TeamTrack data from installation and not
# from the beginning of time.
#
# 2000-12-06 GDR Report both the user and the transition when a transition is
# generated, to make job000133 easier to spot and debug if it happens again.
# Improved message sent when a transition can't be found, by using the state
# names rather than the state ID numbers.
#
# 2000-12-07 GDR Updated user_translator so that it matches users by e-mail
# address if it can; otherwise it defaults to the original algorithm: assume
# that they are identical.