/* Perforce Defect Tracking Integration Project
TEAMTRACK-RECORD.CPP -- PYTHON WRAPPER ROUND TSRECORD CLASS
Gareth Rees, Ravenbrook Limited, 2000-08-07
1. INTRODUCTION
This file implements a Python interface to TSRecord objects (records
from tables in the TeamTrack database). Each TSRecord object is
represented in Python by an object belonging to the teamtrack_record
type. These objects offer part of the Python mapping interface
(namely, length, subscript and ass_sub).
See [Lutz 1996, page 557] for an introduction to writing Python
mapping objects. See [GDR 2000-08-08] for the design.
The intended readership is project developers.
This document is not confidential. */
#include "teamtrack-module.h"
#include "teamtrack-record.h"
#include "TSField.h"
/* teamtrack_record_new(r, server, delete_p) returns a Python wrapper for the
TSRecord object r, or NULL if there is an error. The server argument is the
Python wrapper for the TSServer object to which the record belongs. Iff
delete_p is true, then the TSRecord object will be deleted when the Python
wrapper is deleted. */
PyObject *
teamtrack_record_new(TSRecord *r, teamtrack_server *server, int delete_p) {
/* Check that the TSRecord's server reference matches the Python wrapper's
server reference. */
if (server->s != r->serverRef) {
teamtrack_raise("Attempt to create a TeamTrack record object whose "
"server reference doesn't match the supplied Python "
"wrapper's server reference.", NULL);
}
teamtrack_record *self = PyObject_NEW(teamtrack_record,
&teamtrack_record_type);
if (self == NULL) {
return NULL;
}
self->r = r;
self->server = server;
Py_INCREF(server); /* The new record object has a reference to it.
DECREF is in teamtrack_record_dealloc. */
self->delete_p = delete_p;
return (PyObject *)self;
}
/* Utilities */
PyObject *
teamtrack_field_to_pyobject(TSField *f) {
switch (f->dataType) {
case TS_DATATYPE_BOOL:
case TS_DATATYPE_INTEGER:
return PyInt_FromLong((long)f->intValue);
case TS_DATATYPE_DOUBLE:
return PyFloat_FromDouble(f->doubleValue);
case TS_DATATYPE_STRING:
/* The character value might be NULL. */
if (f->charValue != NULL) {
return PyString_FromString(f->charValue);
} else {
return PyString_FromString("");
}
case TS_DATATYPE_RECORDLIST:
teamtrack_raise("Recordlist fields are not supported by the "
"Python interface to TeamTrack.", NULL);
case TS_DATATYPE_INTLIST:
teamtrack_raise("Intlist fields are not supported by the "
"Python interface to TeamTrack.", NULL);
case TS_DATATYPE_UNKNOWN:
default:
teamtrack_raise("Unknown field type.", NULL);
}
}
/* Instance methods */
static PyObject *
teamtrack_record_add(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
/* Attempt to add the record to the database. Pass the TSRecord object
twice, so that it gets updated to match the new record in the database
(for example, getting the correct id). */
int rc = ttr->server->s->AddRecord(ttr->r, ttr->r);
teamtrack_try(rc, ttr->server->s, NULL);
Py_INCREF(self); /* It will be on Python's stack when returned. */
return self;
}
static PyObject *
teamtrack_record_add_field(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
/* Check that the record is in the right format for the TS_FIELDS table. */
if (ttr->r->tableId != TS_TBLID_FIELDS) {
teamtrack_raise("add_field only works for TS_FIELDS records", NULL);
}
/* Which table should the field be added to? */
int table_id;
int rc = ttr->r->GetInt("TABLEID", &table_id);
teamtrack_try(rc, ttr->server->s, NULL);
/* Get the field type of the field we are trying to add. */
TSField *field_type_field =
ttr->r->fieldList.FindFieldByName("FLDTYPE", NULL);
if (field_type_field == NULL) {
teamtrack_raise("record passed to add_field has no FLDTYPE field", NULL);
}
int field_type = field_type_field->intValue;
/* Attempt to add new field to database. */
int record_id;
rc = ttr->server->s->AddField(ttr->r, table_id, field_type, record_id);
teamtrack_try(rc, ttr->server->s, NULL);
Py_INCREF(Py_None); /* It will be on Python's stack when returned. */
return Py_None;
}
static PyObject *
teamtrack_record_has_key(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
char *keystring;
if (!PyArg_ParseTuple(args, "s", &keystring)) {
return NULL;
}
TSField *f = ttr->r->fieldList.FindFieldByName(keystring, ttr->r->fieldType);
return PyInt_FromLong(f != NULL);
}
static PyObject *
teamtrack_record_keys(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
int nkeys = ttr->r->fieldList.Length();
PyObject *keys = PyList_New(nkeys);
if (keys == NULL) {
return NULL;
}
int i; /* Position in Python list. */
TSPosition *p; /* Position in TSFieldList. */
for (i = 0, p = ttr->r->fieldList.GetFirst();
i < nkeys && p;
++i, p = ttr->r->fieldList.GetNext(p)) {
TSField *f = ttr->r->fieldList.GetAt(p);
if (f == NULL) {
/* We can't just 'raise' here because keys would be leaked. */
teamtrack_set_error(ttr->r->serverRef);
break;
}
PyObject *key = PyString_FromString(f->fieldName);
if (key == NULL) {
break;
}
PyList_SetItem(keys, i, key);
/* key is now owned by keys. Don't decref it because PyList_SetItem didn't
incref it - see [van Rossum 1999-04-13, 1.10.2]. */
}
if (PyErr_Occurred()) {
Py_DECREF(keys); /* Delete keys. */
return NULL;
}
return keys;
}
#ifdef TEAMTRACK_FEATURE_SUBMIT
static PyObject *
teamtrack_record_submit(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
char *login_id;
/* These two arguments are optional. If project_id is not supplied by
the caller, it is taken from the record. The value -1 is a
sentinal value that cannot correspond to any project. If folder_id
is not supplied, it gets the value 0, which is its default value in
the TSServer::Submit prototype. */
int project_id = -1;
int folder_id = 0;
if (!PyArg_ParseTuple(args, "s|ii", &login_id, &project_id, &folder_id)) {
return NULL;
}
TSString login_id_tsstring = login_id;
/* Get the identifier of the project the record belongs to, but only if the
optional argument project_id was not supplied. */
if (project_id == -1) {
TSField *project_id_field =
ttr->r->fieldList.FindFieldByName("PROJECTID", NULL);
if (project_id_field == NULL) {
teamtrack_raise("record passed to submit has no PROJECTID field",
NULL);
}
project_id = project_id_field->intValue;
}
int id = 0; /* Identifier of the submitted record. */
int issue_id = 0; /* Issue ID of the submitted record. */
int rc = ttr->server->s->Submit(&id, &issue_id, login_id_tsstring,
ttr->r, ttr->r->tableId, project_id,
folder_id);
teamtrack_try(rc, ttr->server->s, NULL);
PyObject *py_id = PyInt_FromLong((long)id);
return py_id;
}
#endif /* defined(TEAMTRACK_FEATURE_SUBMIT) */
static PyObject *
teamtrack_record_transition(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
char *login_id;
int transition;
/* This argument is optional; if it is not supplied by the caller it will be
taken from the PROJECTID field of the record. The value -1 is a sentinal
value that cannot correspond to any project. */
int project_id = -1;
if (!PyArg_ParseTuple(args, "si|i", &login_id, &transition, &project_id)) {
return NULL;
}
TSString login_id_tsstring = login_id;
/* Get the record's identifier. */
TSField *record_id_field = ttr->r->fieldList.FindFieldByName("ID", NULL);
if (record_id_field == NULL) {
teamtrack_raise("record passed to transition has no ID field", NULL);
}
int record_id = record_id_field->intValue;
/* Get the identifier of the project the record belongs to, but only if the
optional argument project_id was not supplied. */
if (project_id == -1) {
TSField *project_id_field =
ttr->r->fieldList.FindFieldByName("PROJECTID", NULL);
if (project_id_field == NULL) {
teamtrack_raise("record passed to transition has no PROJECTID field",
NULL);
}
project_id = project_id_field->intValue;
}
/* Attempt the transition. According to the API documentation, only CASES
and INCIDENTS can be transitioned. I don't check this here because that's
a restriction that may change in future releases of the API. I leave it
up to the API to detect attempts to transition records in other tables. */
int rc = ttr->server->s->Transition(login_id_tsstring, ttr->r, project_id,
ttr->r->tableId, record_id, transition);
teamtrack_try(rc, ttr->server->s, NULL);
Py_INCREF(Py_None); /* It will be on Python's stack when returned. */
return Py_None;
}
static PyObject *
teamtrack_record_table(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
return PyInt_FromLong(ttr->r->tableId);
}
static PyObject *
teamtrack_record_update(PyObject *self, PyObject *args) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
/* Pass the TSRecord object twice, so that it gets updated to match the
update record in the database. */
int rc = ttr->server->s->UpdateRecord(ttr->r, ttr->r);
teamtrack_try(rc, ttr->server->s, NULL);
Py_INCREF(self); /* It will be on Python's stack when returned. */
return self;
}
static struct PyMethodDef teamtrack_record_methods[] = {
{ "add", teamtrack_record_add, 1 },
{ "add_field", teamtrack_record_add_field, 1 },
{ "has_key", teamtrack_record_has_key, 1 },
{ "keys", teamtrack_record_keys, 1 },
#ifdef TEAMTRACK_FEATURE_SUBMIT
{ "submit", teamtrack_record_submit, 1 },
#endif /* defined(TEAMTRACK_FEATURE_SUBMIT) */
{ "table", teamtrack_record_table, 1 },
{ "transition", teamtrack_record_transition, 1 },
{ "update", teamtrack_record_update, 1 },
{ NULL, NULL } /* End of methods. */
};
/* Dictionary operators */
/* Length operator returns -1 on error. See Python's abstract.h. */
static int
teamtrack_record_length(PyObject *self) {
teamtrack_check_type(teamtrack_record, self, -1);
teamtrack_record *ttr = (teamtrack_record *)self;
return ttr->r->fieldList.Length();
}
static PyObject *
teamtrack_record_subscript(PyObject *self, PyObject *key) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
char *keystring = PyString_AsString(key);
if (keystring == NULL) {
return NULL;
}
TSField *f = ttr->r->fieldList.FindFieldByName(keystring, ttr->r->fieldType);
if (f == NULL) {
PyErr_SetObject(PyExc_KeyError, key);
return NULL;
}
return teamtrack_field_to_pyobject(f);
}
/* It is not clear from the documentation, but assigning to a subscript must
return 0 for success and -1 for failure. See PyDict_SetItem in Python's
dictobject.c. */
static int
teamtrack_record_ass_sub(PyObject *self, PyObject *key, PyObject *value) {
teamtrack_check_type(teamtrack_record, self, -1);
teamtrack_record *ttr = (teamtrack_record *)self;
char *keystring = PyString_AsString(key);
if (keystring == NULL) {
return -1;
}
TSField *f = ttr->r->fieldList.FindFieldByName(keystring, ttr->r->fieldType);
if (f == NULL) {
PyErr_SetObject(PyExc_KeyError, key);
return -1;
}
if (value == NULL) {
teamtrack_raise("Deletion of fields is not supported "
"by TeamTrack records.", -1);
}
switch (f->dataType) {
case TS_DATATYPE_BOOL:
case TS_DATATYPE_INTEGER: {
long v = PyInt_AsLong(value);
if (PyErr_Occurred()) {
teamtrack_raise("Attempt to assign non-integer to integer field.", -1);
}
if (v < INT_MIN || v > INT_MAX) {
PyErr_SetString(PyExc_OverflowError,
"Field value outside integer range.");
return -1;
}
int rc = ttr->r->SetInt(keystring, (int)v);
teamtrack_try(rc, ttr->r->serverRef, -1);
return 0;
}
case TS_DATATYPE_DOUBLE: {
double v = PyFloat_AsDouble(value);
if (PyErr_Occurred()) {
teamtrack_raise("Attempt to assign non-float to double field.", -1);
}
int rc = ttr->r->SetDouble(keystring, v);
teamtrack_try(rc, ttr->r->serverRef, -1);
return 0;
}
case TS_DATATYPE_STRING: {
char *v = PyString_AsString(value);
if (v == NULL) {
teamtrack_raise("Attempt to assign non-string to string field.", -1);
}
int rc = ttr->r->SetString(keystring, v);
teamtrack_try(rc, ttr->r->serverRef, -1);
return 0;
}
case TS_DATATYPE_RECORDLIST:
teamtrack_raise("Recordlist fields are not supported by the "
"Python interface to TeamTrack.", -1);
case TS_DATATYPE_INTLIST:
teamtrack_raise("Intlist fields are not supported by the "
"Python interface to TeamTrack.", -1);
case TS_DATATYPE_UNKNOWN:
default:
teamtrack_raise("Unknown field type.", -1);
}
}
static PyMappingMethods teamtrack_record_mapping_methods = {
teamtrack_record_length,
teamtrack_record_subscript,
teamtrack_record_ass_sub,
};
/* Basic Python operators */
static void
teamtrack_record_dealloc(PyObject *self) {
if (!teamtrack_record_p(self)) {
Py_FatalError("teamtrack_record_deallocate called on an object that's "
"not a teamtrack_record.");
}
teamtrack_record *ttr = (teamtrack_record *)self;
if (ttr->delete_p) {
delete ttr->r;
}
Py_DECREF(ttr->server); /* INCREF is in teamtrack_record_new. */
PyMem_DEL(self);
}
static PyObject *
teamtrack_record_repr(PyObject *self) {
teamtrack_check_type(teamtrack_record, self, NULL);
teamtrack_record *ttr = (teamtrack_record *)self;
TSString s = ttr->r->StringDump(1, " ");
return PyString_FromString(s.GetBuffer());
}
static PyObject *
teamtrack_record_getattr(PyObject *self, char *name) {
teamtrack_check_type(teamtrack_record, self, NULL);
return Py_FindMethod(teamtrack_record_methods, self, name);
}
/* Type object for the record class */
PyTypeObject teamtrack_record_type = {
/* Type header */
PyObject_HEAD_INIT(&PyType_Type)
0, /* ob_size */
"record", /* tp_name */
sizeof(teamtrack_record), /* tp_basicsize */
0, /* tp_itemsize */
/* Basic methods */
teamtrack_record_dealloc,
NULL, /* print */
teamtrack_record_getattr,
NULL, /* setattr */
NULL, /* compare */
teamtrack_record_repr,
/* Type categories */
NULL, /* number operators */
NULL, /* sequence operators */
&teamtrack_record_mapping_methods, /* mapping operators */
/* Other methods are NULL: see Python's object.h for details */
};
/* A. REFERENCES
[GDR 2000-08-08] "Python interface to TeamTrack: design"; Gareth
Rees; Ravenbrook Limited; 2000-08-08;
.
[Lutz 1996] "Programming Python"; Mark Lutz; O'Reilly; 1996-10; ISBN
1-56592-197-6.
B. DOCUMENT HISTORY
2000-08-07 GDR Created.
2000-08-08 GDR Fixed reference counting in ttrack_record_keys (perhaps).
2000-08-29 GDR Changed "tTrack" to "TeamTrack" throughout.
2000-08-31 GDR The Python wrapper round TSRecord now stores the Python
wrapper around the TSServer object that the record belongs to. This is so
that the record object can provide add, update, submit and transition
methods. Added add, table and update methods to the record class.
2000-09-07 GDR Corrected reference counts of return values so that the
application doesn't free obects when it shouldn't.
2000-09-07 GDR Moved add_field() method implementation from server class to
record class. Changed signature so you don't have to pass redundant
information. Wrote stubs for submit() and transition() methods in the
record class.
2000-09-08 GDR AddField's second parameter needs to be TS_TBLID_FIELDS, not
the table to which the field is being added (see Larry Fish's e-mail
2000-09-08 14:52:22 GMT).
2000-09-15 GDR Implemented and documented record transition method in Python
interface to TeamTrack.
2000-09-15 GDR teamtrack_record_subscript now works correctly in the case
when a string field is uninitialized (and hence the charValue is NULL).
2000-09-15 GDR Implemented submit method.
2000-09-18 GDR Changed implementation of submit and transition methods in
response to Kelly Shaw's e-mail of 2000-09-15 17:04:49 GMT. The submit
method takes three optional parameters: project_id, folder_id, type. The
project_id parameter is deduced from the PROJECTID field of the record if
not supplied. The others default to 0, as they do in the TSServer::Submit
method. The transition method takes one optional parameter: project_id
(apparently this can be used to change the project a case belongs to, and
this may be necessary in the integration). The project_id parameter is
deduced from the PROJECTID field of the record if not supplied.
2002-01-09 GDR Use the version of TSServer::Submit that returns the
record id of the new record (not the issue id) -- but since this was
introduced in TeamTrack 5.0 we can only define it is TS_APIVERSION is
3 or more.
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.4/code/python-teamtrack-interface/teamtrack-record.cpp#2 $ */