diff options
Diffstat (limited to 'cheetah/Tools/MondoReport.py')
-rw-r--r-- | cheetah/Tools/MondoReport.py | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/cheetah/Tools/MondoReport.py b/cheetah/Tools/MondoReport.py new file mode 100644 index 0000000..d0fada2 --- /dev/null +++ b/cheetah/Tools/MondoReport.py @@ -0,0 +1,463 @@ +""" +@@TR: This code is pretty much unsupported. + +MondoReport.py -- Batching module for Python and Cheetah. + +Version 2001-Nov-18. Doesn't do much practical yet, but the companion +testMondoReport.py passes all its tests. +-Mike Orr (Iron) + +TODO: BatchRecord.prev/next/prev_batches/next_batches/query, prev.query, +next.query. + +How about Report: .page(), .all(), .summary()? Or PageBreaker. +""" +import operator, types +try: + from Cheetah.NameMapper import valueForKey as lookup_func +except ImportError: + def lookup_func(obj, name): + if hasattr(obj, name): + return getattr(obj, name) + else: + return obj[name] # Raises KeyError. + +########## CONSTANTS ############################## + +True, False = (1==1), (1==0) +numericTypes = types.IntType, types.LongType, types.FloatType + +########## PUBLIC GENERIC FUNCTIONS ############################## + +class NegativeError(ValueError): + pass + +def isNumeric(v): + return type(v) in numericTypes + +def isNonNegative(v): + ret = isNumeric(v) + if ret and v < 0: + raise NegativeError(v) + +def isNotNone(v): + return v is not None + +def Roman(n): + n = int(n) # Raises TypeError. + if n < 1: + raise ValueError("roman numeral for zero or negative undefined: " + n) + roman = '' + while n >= 1000: + n = n - 1000 + roman = roman + 'M' + while n >= 500: + n = n - 500 + roman = roman + 'D' + while n >= 100: + n = n - 100 + roman = roman + 'C' + while n >= 50: + n = n - 50 + roman = roman + 'L' + while n >= 10: + n = n - 10 + roman = roman + 'X' + while n >= 5: + n = n - 5 + roman = roman + 'V' + while n < 5 and n >= 1: + n = n - 1 + roman = roman + 'I' + roman = roman.replace('DCCCC', 'CM') + roman = roman.replace('CCCC', 'CD') + roman = roman.replace('LXXXX', 'XC') + roman = roman.replace('XXXX', 'XL') + roman = roman.replace('VIIII', 'IX') + roman = roman.replace('IIII', 'IV') + return roman + + +def sum(lis): + return reduce(operator.add, lis, 0) + +def mean(lis): + """Always returns a floating-point number. + """ + lis_len = len(lis) + if lis_len == 0: + return 0.00 # Avoid ZeroDivisionError (not raised for floats anyway) + total = float( sum(lis) ) + return total / lis_len + +def median(lis): + lis = lis[:] + lis.sort() + return lis[int(len(lis)/2)] + + +def variance(lis): + raise NotImplementedError() + +def variance_n(lis): + raise NotImplementedError() + +def standardDeviation(lis): + raise NotImplementedError() + +def standardDeviation_n(lis): + raise NotImplementedError() + + + +class IndexFormats: + """Eight ways to display a subscript index. + ("Fifty ways to leave your lover....") + """ + def __init__(self, index, item=None): + self._index = index + self._number = index + 1 + self._item = item + + def index(self): + return self._index + + __call__ = index + + def number(self): + return self._number + + def even(self): + return self._number % 2 == 0 + + def odd(self): + return not self.even() + + def even_i(self): + return self._index % 2 == 0 + + def odd_i(self): + return not self.even_i() + + def letter(self): + return self.Letter().lower() + + def Letter(self): + n = ord('A') + self._index + return chr(n) + + def roman(self): + return self.Roman().lower() + + def Roman(self): + return Roman(self._number) + + def item(self): + return self._item + + + +########## PRIVATE CLASSES ############################## + +class ValuesGetterMixin: + def __init__(self, origList): + self._origList = origList + + def _getValues(self, field=None, criteria=None): + if field: + ret = [lookup_func(elm, field) for elm in self._origList] + else: + ret = self._origList + if criteria: + ret = filter(criteria, ret) + return ret + + +class RecordStats(IndexFormats, ValuesGetterMixin): + """The statistics that depend on the current record. + """ + def __init__(self, origList, index): + record = origList[index] # Raises IndexError. + IndexFormats.__init__(self, index, record) + ValuesGetterMixin.__init__(self, origList) + + def length(self): + return len(self._origList) + + def first(self): + return self._index == 0 + + def last(self): + return self._index >= len(self._origList) - 1 + + def _firstOrLastValue(self, field, currentIndex, otherIndex): + currentValue = self._origList[currentIndex] # Raises IndexError. + try: + otherValue = self._origList[otherIndex] + except IndexError: + return True + if field: + currentValue = lookup_func(currentValue, field) + otherValue = lookup_func(otherValue, field) + return currentValue != otherValue + + def firstValue(self, field=None): + return self._firstOrLastValue(field, self._index, self._index - 1) + + def lastValue(self, field=None): + return self._firstOrLastValue(field, self._index, self._index + 1) + + # firstPage and lastPage not implemented. Needed? + + def percentOfTotal(self, field=None, suffix='%', default='N/A', decimals=2): + rec = self._origList[self._index] + if field: + val = lookup_func(rec, field) + else: + val = rec + try: + lis = self._getValues(field, isNumeric) + except NegativeError: + return default + total = sum(lis) + if total == 0.00: # Avoid ZeroDivisionError. + return default + val = float(val) + try: + percent = (val / total) * 100 + except ZeroDivisionError: + return default + if decimals == 0: + percent = int(percent) + else: + percent = round(percent, decimals) + if suffix: + return str(percent) + suffix # String. + else: + return percent # Numeric. + + def __call__(self): # Overrides IndexFormats.__call__ + """This instance is not callable, so we override the super method. + """ + raise NotImplementedError() + + def prev(self): + if self._index == 0: + return None + else: + length = self.length() + start = self._index - length + return PrevNextPage(self._origList, length, start) + + def next(self): + if self._index + self.length() == self.length(): + return None + else: + length = self.length() + start = self._index + length + return PrevNextPage(self._origList, length, start) + + def prevPages(self): + raise NotImplementedError() + + def nextPages(self): + raise NotImplementedError() + + prev_batches = prevPages + next_batches = nextPages + + def summary(self): + raise NotImplementedError() + + + + def _prevNextHelper(self, start,end,size,orphan,sequence): + """Copied from Zope's DT_InSV.py's "opt" function. + """ + if size < 1: + if start > 0 and end > 0 and end >= start: + size=end+1-start + else: size=7 + + if start > 0: + + try: sequence[start-1] + except: start=len(sequence) + # if start > l: start=l + + if end > 0: + if end < start: end=start + else: + end=start+size-1 + try: sequence[end+orphan-1] + except: end=len(sequence) + # if l - end < orphan: end=l + elif end > 0: + try: sequence[end-1] + except: end=len(sequence) + # if end > l: end=l + start=end+1-size + if start - 1 < orphan: start=1 + else: + start=1 + end=start+size-1 + try: sequence[end+orphan-1] + except: end=len(sequence) + # if l - end < orphan: end=l + return start,end,size + + + +class Summary(ValuesGetterMixin): + """The summary statistics, that don't depend on the current record. + """ + def __init__(self, origList): + ValuesGetterMixin.__init__(self, origList) + + def sum(self, field=None): + lis = self._getValues(field, isNumeric) + return sum(lis) + + total = sum + + def count(self, field=None): + lis = self._getValues(field, isNotNone) + return len(lis) + + def min(self, field=None): + lis = self._getValues(field, isNotNone) + return min(lis) # Python builtin function min. + + def max(self, field=None): + lis = self._getValues(field, isNotNone) + return max(lis) # Python builtin function max. + + def mean(self, field=None): + """Always returns a floating point number. + """ + lis = self._getValues(field, isNumeric) + return mean(lis) + + average = mean + + def median(self, field=None): + lis = self._getValues(field, isNumeric) + return median(lis) + + def variance(self, field=None): + raiseNotImplementedError() + + def variance_n(self, field=None): + raiseNotImplementedError() + + def standardDeviation(self, field=None): + raiseNotImplementedError() + + def standardDeviation_n(self, field=None): + raiseNotImplementedError() + + +class PrevNextPage: + def __init__(self, origList, size, start): + end = start + size + self.start = IndexFormats(start, origList[start]) + self.end = IndexFormats(end, origList[end]) + self.length = size + + +########## MAIN PUBLIC CLASS ############################## +class MondoReport: + _RecordStatsClass = RecordStats + _SummaryClass = Summary + + def __init__(self, origlist): + self._origList = origlist + + def page(self, size, start, overlap=0, orphan=0): + """Returns list of ($r, $a, $b) + """ + if overlap != 0: + raise NotImplementedError("non-zero overlap") + if orphan != 0: + raise NotImplementedError("non-zero orphan") + origList = self._origList + origList_len = len(origList) + start = max(0, start) + end = min( start + size, len(self._origList) ) + mySlice = origList[start:end] + ret = [] + for rel in range(size): + abs_ = start + rel + r = mySlice[rel] + a = self._RecordStatsClass(origList, abs_) + b = self._RecordStatsClass(mySlice, rel) + tup = r, a, b + ret.append(tup) + return ret + + + batch = page + + def all(self): + origList_len = len(self._origList) + return self.page(origList_len, 0, 0, 0) + + + def summary(self): + return self._SummaryClass(self._origList) + +""" +********************************** + Return a pageful of records from a sequence, with statistics. + + in : origlist, list or tuple. The entire set of records. This is + usually a list of objects or a list of dictionaries. + page, int >= 0. Which page to display. + size, int >= 1. How many records per page. + widow, int >=0. Not implemented. + orphan, int >=0. Not implemented. + base, int >=0. Number of first page (usually 0 or 1). + + out: list of (o, b) pairs. The records for the current page. 'o' is + the original element from 'origlist' unchanged. 'b' is a Batch + object containing meta-info about 'o'. + exc: IndexError if 'page' or 'size' is < 1. If 'origlist' is empty or + 'page' is too high, it returns an empty list rather than raising + an error. + + origlist_len = len(origlist) + start = (page + base) * size + end = min(start + size, origlist_len) + ret = [] + # widow, orphan calculation: adjust 'start' and 'end' up and down, + # Set 'widow', 'orphan', 'first_nonwidow', 'first_nonorphan' attributes. + for i in range(start, end): + o = origlist[i] + b = Batch(origlist, size, i) + tup = o, b + ret.append(tup) + return ret + + def prev(self): + # return a PrevNextPage or None + + def next(self): + # return a PrevNextPage or None + + def prev_batches(self): + # return a list of SimpleBatch for the previous batches + + def next_batches(self): + # return a list of SimpleBatch for the next batches + +########## PUBLIC MIXIN CLASS FOR CHEETAH TEMPLATES ############## +class MondoReportMixin: + def batch(self, origList, size=None, start=0, overlap=0, orphan=0): + bat = MondoReport(origList) + return bat.batch(size, start, overlap, orphan) + def batchstats(self, origList): + bat = MondoReport(origList) + return bat.stats() +""" + +# vim: shiftwidth=4 tabstop=4 expandtab textwidth=79 |