# configure_teamtrack.py -- Build P4DTI configuration for replication to TeamTrack. # Gareth Rees, Ravenbrook Limited, 2000-11-27. # $Id: //info.ravenbrook.com/project/p4dti/branch/2000-12-07/document-history/code/replicator/configure_teamtrack.py#2 $ # # 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 dt_teamtrack import logger import p4 import re import replicator import socket import string import teamtrack error = "Configuration error" # type_table is a map from TeamTrack field type (the value in the FLDTYPE # column in the FIELDS table; see the TeamTrack schema for details) to a tuple # (TeamTrack table name, Perforce field type, Perforce field length, translator # class). The Perforce field length is None when it needs to be the same as # the corresponding field length in TeamTrack. The table name is the table # that auxiliary fields are looked up in. Commented out fields are ones we # don't (yet) support. When new translators are written for those field types, # we can uncomment them. type_table = { 100: ( 'NUMERIC', 'word', 32, replicator.translator ), 101: ( 'TEXT', 'line', None, replicator.translator ), 102: ( 'MEMO', 'text', 0, dt_teamtrack.text_translator ), 103: ( 'DATETIME', 'date', 20, dt_teamtrack.date_translator ), 104: ( 'SELECTION', 'select', 32, dt_teamtrack.single_select_translator ), #105: ( 'BINARY', 'word', 3, ? ), 106: ( 'STATE', 'select', 32, dt_teamtrack.state_translator ), 107: ( 'USER', 'word', 32, dt_teamtrack.user_translator ), 108: ( 'PROJECTS', 'line', 80, dt_teamtrack.auxiliary_translator ), #109: ( 'SUMMATION', '?', ?, ? ), #110: ( 'MULTIPLE_SELECTION', '?', ?, ? ), #111: ( 'CONTACT', 'word', 32, ? ), 112: ( 'COMPANIES', 'line', 80, dt_teamtrack.auxiliary_translator ), #113: ( 'INCIDENT', 'word', 32, ? ), 114: ( 'PRODUCTS', 'line', 80, dt_teamtrack.auxiliary_translator ), 115: ( 'SERVICEAGREEMENTS', 'line', 80, dt_teamtrack.auxiliary_translator ), #116: ( 'FOLDER', 'word', 32, ? ), #117: ( 'KEYWORDLIST', '?', ?, ? ), #118: ( 'PRODUCTLIST', '?', ?, ? ), #119: ( 'PROBLEM', 'word', 32, ? ), #120: ( 'RESOLUTION', 'word', 32, ? ), #121: ( 'MERCHANDISE', 'word', 32, ? ), } # make_state_pairs: (connection * string) -> (string * string) list. Make 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 decision decision [RB 2000-11-28 14:44:36 GMT]. # # The case of state names in these pairs is normalized for usability in # Perforce: see design decision [RB 2000-11-28 14:24:32 GMT]. def make_state_pairs(teamtrack_connection, closed_state): states = teamtrack_connection.query(teamtrack.table['STATES'], '') state_pairs = [] state_p4_to_tt = {} found_closed_state = 0 for s in states: tt_state = string.lower(s['NAME']) if closed_state != None and tt_state == string.lower(closed_state): p4_state = 'closed' found_closed_state = 1 else: p4_state = dt_teamtrack.translate_keyword_tt_to_p4(tt_state) # Perforce jobs can't have state "new". Perforce will change this to # "open". This is a mystery to us, but we have to cope. # Unfortunately, "new" is quite a common name for a state in TeamTrack, # so we don't disallow it, but translate it to "_new" and disallow that # instead. if p4_state == 'new': p4_state = '_new' if (state_p4_to_tt.has_key(p4_state) and state_p4_to_tt[p4_state] != tt_state): raise error, ("Two TeamTrack states '%s' and '%s' map to the " "same Perforce state '%s'." % (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: raise error, ("You specified the closed_state '%s', but there's no " "such TeamTrack state." % closed_state) return state_pairs def configuration(administrator_address = None, changelist_url = None, closed_state = None, log_file = 'p4dti.log', p4_client_executable = 'p4', p4_password = None, p4_port = None, p4_server_description = 'P4DTI Perforce server', p4_user = None, replicated_fields = None, replicator_address = None, rid = 'p4dti', sid = 'p4dti', smtp_server = None, teamtrack_password = '', teamtrack_server = None, teamtrack_user = None, ): # Check required arguments. if administrator_address == None: raise error, "You must specify an e-mail address for the administrator." if smtp_server == None: raise error, "You must specify an SMTP server." if teamtrack_server == None: raise error, "You must specify a TeamTrack server host and port." if replicated_fields == None: raise error, "You must specify some TeamTrack fields to replicate." # Open a connection to the TeamTrack server. connection = teamtrack.connect(teamtrack_user, teamtrack_password, teamtrack_server) # Make a list of (TeamTrack state, Perforce state) pairs. state_pairs = make_state_pairs(connection, closed_state) # Read the FIELDS table, turn it into a map We only want fields in the # CASES table; the FIELDS table describes fields in several tables. If # STATUS of a field is not 0, then it's "inactive", meaning deleted. tt_fields = {} for f in connection.query(teamtrack.table['FIELDS'], 'TS_TABLEID = %d AND TS_STATUS = 0' % teamtrack.table['CASES']): tt_fields[f['DBNAME']] = f # Get TeamTrack's descriptions for the system fields. try: state_description = tt_fields['STATE']['DESCRIPTION'] owner_description = tt_fields['OWNER']['DESCRIPTION'] title_description = tt_fields['TITLE']['DESCRIPTION'] title_length = tt_fields['TITLE']['LEN'] except KeyError: raise error, ("Couldn't get descriptions for TeamTrack system fields " "STATE, OWNER, and TITLE.") # p4_fields maps TeamTrack field name to the jobspec data (number, name, # type, length, dispositon, preset, values, help text, translator). The # five fields 101 to 105 are predefined because they are needed by both # TeamTrack and Perforce and the replicator. The fields Job and Date are # special: they are not replicated from TeamTrack but are required by # Perforce, so we have them here. Note that their help text is given (the # other help texts will be fetched from TeamTrack). p4_fields = { \ '(JOB)': ( 101, 'Job', 'word', 32, 'required', None, None, "The job name.", None ), 'STATE': ( 102, 'State', 'select', 32, 'required', state_pairs[0][1], string.join(map((lambda x: x[1]), state_pairs), '/'), state_description, dt_teamtrack.state_translator(state_pairs) ), 'OWNER': ( 103, 'Owner', 'word', 32, 'required', '$user', None, owner_description, dt_teamtrack.user_translator() ), '(DATE)': ( 104, 'Date', 'date', 20, 'always', '$now', None, "The date this job was last modified.", None ), 'TITLE': ( 105, 'Title', 'line', title_length, 'required', '$blank', None, title_description, replicator.translator() ), '(FILESPECS)': ( 191, 'P4DTI-filespecs', 'text', 0, 'default', None, None, "Associated filespecs. See section ? of the P4DTI " "user guide.", None ), '(ACTION)': ( 192, 'P4DTI-action', 'select', 32, 'required', 'replicate', 'keep/discard/wait/replicate', "Replicator action. See section 11 of the P4DTI " "administrator guide.", None ), '(RID)': ( 193, 'P4DTI-rid', 'word', 32, 'required', 'None', None, "P4DTI replicator identifier. Do not edit!", None ), '(ISSUE)': ( 194, 'P4DTI-issue-id', 'word', 32, 'required', 'None', None, "TeamTrack issue database identifier. Do not edit!", None ), '(USER)': ( 195, 'P4DTI-user', 'word', 32, 'always', '$user', None, "Last user to edit this job. You can't edit this!", None ), } # The other replicated fields will be sequential from this field id. p4_field_id = 110 # Go through the replicated_fields list, build structures and add them to # p4_fields. for tt_field_name in replicated_fields: # Convert the field name to uppercase. tt_field_name = string.upper(tt_field_name) # Field must exist in TeamTrack. if not tt_fields.has_key(tt_field_name): raise error, ("Field '%s' specified in 'replicated_fields' list " "not in TeamTrack FIELDS table." % tt_field_name) # Field must not be already in the p4_fields structure. if p4_fields.has_key(tt_field_name): if tt_field_name in ['STATE', 'OWNER', 'TITLE']: raise error, ("Field '%s' specified in 'replicated_fields' " "list is a system field: leave it out!" % tt_field_name) else: raise error, ("Field '%s' appears twice in " "'replicated_fields'." % tt_field_name) f = tt_fields[tt_field_name] tt_field_type = f['FLDTYPE'] # TeamTrack variable length text fields are often (always?) not given # as MEMO fields (type 102) but as TEXT fields (type 101) with # ATTRIBUTES set to 0 to indicate that it is variable-length. So # handle that special case here. if tt_field_type == 101 and f['ATTRIBUTES'] == 0: tt_field_type = 102 # Look up the field type in type_table to find out how to convert it to # a Perforce field. if not type_table.has_key(tt_field_type): raise error, ("Field '%s' has type %d: this is not supported by " "P4DTI." % (tt_field_name, tt_field_type)) tt_table_name, p4_field_type, p4_field_length, translator_class = \ type_table[tt_field_type] p4_field_values = None p4_field_preset = None p4_field_name = dt_teamtrack.translate_keyword_tt_to_p4(f['NAME']) # "p4 -G" uses the field "code" to indicate whether the Perforce # command succeeded or failed. See job000003. if p4_field_name == 'code': raise error, ("You can't have a field called 'code' in the " "Perforce jobspec.") # Fixed-length text fields get the length from TeamTrack. if p4_field_length == None: p4_field_length = f['LEN'] # Work out the arguments to pass to the translator constructor, and # apply any other special cases. translator_arguments = () if translator_class == dt_teamtrack.single_select_translator: # For single-select fields, get the list of values from TeamTrack. selections = connection.query(teamtrack.table['SELECTIONS'], "TS_FLDID = %d" % f['ID']) p4_field_values = string.join(map(lambda(s): dt_teamtrack.translate_keyword_tt_to_p4(s['NAME']), selections) + ['(None)'], '/') translator_arguments = (f['DBNAME'],) elif translator_class == dt_teamtrack.state_translator: translator_arguments = (state_pairs,) elif translator_class == dt_teamtrack.auxiliary_translator: translator_arguments = (tt_table_name,) 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'], apply(translator_class,translator_arguments), ) p4_field_id = p4_field_id + 1 if p4_field_id >= 191: raise error, ("Too many fields to replicate: Perforce jobs can " "contain only 99 fields.") # Build a jobspec, suitable for "p4 -G jobspec -i". jobspec = {} p4_fields_list = p4_fields.values() # This works in Python 1.5.2, but it doesn't follow the manual, which says # that the comparison function must return -1, 0, or 1. What about lexical # sorting? p4_fields_list.sort(lambda x,y: x[0] - y[0]) i = 0 for field in p4_fields_list: jobspec['Fields%d' % i] = "%s %s %s %s %s" % field[0:5] i = i + 1 i = 0 for field in p4_fields_list: if field[6] != None: jobspec['Values%d' % i] = "%s %s" % (field[1], field[6]) i = i + 1 i = 0 for field in p4_fields_list: if field[5] != None: jobspec['Presets%d' % i] = "%s %s" % (field[1], field[5]) i = i + 1 def comment(field): return "# %s: %s\n" % (field[1], field[7]) jobspec['Comments'] = ("# DO NOT CREATE NEW JOBS IN PERFORCE. " "USE THE DEFECT TRACKER.\n#\n" + string.join(map(comment, p4_fields.values()), "")) # The logger object that handles the logging. This logger object logs both # to the file and to stdout. logger_object = logger.multi_logger([logger.file_logger(open(log_file, "a")), logger.file_logger()]) # Build teamtrack and replicator configuration dictionaries. teamtrack_config = \ { 'changelist-url': changelist_url, 'logger': logger_object, 'p4-server-description': p4_server_description, 'password': teamtrack_password, 'server': teamtrack_server, 'state-pairs': state_pairs, 'user': teamtrack_user, } # configured_fields is a list of triples (TeamTrack database field name, # Perforce field name, translator) required by the replicator. We use the # filter to remove the fields that aren't replicated: these have no # translator. configured_fields = \ map(lambda item: (item[0], item[1][1], item[1][8]), filter(lambda item: item[1][8] != None, p4_fields.items())) p4_config = \ { 'p4-client': 'p4dti-%s' % socket.gethostname(), 'p4-client-executable': p4_client_executable, 'p4-password': p4_password, 'p4-port': p4_port, 'p4-user': p4_user, } replicator_config = \ { 'administrator-address': administrator_address, 'date-translator': dt_teamtrack.date_translator(), 'job-owner-field': 'Owner', 'job-status-field': 'State', 'logger': logger_object, 'replicated-fields': configured_fields, 'replicator-address': replicator_address, 'smtp-server': smtp_server, 'user-translator': dt_teamtrack.user_translator(), } return teamtrack_config, p4_config, replicator_config, jobspec # B. Document History # # 2000-12-07 RB Updated "resolver" to "administrator" in some error messages. # Fixed the field length on "P4DTI-issue-id".