# 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-01/no-conflicts/code/replicator/configure_teamtrack.py#3 $
#
# 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 field type 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.  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: ( 'PROJECT', 'word', 32, ? ),
    #109: ( 'SUMMATION', '?', ?, ? ),
    #110: ( 'MULTIPLE_SELECTION', '?', ?, ? ),
    #111: ( 'CONTACT', 'word', 32, ? ),
    #112: ( 'COMPANY', 'word', 32, ? ),
    #113: ( 'INCIDENT', 'word', 32, ? ),
    #114: ( 'PRODUCT', 'word', 32, ? ),
    #115: ( 'SERVICEAGREEMENT', '?', ?, ? ),
    #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 resolver."
    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']
    except:
        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', 80, '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', 0, '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))
        _, 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.")
        translator_arguments = ()

        # There are a number of special cases to consider.
        if p4_field_type == 'line':
            # It's a fixed-length text field, so get the length from TeamTrack.
            p4_field_length = f['LEN']
        elif 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,)

        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
