# Perforce Defect Tracking Integration Project
#
#
# CONFIGURE_TEAMTRACK.PY -- BUILD P4DTI CONFIGURATION FOR TEAMTRACK
#
# Gareth Rees, Ravenbrook Limited, 2000-11-17
#
#
# 1. INTRODUCTION
#
# This module defines a configuration generator for the TeamTrack
# integration. Configuration generators are documented in detail in
# [GDR 2000-10-16, 8].
#
# The intended readership of this document is project developers.
#
# This document is not confidential.
import catalog
import check_config
import dt_teamtrack
import logger
import re
import string
import teamtrack
import time
import translator
import types
error = "TeamTrack configuration error"
# 2. BUILD THE MAPPING BETWEEN STATES IN TEAMTRACK AND PERFORCE
#
# The make_state_pairs function takes a TeamTrack connection and the
# "closed state", and returns a list of pairs of state names (TeamTrack
# state, Perforce state). This list will be used to translate between
# states, and also to generate the possible values for the State field
# in Perforce.
#
# The closed_state argument is the TeamTrack state which maps to the
# special state 'closed' in Perforce, or None if there is no such state.
# See requirement 45. See the decision decision [RB 2000-11-28b].
#
# The case of state names in these pairs is normalized for usability in
# Perforce: see the design decision [RB 2000-11-28a].
keyword_translator = translator.keyword_translator()
def make_state_pairs(config, teamtrack_connection, closed_state):
# 2.1. Get TeamTrack states
#
# Select states belong to CASES workflows (ignore states belonging
# to INCIDENTS workflows). This was motivated by job000194. See
# the definition of the TS_STATES table in the TeamTrack database
# schema [TeamShare 2001-04-30].
query = ('TS_PROJECTID IN (SELECT TS_ID FROM TS_WORKFLOWS WHERE '
'TS_TABLEID = %d)' % teamtrack_connection.case_table_id())
if not config.use_deleted_selections:
query = query + ' AND TS_STATUS = 0'
states = teamtrack_connection.query(teamtrack.table['STATES'],
query)
state_pairs = []
state_p4_to_tt = {}
found_closed_state = 0
# 2.2. Make Perforce versions of the state names
#
# We convert the TeamTrack state name to lowercase (so that it's
# easy to type in Perforce), apply the keyword translator (so that
# the state is legal in Perforce), and then apply two special cases:
#
# 1. The "closed_state" configuration parameter.
#
# 2. Perforce jobs can't have state "new" (this indicates a fresh
# job and Perforce changes the state to "open"). Nor can they have
# state "ignore", because that is used in the submit form to
# indicate that a job shouldn't be fixed by the change.
#
# Unfortunately, "new" and "ignore" are common names for states in
# the defect tracker (the former is in the default workflow in
# TeamTrack), so we don't disallow them, but translate them to
# "_new" and "_ignore"; we disallow those instead (or rather, we
# quit if two TeamTrack states map to the same state in Perforce.)
# See job000141.
for s in states:
tt_state = string.lower(s['NAME'])
if closed_state and tt_state == string.lower(closed_state):
p4_state = 'closed'
found_closed_state = 1
else:
p4_state = keyword_translator.translate_0_to_1(tt_state)
if p4_state in ['new', 'ignore']:
p4_state = '_' + p4_state
if (state_p4_to_tt.has_key(p4_state)
and state_p4_to_tt[p4_state] != tt_state):
# "Two TeamTrack states '%s' and '%s' map to the same
# Perforce state '%s'."
raise error, catalog.msg(400, (tt_state,
state_p4_to_tt[p4_state],
p4_state))
state_p4_to_tt[p4_state] = tt_state
pair = (tt_state, p4_state)
if pair not in state_pairs:
state_pairs.append(pair)
if closed_state != None and not found_closed_state:
# "You specified the closed_state '%s', but there's no such
# TeamTrack state."
raise error, catalog.msg(401, closed_state)
return state_pairs
# 3. CONVERT DATE/TIME TO SECONDS
#
# This function converts a date/time in standard format, like
# '2001-02-12 19:19:24' [ISO 8601] into seconds since the epoch.
#
# We use this to convert the start_date configuration parameter. It is
# specified as an date/time for ease of entry, but TeamTrack represents
# date/times as seconds since the epoch. (Note that we specify -1 for
# the DST flag -- see job000381).
def convert_isodate_to_secs(isodate):
assert isinstance(isodate, types.StringType)
date_re = "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$"
match = re.match(date_re, isodate)
assert match
return time.mktime(tuple(map(int, match.groups()) + [0,0,-1]))
# 4. MIGRATION FUNCTIONS
def prepare_issue_advanced(config, tt, p4, dict, job):
# Deduce a SUBMITTER for the issue, unless we have one already.
if dict.get('SUBMITTER', 0) == 0:
for field in ['P4DTI-user', 'Owner', 'User']:
if job.has_key(field):
s = config.user_translator.translate_1_to_0(
job[field], tt, p4)
dict['SUBMITTER'] = s
break
# if LASTMODIFIEDDATE or SUBMITDATE is replicated and hasn't been
# filled in, remove it (TeamTrack will fill it in).
if dict.get('LASTMODIFIEDDATE', None) == -2:
del dict['LASTMODIFIEDDATE']
if dict.get('SUBMITDATE', None) == -2:
del dict['SUBMITDATE']
# Supply default of 0 for ISSUETYPE as promised [GDR 2001-11-14, 3].
dict['ISSUETYPE'] = dict.get('ISSUETYPE', 0)
# Supply default of 0 for PROJECTID as promised [GDR 2001-11-14, 3].
dict['PROJECTID'] = dict.get('PROJECTID', 0)
# Call user hook (see [GDR 2001-11-14, 3]).
config.prepare_issue(dict, job)
def translate_jobspec_advanced(config, dt, p4, job):
# Call user hook (see [GDR 2001-11-14, 4.6]).
return config.translate_jobspec(job)
# 5. BUILD P4DTI CONFIGURATION FOR TEAMTRACK
def configuration(config):
# 5.1. Check TeamTrack-specific configuration parameters
check_config.check_list_of(config, 'replicated_fields',
types.StringType, 'strings')
check_config.check_string(config, 'teamtrack_password')
check_config.check_string(config, 'teamtrack_server')
check_config.check_string(config, 'teamtrack_user')
check_config.check_string(config, 'teamtrack_version')
check_config.check_bool(config, 'use_windows_event_log')
# 5.2. Open a connection to the TeamTrack server
connection = teamtrack.connect(config.teamtrack_user,
config.teamtrack_password,
config.teamtrack_server)
# 5.3. Translators
#
# We make only one user translator so that the TeamTrack USER table
# only needs to be read once when the replicator starts and once
# when there's a cache miss. See job000148.
date_translator = dt_teamtrack.date_translator()
elapsed_time_translator = dt_teamtrack.elapsed_time_translator()
int_translator = dt_teamtrack.int_translator()
float_translator = dt_teamtrack.float_translator()
text_translator = dt_teamtrack.text_translator()
user_translator = dt_teamtrack.user_translator()
state_pairs = make_state_pairs(config, connection,
config.closed_state)
state_translator = dt_teamtrack.state_translator(state_pairs)
# 5.4. TeamTrack field types
#
# The table 'type_table' defines which TeamTrack field types are
# supported by the P4DTI, and defines how values in these fields are
# translated between TeamTrack and Perforce.
#
# 'type_table' is a map from TeamTrack field type to a tuple
# (TeamTrack field type name, Perforce field type, Perforce field
# length, translator).
#
# The TeamTrack field type is the value in the TS_FLDTYPE column in
# the TS_FIELDS table; see the TeamTrack schema for details
# [TeamShare 2001-04-30].
#
# The field type name is for documentation only.
#
# Commented out fields are ones we don't support. If new
# translators are written for those field types, we can uncomment
# them.
#
# This table isn't complete. There are a number of special cases
# that will be applied when each field is considered:
#
# 1. A TEXT field (type 101) with TS_ATTRIBUTES = 0 is actually a
# MEMO field.
#
# 2. A DATETIME field (type 103) with TS_ATTRIBUTES = 2 or 3 is
# actually an elapsed time field. We have the dummy type 'time' for
# elapsed time fields.
#
# 3. Each SELECTION field (type 104) needs its own translator, and
# the translator needs to know the name of the field in the TS_CASES
# table. So we make a translator object for each replicated field.
#
# 4. A TEXT field (type 101) with TS_ATTRIBUTES = 2 or 3 is a
# journal field. See [GDR 2001-09-26].
aux = dt_teamtrack.auxiliary_translator
type_table = {
100: ( 'NUMERIC', 'line', 32, int_translator ),
'float': ( 'NUMERIC', 'line', 32, float_translator ),
'fixed': ( 'NUMERIC', 'line', 32, None ),
101: ( 'TEXT', 'text', 0, text_translator ),
102: ( 'MEMO', 'text', 0, text_translator ),
'journal': ( 'MEMO', 'text', 0, None, ),
103: ( 'DATETIME', 'date', 20, date_translator ),
'time': ( 'DATETIME', 'line', 32,
elapsed_time_translator ),
104: ( 'SELECTION', 'select', 32, None ),
105: ( 'BINARY', 'select', 32, None ),
106: ( 'STATE', 'select', 32, state_translator ),
107: ( 'USER', 'word', 32, user_translator ),
108: ( 'PROJECTS', 'line', 80, aux('PROJECTS') ),
#109: ( 'SUMMATION', '?', ?, ? ),
#110: ( 'MULTIPLE_SELECTION', '?', ?, ? ),
#111: ( 'CONTACT', 'word', 32, ? ),
112: ( 'COMPANIES', 'line', 80, aux('COMPANIES') ),
#113: ( 'INCIDENT', 'word', 32, ? ),
114: ( 'PRODUCTS', 'line', 80, aux('PRODUCTS') ),
115: ( 'SERVICEAGREEMENTS', 'line', 80,
aux('SERVICEAGREEMENTS') ),
#116: ( 'FOLDER', 'word', 32, ? ),
#117: ( 'KEYWORDLIST', '?', ?, ? ),
#118: ( 'PRODUCTLIST', '?', ?, ? ),
#119: ( 'PROBLEM', 'word', 32, ? ),
#120: ( 'RESOLUTION', 'word', 32, ? ),
#121: ( 'MERCHANDISE', 'word', 32, ? ),
}
# 5.5. Read TeamTrack fields
#
# Read the TS_FIELDS table and turn it into a map from the field's
# database name (DBNAME field) to the field definition.
#
# We only want to consider fields from the TS_CASES (if there's a
# field in the TS_INCIDENTS table with the same DBNAME as a field in
# the TS_CASES table we don't want to use the former by mistake).
# Hence the selection on the TS_TABLEID field.
#
# If the TS_STATUS of a field is not 0, then it's "inactive",
# meaning deleted. We don't want to consider those either.
tt_fields = {}
for f in connection.query(teamtrack.table['FIELDS'],
'TS_TABLEID = %d AND TS_STATUS = 0'
% connection.case_table_id()):
tt_fields[f['DBNAME']] = f
try:
state_description = tt_fields['STATE']['DESCRIPTION']
owner_description = tt_fields['OWNER']['DESCRIPTION']
title_description = tt_fields['TITLE']['DESCRIPTION']
except KeyError:
# "Couldn't get descriptions for TeamTrack system fields STATE,
# OWNER, and TITLE."
raise error, catalog.msg(402)
# 5.6. Make values for the State field in Perforce
#
# Work out the legal values of the State field in Perforce. Note
# that "closed" must be a legal state because "p4 fix -c CHANGE
# JOBNAME" always sets the State to "closed" even if "closed" is not
# a legal value. See job000225.
legal_states = map((lambda x: x[1]), state_pairs)
if 'closed' not in legal_states:
legal_states.append('closed')
state_values = string.join(legal_states, '/')
# 5.7. Fields that always appear in the Perforce jobspec
#
# The 'p4_fields' table maps TeamTrack field name to a definition of
# the corresponding field in Perforce. The table also has entries
# for field not replicated from TeamTrack: these appear under dummy
# TeamTrack field names in parentheses.
#
# Perforce field definitions have nine elements:
#
# 1. Field number;
#
# 2. Field name;
#
# 3. Field type (word, line, select, date, text);
#
# 4. Field length;
#
# 5. Field disposition (always, required, optional, default);
#
# 6. The default value for the field, or None if there isn't one
# (field Preset);
#
# 7. Legal values for the field (if it's a "select" field) or None
# otherwise (field Values);
#
# 8. Help text for the field;
#
# 9. Translator for the field (if the field is replicated from
# TeamTrack), or None (if the field is not replicated).
#
# The five fields 101 to 105 are predefined because they are
# required by Perforce. The fields Job and Date are special: they
# are required by Perforce but are not replicated from TeamTrack.
# Note that their help text is given (the other help texts will be
# fetched from TeamTrack).
#
# We extend this table with fields from the "replicated_fields"
# configuration parameter (section 5.8). Next we use the table to
# buid the Perforce jobspec (section 5.9). Finally, we use the
# table to build the "field_map" configuration parameter which the
# replicator module uses to replicate the field (section 5.10).
p4_fields = {
'(JOB)': ( 101, 'Job', 'word', 32, 'required',
None, None,
"The job name.",
None ),
'STATE': ( 102, 'State', 'select', 32, 'required',
state_pairs[0][1], state_values,
state_description,
state_translator ),
'OWNER': ( 103, 'Owner', 'word', 32, 'required',
'$user', None,
owner_description,
user_translator ),
'(DATE)': ( 104, 'Date', 'date', 20, 'always',
'$now', None,
"The date this job was last modified.",
None ),
'TITLE': ( 105, 'Title', 'text', 0, 'required',
'$blank', None,
title_description,
text_translator ),
'(FILESPECS)': ( 191, 'P4DTI-filespecs', 'text', 0, 'optional',
None, None,
"Associated filespecs.",
None ),
'(RID)': ( 192, 'P4DTI-rid', 'word', 32, 'required',
'None', None,
"P4DTI replicator identifier. Do not edit!",
None ),
'(ISSUE)': ( 193, 'P4DTI-issue-id', 'word', 32, 'required',
'None', None,
"TeamTrack issue database identifier. Do not "
"edit!",
None ),
'(USER)': ( 194, 'P4DTI-user', 'word', 32, 'always',
'$user', None,
"Last user to edit this job. You can't edit "
"this!",
None ),
}
# 5.8. Add replicated fields to table
#
# Go through the "replicated_fields" configuration parameter. For
# each replicated field in TeamTrack, build a structure of none
# elements describing the corresponding field in Perforce (see
# section 5.7 for the definition of this structure) and add it to
# the p4_fields table.
#
# Replicated fields will start with the Perforce field number 110.
# This gives some opportunity for people using advanced
# configuration to add extra fields near the start of the Perforce
# jobspec. Since the P4DTI fields start at 191, this allows 81
# replicated fields, which should be plenty.
p4_field_id = 110
for tt_field_name in config.replicated_fields:
# Convert the field name to uppercase. This allows people to
# specify field names case-insensively in the
# "replicated_fields" configuration parameter.
tt_field_name = string.upper(tt_field_name)
# The field must be a valid TeamTrack field.
if not tt_fields.has_key(tt_field_name):
# "Field '%s' specified in 'replicated_fields' list not in
# TeamTrack FIELDS table."
raise error, catalog.msg(403, tt_field_name)
# The field must not be already in the p4_fields table.
if p4_fields.has_key(tt_field_name):
if tt_field_name in ['STATE', 'OWNER', 'TITLE']:
# "Field '%s' specified in 'replicated_fields' list is a
# system field: leave it out!"
raise error, catalog.msg(404, tt_field_name)
else:
# "Field '%s' appears twice in 'replicated_fields'."
raise error, catalog.msg(405, tt_field_name)
f = tt_fields[tt_field_name]
tt_field_type = f['FLDTYPE']
# TeamTrack numeric fields have type 100, with ATTRIBUTES set to
# 0 (integer), 1 (float), or 2 (fixed precision).
if tt_field_type == 100:
if f['ATTRIBUTES'] == 1:
tt_field_type = 'float'
elif f['ATTRIBUTES'] == 2:
tt_field_type = 'fixed'
# TeamTrack variable length text fields are not given as MEMO
# fields (type 102) but as TEXT fields (type 101) with
# ATTRIBUTES set to 0 (ordinary variable-length), 2 (journal) or
# 3 (append-only journal). Handle these special cases here.
# See job000370 for the problem with journal fields.
elif tt_field_type == 101:
if f['ATTRIBUTES'] in [2, 3]:
tt_field_type = 'journal'
elif f['ATTRIBUTES'] != 1:
tt_field_type = 102
# TeamTrack date/time fields have four types: 0 (date only), 1
# (date and time), 2 (time of day), 3 (elapsed time). The
# former two should be represented as Perforce dates and
# translated using date_translator; the latter two should be
# represented as text fields and translated using
# elapsed_time_translator. See job000182.
elif tt_field_type == 103 and f['ATTRIBUTES'] in [2,3]:
tt_field_type = 'time'
# Look up the field type in type_table to find out how to
# convert it to a Perforce field. See section 5.4.
if not type_table.has_key(tt_field_type):
# "Field '%s' has type %d: this is not supported by P4DTI."
raise error, catalog.msg(406, (tt_field_name,
tt_field_type))
tt_table_name, p4_field_type, p4_field_length, trans = \
type_table[tt_field_type]
p4_field_values = None
p4_field_preset = None
p4_field_name = keyword_translator.translate_0_to_1(f['NAME'])
# "p4 -G" uses the field "code" to indicate whether the Perforce
# command succeeded or failed. See job000003.
if p4_field_name == 'code':
# "You can't have a field called 'code' in the Perforce
# jobspec."
raise error, catalog.msg(407)
# For single-select fields, work out the values for that field
# in Perforce, and build a translator.
if tt_field_type == 104:
query = "TS_FLDID = %d" % f['ID']
if not config.use_deleted_selections:
query = query + ' AND TS_STATUS = 0'
selections = connection.query(teamtrack.table['SELECTIONS'],
query)
values = map(lambda s: keyword_translator.translate_0_to_1(
s['NAME']), selections)
values.append('(None)')
p4_field_values = string.join(values, '/')
trans = dt_teamtrack.single_select_translator(
f['DBNAME'], keyword_translator)
# For journal fields, the translator needs to know whether the
# field is append-only, and the TeamTrack database name of the
# field, so that it can enforce the append-only constraint.
elif tt_field_type == 'journal':
trans = dt_teamtrack.journal_translator(
f['ATTRIBUTES'] == 3, f['DBNAME'], user_translator)
# For binary fields, the translator needs to know the labels for
# 0 and 1 (note that the label for 0 is LABEL1 and the label for
# 1 is LABEL2). Supply defaults when the labels are empty
# strings in TeamTrack, just in case.
elif tt_field_type == 105:
tt_labels = (f['LABEL1'], f['LABEL2'])
if not tt_labels[0]:
tt_labels[0] = 'No'
if not tt_labels[1]:
tt_labels[1] = 'Yes'
p4_labels = map(keyword_translator.translate_0_to_1,
tt_labels)
p4_field_values = string.join(p4_labels, '/')
trans = dt_teamtrack.binary_translator(p4_labels)
# For fixed-point numeric fields, the translator needs to know
# the number of digits after the decimal point.
elif tt_field_type == 'fixed':
# TS_FLDOPTION_PRECISION_MASK is 0x0F00 [TeamShare
# 2001-04-30].
digits = (f['OPTIONS'] & 0x0F00) >> 8
trans = dt_teamtrack.fixed_point_translator(digits)
# Build a structure describing the Perforce field; add it to the
# table.
p4_fields[tt_field_name] = \
( p4_field_id,
p4_field_name,
p4_field_type,
p4_field_length,
'optional',
p4_field_preset,
p4_field_values,
f['DESCRIPTION'],
trans
)
p4_field_id = p4_field_id + 1
if p4_field_id >= 191:
# "Too many fields to replicate: Perforce jobs can contain
# only 99 fields."
raise error, catalog.msg(408)
# 5.9. Make jobspec description
comment = ("# A Perforce Job Specification automatically "
"produced by the\n"
"# Perforce Defect Tracking Integration\n")
jobspec = (comment, p4_fields.values())
# 5.10. Generate configuration parameters
loggers = []
log_params = {
'priority': config.log_level,
'max_length': config.log_max_message_length,
}
# The log messages should go to (up to) three places:
# 1. to standard output (if running from a command line);
if config.use_stdout_log:
loggers.append(apply(logger.file_logger, (), log_params))
# 2. to the file named by the log_file configuration parameter (if
# not None);
if config.log_file != None:
loggers.append(apply(logger.file_logger,
(open(config.log_file, "a"),),
log_params))
# 3. to the Windows event log (if use_windows_event_log is true).
if config.use_windows_event_log:
loggers.append(apply(logger.win32_event_logger,
(config.rid,),
log_params))
config.logger = logger.multi_logger(loggers)
# Set configuration parameters needed by dt_teamtrack.
config.start_date = convert_isodate_to_secs(config.start_date)
config.state_pairs = state_pairs
# Set configuration parameters needed by the replicator.
config.date_translator = date_translator
config.job_owner_field = 'Owner'
config.job_status_field = 'State'
config.job_date_field = 'Date'
config.jobspec = jobspec
config.prepare_issue_advanced = prepare_issue_advanced
config.translate_jobspec_advanced = translate_jobspec_advanced
config.text_translator = text_translator
config.user_translator = user_translator
# The field_map parameter is a list of triples (TeamTrack database
# field name, Perforce field name, translator) required by the
# replicator.
#
# This is generated from the p4_field table by filtering out fields
# that aren't replicated (these have no translator) and selecting
# only the three elements of interest.
config.field_map = \
map(lambda item: (item[0], item[1][1], item[1][8]),
filter(lambda item: item[1][8] != None, p4_fields.items()))
return config
# A. REFERENCES
#
# [GDR 2001-11-14] "Perforce Defect Tracking Integration Advanced
# Administrator's Guide"; Gareth Rees; Ravenbrook Limited; 2001-11-14;
# .
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Richard Brooksby; Ravenbrook Limited; 2000-10-16;
# .
#
# [GDR 2001-09-26] "TeamTrack journal fields"; Gareth Rees; Ravenbrook
# Limited; 2001-09-26;
# .
#
# [GDR 2000-10-16] "Perforce Defect Tracking Integration Integrator's
# Guide"; Gareth Rees; Ravenbrook Limited; 2000-10-16;
# .
#
# [ISO 8601] "Representation of dates and times"; ISO; 1988-06-15.
#
# [RB 2000-11-28a] "Case of state names" (e-mail message); Richard
# Brooksby; Ravenbrook; 2000-11-28;
# .
#
# [RB 2000-11-28b] "Distinguished state to map to 'closed'" (e-mail
# message); Richard Brooksby; Ravenbrook; 2000-11-28;
# .
#
# [TeamShare 2000-01-20] "TeamTrack Database Schema (Database Version:
# 21)"; TeamShare; 2000-01-20;
# .
#
# [TeamShare 2001-04-30] "TeamTrack Database Schema (Database Version:
# 514)"; TeamShare; 2001-04-30;
# .
#
#
# B. DOCUMENT HISTORY
#
# 2000-12-07 RB Updated "resolver" to "administrator" in some error
# messages. Fixed the field length on "P4DTI-issue-id".
#
# 2000-12-08 GDR Translate state name "ignore" to "_ignore".
#
# 2000-12-15 NB Added verbosity control.
#
# 2001-01-19 NB Validate config items. log_file may be None.
#
# 2001-02-04 GDR Added start_date parameter.
#
# 2001-02-12 GDR Convert start_date to seconds since epoch before
# passing to dt_teamtrack.
#
# 2001-02-13 GDR Allow administrator_address and smtp_server to be None.
#
# 2001-02-15 GDR Time fields map to text fields, use
# elapsed_time_translator
#
# 2001-02-16 NB Added replicate_p configuration parameter.
#
# 2001-02-19 NB Moved keyword translation to p4.py.
#
# 2001-02-22 GDR Moved keyword translation to keyword.py. Made sure
# that 'closed' is a legal job state in Perforce. Added text translator
# to replicator config.
#
# 2001-02-26 GDR Changed error name to "TeamTrack configuration error"
# for consistency with "Bugzilla configuration error" in
# configure_bugzilla.py. Refer to "TeamTrack" explicitly in messages,
# not just "defect tracker".
#
# 2001-03-02 RB Transferred copyright to Perforce under their license.
#
# 2001-03-12 GDR Using messages for errors.
#
# 2001-03-13 GDR Remove verbose parameter; added log_level. Removed
# P4DTI-action field. Made P4DTI-filespec field optional. Get
# keyword_translator from translator, not keyword.
#
# 2001-03-15 GDR Get configuration from the config module.
#
# 2001-03-17 GDR Formatted as document. Added links to design. Make
# only a single user translator to fix job000148.
#
# 2001-03-23 GDR Added job-date-field to replicator configuration.
#
# 2001-06-22 NB Moved common jobspec code into p4.py.
#
# 2001-06-22 NB Added initial comment to the jobspec description.
#
# 2001-06-29 GDR Made portable between TeamTrack 4.5 and TeamTrack 5.0
# by using case_table_id() and case_table_name().
#
# 2001-07-24 GDR Recognize journal fields as being multi-line text
# fields.
#
# 2001-08-06 GDR Specify -1 for DST argument to mktime().
#
# 2001-09-12 GDR Log to the Windows event log.
#
# 2001-10-01 GDR Use journal_translator for journal fields.
#
# 2001-10-04 GDR Support numeric and binary fields.
#
# 2001-10-25 GDR Added migrate_issue function.
#
# 2001-11-05 GDR Added prepare_issue_advanced to replicator
# configuration.
#
# 2001-11-13 GDR Log to stdout only if use_stdout_log configuration
# parameter is set.
#
# 2001-11-20 GDR Added translate_jobspec_advanced to replicator
# configuration. Put jobspec in config rather than returning it.
#
# 2001-11-21 GDR Check teamtrack_version configuration parameter.
#
# 2001-11-22 GDR Support use_deleted_selections parameters. Support
# fixed-point numeric fields.
#
# 2001-11-26 GDR Support log_max_message_length configuration parameter.
#
# 2002-01-15 GDR Use 'text' type in Perforce for all text fields; see
# job000442.
#
# 2002-03-28 NB Make lambda syntax consistent.
#
# 2002-04-05 NB job000501: handle creation of new jobs when
# LASTMODIFIEDDATE or SUBMITDATE are replicated
#
#
# C. COPYRIGHT AND LICENCE
#
# This file is copyright (c) 2001 Perforce Software, Inc. All rights
# reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
#
#
# $Id: //info.ravenbrook.com/project/p4dti/version/1.5/code/replicator/configure_teamtrack.py#3 $