| 1 |
# -*- coding: utf-8 -*- |
|---|
| 2 |
# dabo/db/dCursorMixin |
|---|
| 3 |
|
|---|
| 4 |
import types |
|---|
| 5 |
import datetime |
|---|
| 6 |
import inspect |
|---|
| 7 |
import random |
|---|
| 8 |
import sys |
|---|
| 9 |
import re |
|---|
| 10 |
import array |
|---|
| 11 |
# Make sure that the user's installation supports Decimal. |
|---|
| 12 |
_USE_DECIMAL = True |
|---|
| 13 |
try: |
|---|
| 14 |
from decimal import Decimal |
|---|
| 15 |
except ImportError: |
|---|
| 16 |
_USE_DECIMAL = False |
|---|
| 17 |
|
|---|
| 18 |
import dabo |
|---|
| 19 |
import dabo.dConstants as kons |
|---|
| 20 |
from dabo.dLocalize import _ |
|---|
| 21 |
import dabo.dException as dException |
|---|
| 22 |
from dabo.dObject import dObject |
|---|
| 23 |
from dNoEscQuoteStr import dNoEscQuoteStr |
|---|
| 24 |
from dabo.db import dTable |
|---|
| 25 |
from dabo.db.dDataSet import dDataSet |
|---|
| 26 |
from dabo.lib import dates |
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
class dCursorMixin(dObject): |
|---|
| 30 |
"""Dabo's cursor class, representing the lowest tier.""" |
|---|
| 31 |
_call_initProperties = False |
|---|
| 32 |
|
|---|
| 33 |
|
|---|
| 34 |
def debug(self): |
|---|
| 35 |
return self._bizobj.DataSource == "FotoInf" |
|---|
| 36 |
|
|---|
| 37 |
|
|---|
| 38 |
|
|---|
| 39 |
def __init__(self, sql="", *args, **kwargs): |
|---|
| 40 |
self._convertStrToUnicode = True |
|---|
| 41 |
self._initProperties() |
|---|
| 42 |
if sql and isinstance(sql, basestring) and len(sql) > 0: |
|---|
| 43 |
self.UserSQL = sql |
|---|
| 44 |
|
|---|
| 45 |
#self.super() |
|---|
| 46 |
#super(dCursorMixin, self).__init__() |
|---|
| 47 |
## pkm: Neither of the above are correct. We need to explicitly |
|---|
| 48 |
## call dObject's __init__, otherwise the cursor object with |
|---|
| 49 |
## which we are mixed-in will take the __init__. |
|---|
| 50 |
dObject.__init__(self, *args, **kwargs) |
|---|
| 51 |
|
|---|
| 52 |
# Just in case this is used outside of the context of a bizobj |
|---|
| 53 |
if not hasattr(self, "superCursor") or self.superCursor is None: |
|---|
| 54 |
myBases = self.__class__.__bases__ |
|---|
| 55 |
for base in myBases: |
|---|
| 56 |
# Find the first base class that doesn't have the 'autoPopulatePK' |
|---|
| 57 |
# attribute. Designate that class as the superCursor class. |
|---|
| 58 |
if hasattr(base, "fetchall"): |
|---|
| 59 |
self.superCursor = base |
|---|
| 60 |
break |
|---|
| 61 |
|
|---|
| 62 |
|
|---|
| 63 |
def _initProperties(self): |
|---|
| 64 |
# Holds the dict used for adding new blank records |
|---|
| 65 |
self._blank = {} |
|---|
| 66 |
# Writable version of the dbapi 'description' attribute |
|---|
| 67 |
self.descriptionClean = None |
|---|
| 68 |
# Last executed sql params |
|---|
| 69 |
self.lastParams = None |
|---|
| 70 |
# Column on which the result set is sorted |
|---|
| 71 |
self.sortColumn = "" |
|---|
| 72 |
# Order of the sorting. Should be either ASC, DESC or empty for no sort |
|---|
| 73 |
self.sortOrder = "" |
|---|
| 74 |
# Is the sort case-sensitive? |
|---|
| 75 |
self.sortCase = True |
|---|
| 76 |
# Holds the keys in the original, unsorted order for unsorting the dataset |
|---|
| 77 |
self.__unsortedRows = [] |
|---|
| 78 |
# Holds the name of fields to be skipped when updating the backend, such |
|---|
| 79 |
# as calculated or derived fields, or fields that are otherwise not to be updated. |
|---|
| 80 |
self.__nonUpdateFields = None |
|---|
| 81 |
# User-editable list of non-updated fields |
|---|
| 82 |
self.nonUpdateFields = [] |
|---|
| 83 |
# Flag that is set when the user explicitly sets the Key Field |
|---|
| 84 |
self._keyFieldSet = False |
|---|
| 85 |
# Cursor that manages this cursor's SQL. Default to self; |
|---|
| 86 |
# in some cases, such as a single bizobj managing several cursors, |
|---|
| 87 |
# it will be a separate object. |
|---|
| 88 |
self.sqlManager = self |
|---|
| 89 |
# Attribute that holds the data of the cursor |
|---|
| 90 |
self._records = dDataSet() |
|---|
| 91 |
# Attribute that holds the current row number |
|---|
| 92 |
self.__rownumber = -1 |
|---|
| 93 |
# Data structure info |
|---|
| 94 |
self._dataStructure = None |
|---|
| 95 |
self._table = "" |
|---|
| 96 |
self._keyField = "" |
|---|
| 97 |
self._userSQL = None |
|---|
| 98 |
self._virtualFields = {} |
|---|
| 99 |
|
|---|
| 100 |
self._autoPopulatePK = True |
|---|
| 101 |
self._autoQuoteNames = True |
|---|
| 102 |
|
|---|
| 103 |
self.__tmpPK = -1 # temp PK value for new records. |
|---|
| 104 |
# Holds the data types for each field |
|---|
| 105 |
self._types = {} |
|---|
| 106 |
|
|---|
| 107 |
# Holds reference to auxiliary cursor that handles queries that |
|---|
| 108 |
# are not supposed to affect the record set. |
|---|
| 109 |
self.__auxCursor = None |
|---|
| 110 |
# Marks the cursor as an auxiliary cursor |
|---|
| 111 |
self._isAuxiliary = False |
|---|
| 112 |
|
|---|
| 113 |
# Reference to the object with backend-specific behaviors |
|---|
| 114 |
self.__backend = None |
|---|
| 115 |
|
|---|
| 116 |
# Reference to the bizobj that 'owns' this cursor, if any, |
|---|
| 117 |
self._bizobj = None |
|---|
| 118 |
|
|---|
| 119 |
# set properties for the SQL Builder functions |
|---|
| 120 |
self.clearSQL() |
|---|
| 121 |
self.hasSqlBuilder = True |
|---|
| 122 |
|
|---|
| 123 |
# props for building the auxiliary cursor |
|---|
| 124 |
self._cursorFactoryFunc = None |
|---|
| 125 |
self._cursorFactoryClass = None |
|---|
| 126 |
|
|---|
| 127 |
# mementos and new records, keyed on record object ids: |
|---|
| 128 |
self._mementos = {} |
|---|
| 129 |
self._newRecords = {} |
|---|
| 130 |
|
|---|
| 131 |
# Flag preference cursors so that they don't fill up the logs |
|---|
| 132 |
self._isPrefCursor = False |
|---|
| 133 |
|
|---|
| 134 |
self.initProperties() |
|---|
| 135 |
|
|---|
| 136 |
|
|---|
| 137 |
def setCursorFactory(self, func, cls): |
|---|
| 138 |
self._cursorFactoryFunc = func |
|---|
| 139 |
self._cursorFactoryClass = cls |
|---|
| 140 |
|
|---|
| 141 |
|
|---|
| 142 |
def clearSQL(self): |
|---|
| 143 |
self._fieldClause = "" |
|---|
| 144 |
self._fromClause = "" |
|---|
| 145 |
self._joinClause = "" |
|---|
| 146 |
self._whereClause = "" |
|---|
| 147 |
self._childFilterClause = "" |
|---|
| 148 |
self._groupByClause = "" |
|---|
| 149 |
self._orderByClause = "" |
|---|
| 150 |
self._limitClause = "" |
|---|
| 151 |
self._defaultLimit = 1000 |
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 |
def getSortColumn(self): |
|---|
| 155 |
return self.sortColumn |
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 |
def getSortOrder(self): |
|---|
| 159 |
return self.sortOrder |
|---|
| 160 |
|
|---|
| 161 |
|
|---|
| 162 |
def getSortCase(self): |
|---|
| 163 |
return self.sortCase |
|---|
| 164 |
|
|---|
| 165 |
|
|---|
| 166 |
def pkExpression(self, rec=None): |
|---|
| 167 |
"""Returns the PK expression for the passed record.""" |
|---|
| 168 |
if rec is None: |
|---|
| 169 |
rec = self._records[self.RowNumber] |
|---|
| 170 |
if isinstance(self.KeyField, tuple): |
|---|
| 171 |
pk = tuple([rec[kk] for kk in self.KeyField]) |
|---|
| 172 |
else: |
|---|
| 173 |
pk = rec[self.KeyField] |
|---|
| 174 |
return pk |
|---|
| 175 |
|
|---|
| 176 |
|
|---|
| 177 |
def _correctFieldType(self, field_val, field_name, _fromRequery=False): |
|---|
| 178 |
"""Correct the type of the passed field_val, based on self.DataStructure. |
|---|
| 179 |
|
|---|
| 180 |
This is called by self.execute(), and contains code to convert all strings |
|---|
| 181 |
to unicode, as well as to correct any datatypes that don't match what |
|---|
| 182 |
self.DataStructure reports. The latter can happen with SQLite, for example, |
|---|
| 183 |
which only knows about a quite limited number of types. |
|---|
| 184 |
""" |
|---|
| 185 |
ret = field_val |
|---|
| 186 |
showError = False |
|---|
| 187 |
if _fromRequery: |
|---|
| 188 |
pythonType = self._types.get(field_name, type(field_val)) |
|---|
| 189 |
daboType = dabo.db.getDaboType(pythonType) |
|---|
| 190 |
|
|---|
| 191 |
if pythonType in (type(None), None) or isinstance(field_val, pythonType): |
|---|
| 192 |
# No conversion needed. |
|---|
| 193 |
return ret |
|---|
| 194 |
|
|---|
| 195 |
if pythonType in (unicode,): |
|---|
| 196 |
# Unicode conversion happens below. |
|---|
| 197 |
pass |
|---|
| 198 |
elif field_val is None: |
|---|
| 199 |
# Fields of any type can be None (NULL). |
|---|
| 200 |
return field_val |
|---|
| 201 |
elif pythonType in (datetime.datetime, ) and isinstance(field_val, basestring): |
|---|
| 202 |
ret = dates.getDateTimeFromString(field_val) |
|---|
| 203 |
if ret is None: |
|---|
| 204 |
ret = field_val |
|---|
| 205 |
else: |
|---|
| 206 |
return ret |
|---|
| 207 |
elif pythonType in (datetime.date,) and isinstance(field_val, basestring): |
|---|
| 208 |
ret = dates.getDateFromString(field_val) |
|---|
| 209 |
if ret is None: |
|---|
| 210 |
ret = field_val |
|---|
| 211 |
else: |
|---|
| 212 |
return ret |
|---|
| 213 |
elif _USE_DECIMAL and pythonType in (Decimal,): |
|---|
| 214 |
ds = self.DataStructure |
|---|
| 215 |
ret = None |
|---|
| 216 |
_field_val = field_val |
|---|
| 217 |
if type(field_val) in (float,): |
|---|
| 218 |
# Can't convert to decimal directly from float |
|---|
| 219 |
_field_val = str(_field_val) |
|---|
| 220 |
# Need to convert to the correct scale: |
|---|
| 221 |
scale = None |
|---|
| 222 |
for s in ds: |
|---|
| 223 |
if s[0] == field_name: |
|---|
| 224 |
if len(s) > 5: |
|---|
| 225 |
scale = s[5] |
|---|
| 226 |
if scale is None: |
|---|
| 227 |
scale = 2 |
|---|
| 228 |
return Decimal(_field_val).quantize(Decimal("0.%s" % (scale * "0",))) |
|---|
| 229 |
else: |
|---|
| 230 |
try: |
|---|
| 231 |
return pythonType(field_val) |
|---|
| 232 |
except Exception, e: |
|---|
| 233 |
dabo.infoLog.write(_("_correctFieldType() failed for field: '%s'; value: '%s'; type: '%s'") % |
|---|
| 234 |
(field_name, field_val, type(field_val))) |
|---|
| 235 |
|
|---|
| 236 |
# Do the unicode conversion last: |
|---|
| 237 |
if isinstance(field_val, str) and self._convertStrToUnicode: |
|---|
| 238 |
try: |
|---|
| 239 |
return unicode(field_val, self.Encoding) |
|---|
| 240 |
except UnicodeDecodeError, e: |
|---|
| 241 |
# Try some common encodings: |
|---|
| 242 |
ok = False |
|---|
| 243 |
for enc in ("utf-8", "latin-1", "iso-8859-1"): |
|---|
| 244 |
if enc != self.Encoding: |
|---|
| 245 |
try: |
|---|
| 246 |
ret = unicode(field_val, enc) |
|---|
| 247 |
ok = True |
|---|
| 248 |
except UnicodeDecodeError: |
|---|
| 249 |
continue |
|---|
| 250 |
if ok: |
|---|
| 251 |
# change self.Encoding and log the message |
|---|
| 252 |
self.Encoding = enc |
|---|
| 253 |
dabo.errorLog.write(_("Field %(fname)s: Incorrect unicode encoding set; using '%(enc)s' instead") |
|---|
| 254 |
% {'fname':field_name, 'enc':enc} ) |
|---|
| 255 |
return ret |
|---|
| 256 |
break |
|---|
| 257 |
else: |
|---|
| 258 |
raise UnicodeDecodeError, e |
|---|
| 259 |
# elif isinstance(field_val, array.array): |
|---|
| 260 |
# # Usually blob data |
|---|
| 261 |
# ret = field_val.tostring() |
|---|
| 262 |
|
|---|
| 263 |
dabo.errorLog.write(_("%s couldn't be converted to %s (field %s)") |
|---|
| 264 |
% (repr(field_val), pythonType, field_name)) |
|---|
| 265 |
return ret |
|---|
| 266 |
|
|---|
| 267 |
|
|---|
| 268 |
def execute(self, sql, params=(), _fromRequery=False, errorClass=None): |
|---|
| 269 |
""" Execute the sql, and populate the DataSet if it is a select statement.""" |
|---|
| 270 |
# The idea here is to let the super class do the actual work in |
|---|
| 271 |
# retrieving the data. However, many cursor classes can only return |
|---|
| 272 |
# row information as a list, not as a dictionary. This method will |
|---|
| 273 |
# detect that, and convert the results to a dictionary. |
|---|
| 274 |
if not isinstance(sql, unicode): |
|---|
| 275 |
sql = unicode(sql, self.Encoding) |
|---|
| 276 |
# Some backends, notably Firebird, require that fields be specially marked. |
|---|
| 277 |
sql = self.processFields(sql) |
|---|
| 278 |
|
|---|
| 279 |
try: |
|---|
| 280 |
if params is None or len(params) == 0: |
|---|
| 281 |
res = self.superCursor.execute(self, sql) |
|---|
| 282 |
if not self.IsPrefCursor: |
|---|
| 283 |
dabo.dbActivityLog.write("SQL: %s" % sql.replace("\n", " ")) |
|---|
| 284 |
else: |
|---|
| 285 |
res = self.superCursor.execute(self, sql, params) |
|---|
| 286 |
if not self.IsPrefCursor: |
|---|
| 287 |
dabo.dbActivityLog.write("SQL: %s, PARAMS: %s" % (sql.replace("\n", " "), ", ".join(params))) |
|---|
| 288 |
except Exception, e: |
|---|
| 289 |
# There can be cases where errors are expected. In those cases, the |
|---|
| 290 |
# calling routine will pass the class of the expected error, and will |
|---|
| 291 |
# handle it appropriately. |
|---|
| 292 |
if errorClass is not None and isinstance(e, errorClass): |
|---|
| 293 |
raise errorClass, e |
|---|
| 294 |
dabo.dbActivityLog.write("FAILED SQL: %s, PARAMS: %s" % (sql.replace("\n", " "), ", ".join(params))) |
|---|
| 295 |
# If this is due to a broken connection, let the user know. |
|---|
| 296 |
# Different backends have different messages, but they |
|---|
| 297 |
# should all contain the string 'connect' in them. |
|---|
| 298 |
if "connect" in str(e).lower(): |
|---|
| 299 |
raise dException.ConnectionLostException, e |
|---|
| 300 |
if "access" in str(e).lower(): |
|---|
| 301 |
raise dException.DBNoAccessException(e) |
|---|
| 302 |
else: |
|---|
| 303 |
raise dException.DBQueryException(e, sql) |
|---|
| 304 |
|
|---|
| 305 |
# Some backend programs do odd things to the description |
|---|
| 306 |
# This allows each backend to handle these quirks individually. |
|---|
| 307 |
self.BackendObject.massageDescription(self) |
|---|
| 308 |
|
|---|
| 309 |
if _fromRequery: |
|---|
| 310 |
self._storeFieldTypes() |
|---|
| 311 |
|
|---|
| 312 |
if sql.strip().split()[0].lower() not in ("select", "pragma"): |
|---|
| 313 |
# No need to massage the data for DML commands |
|---|
| 314 |
self._records = dDataSet(tuple()) |
|---|
| 315 |
return res |
|---|
| 316 |
|
|---|
| 317 |
try: |
|---|
| 318 |
_records = self.fetchall() |
|---|
| 319 |
except Exception, e: |
|---|
| 320 |
_records = tuple() |
|---|
| 321 |
dabo.errorLog.write("Error fetching records: %s" % e) |
|---|
| 322 |
|
|---|
| 323 |
if _records and not self.BackendObject._alreadyCorrectedFieldTypes: |
|---|
| 324 |
if isinstance(_records[0], (tuple, list)): |
|---|
| 325 |
# Need to convert each row to a Dict, since the backend didn't do it. |
|---|
| 326 |
tmpRows = [] |
|---|
| 327 |
fldNames = [f[0] for f in self.FieldDescription] |
|---|
| 328 |
for row in _records: |
|---|
| 329 |
dic = {} |
|---|
| 330 |
for idx, fldName in enumerate(fldNames): |
|---|
| 331 |
dic[fldName] = self._correctFieldType(field_val=row[idx], |
|---|
| 332 |
field_name=fldName, _fromRequery=_fromRequery) |
|---|
| 333 |
tmpRows.append(dic) |
|---|
| 334 |
_records = tmpRows |
|---|
| 335 |
else: |
|---|
| 336 |
# Already a DictCursor, but we still need to correct the field types. |
|---|
| 337 |
for row in _records: |
|---|
| 338 |
for fld, val in row.items(): |
|---|
| 339 |
row[fld] = self._correctFieldType(field_val=val, field_name=fld, |
|---|
| 340 |
_fromRequery=_fromRequery) |
|---|
| 341 |
|
|---|
| 342 |
self._records = dDataSet(_records) |
|---|
| 343 |
|
|---|
| 344 |
if self.RowCount > 0: |
|---|
| 345 |
self.RowNumber = max(0, self.RowNumber) |
|---|
| 346 |
maxrow = max(0, (self.RowCount-1) ) |
|---|
| 347 |
self.RowNumber = min(self.RowNumber, maxrow) |
|---|
| 348 |
|
|---|
| 349 |
return res |
|---|
| 350 |
|
|---|
| 351 |
|
|---|
| 352 |
def executeSafe(self, sql): |
|---|
| 353 |
"""Execute the passed SQL using an auxiliary cursor. |
|---|
| 354 |
This is considered 'safe', because it won't harm the contents |
|---|
| 355 |
of the main cursor. |
|---|
| 356 |
""" |
|---|
| 357 |
ac = self.AuxCursor |
|---|
| 358 |
self._syncAuxProperties() |
|---|
| 359 |
return ac.execute(sql) |
|---|
| 360 |
|
|---|
| 361 |
|
|---|
| 362 |
def _syncAuxProperties(self): |
|---|
| 363 |
"""Make sure that the auxiliary cursor has the same property |
|---|
| 364 |
settings as the main cursor. |
|---|
| 365 |
""" |
|---|
| 366 |
if self._isAuxiliary: |
|---|
| 367 |
# Redundant! |
|---|
| 368 |
return |
|---|
| 369 |
ac = self.AuxCursor |
|---|
| 370 |
ac.AutoPopulatePK = self.AutoPopulatePK |
|---|
| 371 |
ac.AutoQuoteNames = self.AutoQuoteNames |
|---|
| 372 |
ac.DataStructure = self.DataStructure |
|---|
| 373 |
ac.IsPrefCursor = self.IsPrefCursor |
|---|
| 374 |
ac.KeyField = self.KeyField |
|---|
| 375 |
ac.Table = self.Table |
|---|
| 376 |
|
|---|
| 377 |
|
|---|
| 378 |
def requery(self, params=None): |
|---|
| 379 |
self._lastSQL = self.CurrentSQL |
|---|
| 380 |
self.lastParams = params |
|---|
| 381 |
self._savedStructureDescription = [] |
|---|
| 382 |
|
|---|
| 383 |
if self.debug(): |
|---|
| 384 |
print "CURSOR REQUERY - USERSQL: ", self.UserSQL, self |
|---|
| 385 |
|
|---|
| 386 |
self.execute(self.CurrentSQL, params, _fromRequery=True) |
|---|
| 387 |
|
|---|
| 388 |
# clear mementos and new record flags: |
|---|
| 389 |
self._mementos = {} |
|---|
| 390 |
self._newRecords = {} |
|---|
| 391 |
|
|---|
| 392 |
# Check for any derived fields that should not be included in |
|---|
| 393 |
# any updates. |
|---|
| 394 |
self.__setNonUpdateFields() |
|---|
| 395 |
|
|---|
| 396 |
# Clear the unsorted list, and then apply the current sort |
|---|
| 397 |
self.__unsortedRows = [] |
|---|
| 398 |
if self.sortColumn: |
|---|
| 399 |
try: |
|---|
| 400 |
self.sort(self.sortColumn, self.sortOrder) |
|---|
| 401 |
except dException.NoRecordsException, e: |
|---|
| 402 |
# No big deal |
|---|
| 403 |
pass |
|---|
| 404 |
return True |
|---|
| 405 |
|
|---|
| 406 |
|
|---|
| 407 |
def _storeFieldTypes(self, target=None): |
|---|
| 408 |
"""Stores the data type for each column in the result set.""" |
|---|
| 409 |
if target is None: |
|---|
| 410 |
target = self |
|---|
| 411 |
target._types = {} |
|---|
| 412 |
for field in self.DataStructure: |
|---|
| 413 |
field_alias, field_type = field[0], field[1] |
|---|
| 414 |
target._types[field_alias] = dabo.db.getPythonType(field_type) |
|---|
| 415 |
|
|---|
| 416 |
|
|---|
| 417 |
def sort(self, col, dir=None, caseSensitive=True): |
|---|
| 418 |
""" Sort the result set on the specified column in the specified order. |
|---|
| 419 |
|
|---|
| 420 |
If the sort direction is not specified, sort() cycles among Ascending, |
|---|
| 421 |
Descending and no sort order. |
|---|
| 422 |
""" |
|---|
| 423 |
currCol = self.sortColumn |
|---|
| 424 |
currOrd = self.sortOrder |
|---|
| 425 |
currCase = self.sortCase |
|---|
| 426 |
|
|---|
| 427 |
# Check to make sure that we have data |
|---|
| 428 |
if self.RowCount < 1: |
|---|
| 429 |
raise dException.NoRecordsException, _("No rows to sort.") |
|---|
| 430 |
|
|---|
| 431 |
# Make sure that the specified column is a column in the result set |
|---|
| 432 |
if not self._records[0].has_key(col) and not self.VirtualFields.has_key(col): |
|---|
| 433 |
raise dException.dException, _("Invalid column specified for sort: ") + col |
|---|
| 434 |
|
|---|
| 435 |
newCol = col |
|---|
| 436 |
if col == currCol: |
|---|
| 437 |
# Not changing the column; most likely they are flipping |
|---|
| 438 |
# the sort order. |
|---|
| 439 |
if (dir is None) or not dir: |
|---|
| 440 |
# They didn't specify the sort. Cycle through the sort orders |
|---|
| 441 |
if currOrd == "ASC": |
|---|
| 442 |
newOrd = "DESC" |
|---|
| 443 |
elif currOrd == "DESC": |
|---|
| 444 |
newOrd = "" |
|---|
| 445 |
else: |
|---|
| 446 |
newOrd = "ASC" |
|---|
| 447 |
else: |
|---|
| 448 |
if dir.upper() in ("ASC", "DESC", ""): |
|---|
| 449 |
newOrd = dir.upper() |
|---|
| 450 |
else: |
|---|
| 451 |
raise dException.dException, _("Invalid Sort direction specified: ") + dir |
|---|
| 452 |
|
|---|
| 453 |
else: |
|---|
| 454 |
# Different column specified. |
|---|
| 455 |
if (dir is None) or not dir: |
|---|
| 456 |
# Start in ASC order |
|---|
| 457 |
newOrd = "ASC" |
|---|
| 458 |
else: |
|---|
| 459 |
if dir.upper() in ("ASC", "DESC", ""): |
|---|
| 460 |
newOrd = dir.upper() |
|---|
| 461 |
else: |
|---|
| 462 |
raise dException.dException, _("Invalid Sort direction specified: ") + dir |
|---|
| 463 |
|
|---|
| 464 |
self.__sortRows(newCol, newOrd, caseSensitive) |
|---|
| 465 |
# Save the current sort values |
|---|
| 466 |
self.sortColumn = newCol |
|---|
| 467 |
self.sortOrder = newOrd |
|---|
| 468 |
self.sortCase = caseSensitive |
|---|
| 469 |
|
|---|
| 470 |
|
|---|
| 471 |
def __sortRows(self, col, ord, caseSensitive): |
|---|
| 472 |
""" Sort the rows of the cursor. |
|---|
| 473 |
|
|---|
| 474 |
At this point, we know we have a valid column and order. We need to |
|---|
| 475 |
preserve the unsorted order if we haven't done that yet; then we sort |
|---|
| 476 |
the data according to the request. |
|---|
| 477 |
""" |
|---|
| 478 |
kf = self.KeyField |
|---|
| 479 |
if not kf: |
|---|
| 480 |
return |
|---|
| 481 |
|
|---|
| 482 |
if not self.__unsortedRows: |
|---|
| 483 |
# Record the PK values |
|---|
| 484 |
for row in self._records: |
|---|
| 485 |
if self._compoundKey: |
|---|
| 486 |
key = tuple([row[k] for k in kf]) |
|---|
| 487 |
self.__unsortedRows.append(key) |
|---|
| 488 |
else: |
|---|
| 489 |
self.__unsortedRows.append(row[self.KeyField]) |
|---|
| 490 |
|
|---|
| 491 |
# First, preserve the PK of the current row so that we can reset |
|---|
| 492 |
# the RowNumber property to point to the same row in the new order. |
|---|
| 493 |
try: |
|---|
| 494 |
if self._compoundKey: |
|---|
| 495 |
currRow = self._records[self.RowNumber] |
|---|
| 496 |
currRowKey = tuple([currRow[k] for k in kf]) |
|---|
| 497 |
else: |
|---|
| 498 |
currRowKey = self._records[self.RowNumber][self.KeyField] |
|---|
| 499 |
except IndexError: |
|---|
| 500 |
# Row no longer exists, such as after a Requery that returns |
|---|
| 501 |
# fewer rows. |
|---|
| 502 |
currRowKey = None |
|---|
| 503 |
# Create the list to hold the rows for sorting |
|---|
| 504 |
sortList = [] |
|---|
| 505 |
if not ord: |
|---|
| 506 |
# Restore the rows to their unsorted order |
|---|
| 507 |
for row in self._records: |
|---|
| 508 |
if self._compoundKey: |
|---|
| 509 |
key = tuple([row[k] for k in kf]) |
|---|
| 510 |
sortList.append([self.__unsortedRows.index(key), row]) |
|---|
| 511 |
else: |
|---|
| 512 |
sortList.append([self.__unsortedRows.index(row[self.KeyField]), row]) |
|---|
| 513 |
else: |
|---|
| 514 |
for row, rec in enumerate(self._records): |
|---|
| 515 |
sortList.append([self.getFieldVal(col, row), rec]) |
|---|
| 516 |
# At this point we have a list consisting of lists. Each of these member |
|---|
| 517 |
# lists contain the sort value in the zeroth element, and the row as |
|---|
| 518 |
# the first element. |
|---|
| 519 |
# First, see if we are comparing strings |
|---|
| 520 |
compString = isinstance(sortList[0][0], basestring) |
|---|
| 521 |
sortfunc = None |
|---|
| 522 |
|
|---|
| 523 |
# can't compare NoneType to some types: sort None lower than anything else: |
|---|
| 524 |
def noneSort(vv, ww): |
|---|
| 525 |
xx, yy = vv[0], ww[0] |
|---|
| 526 |
if xx is None and yy is None: |
|---|
| 527 |
return 0 |
|---|
| 528 |
elif xx is None and yy is not None: |
|---|
| 529 |
return -1 |
|---|
| 530 |
elif xx is not None and yy is None: |
|---|
| 531 |
return 1 |
|---|
| 532 |
else: |
|---|
| 533 |
return cmp(xx, yy) |
|---|
| 534 |
|
|---|
| 535 |
def caseInsensitiveSort(vv, ww): |
|---|
| 536 |
vv, ww = vv[0], ww[0] |
|---|
| 537 |
if vv is None: |
|---|
| 538 |
vv = "" |
|---|
| 539 |
if ww is None: |
|---|
| 540 |
ww = "" |
|---|
| 541 |
return cmp(vv.lower(), ww.lower()) |
|---|
| 542 |
|
|---|
| 543 |
if compString and not caseSensitive: |
|---|
| 544 |
sortfunc = caseInsensitiveSort |
|---|
| 545 |
else: |
|---|
| 546 |
sortfunc = noneSort |
|---|
| 547 |
sortList.sort(sortfunc) |
|---|
| 548 |
|
|---|
| 549 |
# Unless DESC was specified as the sort order, we're done sorting |
|---|
| 550 |
if ord == "DESC": |
|---|
| 551 |
sortList.reverse() |
|---|
| 552 |
# Extract the rows into a new list, then convert them back to the _records tuple |
|---|
| 553 |
newRows = [] |
|---|
| 554 |
for elem in sortList: |
|---|
| 555 |
newRows.append(elem[1]) |
|---|
| 556 |
self._records = dDataSet(newRows) |
|---|
| 557 |
|
|---|
| 558 |
# restore the RowNumber |
|---|
| 559 |
if currRowKey: |
|---|
| 560 |
for ii in range(0, self.RowCount): |
|---|
| 561 |
row = self._records[ii] |
|---|
| 562 |
if self._compoundKey: |
|---|
| 563 |
key = tuple([row[k] for k in kf]) |
|---|
| 564 |
found = (key == currRowKey) |
|---|
| 565 |
else: |
|---|
| 566 |
found = row[self.KeyField] == currRowKey |
|---|
| 567 |
|---|