| [4260] | 1 | from trac.web.api import ITemplateStreamFilter |
|---|
| 2 | from trac.core import * |
|---|
| 3 | from genshi.core import * |
|---|
| 4 | from genshi.builder import tag |
|---|
| [6531] | 5 | try: |
|---|
| 6 | set |
|---|
| 7 | except NameError: |
|---|
| 8 | from sets import Set as set # Python 2.3 fallback |
|---|
| 9 | |
|---|
| [4260] | 10 | from genshi.filters.transform import Transformer |
|---|
| [4280] | 11 | import re |
|---|
| [7000] | 12 | import dbhelper |
|---|
| [4260] | 13 | |
|---|
| [4281] | 14 | from trac.ticket.report import ReportModule |
|---|
| 15 | from trac.util.datefmt import format_datetime, format_time |
|---|
| 16 | import csv |
|---|
| 17 | from trac.web.api import RequestDone |
|---|
| 18 | |
|---|
| [6790] | 19 | |
|---|
| 20 | # This can go away once they fix http://genshi.edgewall.org/ticket/136 |
|---|
| 21 | # At that point we should use Transformer.filter |
|---|
| [6826] | 22 | # THIS IS STILL SOLVING PROBLEMS WELL AFTER THAT TICKET HAS BEEN CLOSED - A new ticket #290 [1000] has fixed the bug, but is |
|---|
| 23 | # not the trac default yet |
|---|
| [6790] | 24 | # Without this (using the default filter) I was getting omitted closing tags for some tags (Based on whitespace afaict) |
|---|
| 25 | class FilterTransformation(object): |
|---|
| 26 | """Apply a normal stream filter to the selection. The filter is called once |
|---|
| 27 | for each contiguous block of marked events.""" |
|---|
| 28 | |
|---|
| 29 | def __init__(self, filter): |
|---|
| 30 | """Create the transform. |
|---|
| 31 | |
|---|
| 32 | :param filter: The stream filter to apply. |
|---|
| 33 | """ |
|---|
| 34 | self.filter = filter |
|---|
| 35 | |
|---|
| 36 | def __call__(self, stream): |
|---|
| 37 | """Apply the transform filter to the marked stream. |
|---|
| 38 | |
|---|
| 39 | :param stream: The marked event stream to filter |
|---|
| 40 | """ |
|---|
| 41 | def flush(queue): |
|---|
| 42 | if queue: |
|---|
| 43 | for event in self.filter(queue): |
|---|
| 44 | yield event |
|---|
| 45 | del queue[:] |
|---|
| 46 | |
|---|
| 47 | queue = [] |
|---|
| 48 | for mark, event in stream: |
|---|
| 49 | if mark: |
|---|
| 50 | queue.append(event) |
|---|
| 51 | else: |
|---|
| 52 | for e in flush(queue): |
|---|
| 53 | yield None,e |
|---|
| 54 | yield None,event |
|---|
| 55 | for event in flush(queue): |
|---|
| 56 | yield None,event |
|---|
| 57 | |
|---|
| [7011] | 58 | def split_stream(stream): |
|---|
| 59 | """splits the stream based on toplevel START / END tags""" |
|---|
| 60 | cl = [] |
|---|
| 61 | res = [] |
|---|
| 62 | num_start=0 |
|---|
| 63 | for kind, data, pos in stream: |
|---|
| 64 | cl.append((kind, data, pos)) |
|---|
| 65 | if kind == Stream.START: |
|---|
| 66 | num_start = num_start+1 |
|---|
| 67 | elif kind == Stream.END: |
|---|
| 68 | num_start = num_start-1 |
|---|
| 69 | if num_start == 0: |
|---|
| 70 | res.append(Stream(cl)) |
|---|
| 71 | cl=[] |
|---|
| 72 | if cl != []: |
|---|
| 73 | res.append(Stream(cl)) |
|---|
| 74 | return res |
|---|
| 75 | |
|---|
| [6790] | 76 | |
|---|
| [4280] | 77 | billing_report_regex = re.compile("\{(?P<reportid>\d*)\}") |
|---|
| 78 | def report_id_from_text(text): |
|---|
| 79 | m = billing_report_regex.match(text) |
|---|
| 80 | if m: |
|---|
| 81 | return int(m.groupdict()["reportid"]) |
|---|
| 82 | |
|---|
| 83 | def get_billing_reports(comp): |
|---|
| [7000] | 84 | billing_reports = set() |
|---|
| 85 | rows = dbhelper.get_all(comp, "SELECT id FROM custom_report")[1] |
|---|
| 86 | if rows: |
|---|
| 87 | billing_reports = set([x[0] for x in rows]) |
|---|
| 88 | return billing_reports |
|---|
| [4280] | 89 | |
|---|
| [7011] | 90 | |
|---|
| [4260] | 91 | class RowFilter(object): |
|---|
| 92 | """A genshi filter that operates on table rows, completely hiding any that |
|---|
| 93 | are in the billing_reports table.""" |
|---|
| 94 | |
|---|
| 95 | def __init__(self, comp): |
|---|
| 96 | self.component = comp |
|---|
| [4280] | 97 | self.billing_reports = get_billing_reports(comp) |
|---|
| [4260] | 98 | self.component.log.debug('self.billing_reports= %r' % self.billing_reports) |
|---|
| 99 | |
|---|
| 100 | def __call__(self, row_stream): |
|---|
| [7011] | 101 | #stream = Stream(list(row_stream)) |
|---|
| 102 | def tryInt(v): |
|---|
| 103 | try: |
|---|
| 104 | return int(v) |
|---|
| 105 | except: |
|---|
| 106 | return None |
|---|
| 107 | streams = split_stream(row_stream) |
|---|
| 108 | #report_urls = [tryInt(i.get('href').split('/')[-1]) for i in stream.select('td[@class="report"]/a/@href')] |
|---|
| 109 | #self.component.log.debug("ReportRowFilter: #%s#, %r" % (len(streams), list(report_urls))) |
|---|
| 110 | for stream in streams: |
|---|
| 111 | show_row = True |
|---|
| 112 | try: |
|---|
| 113 | report_url = stream.select('td[@class="report"]/a/@href').render() |
|---|
| 114 | id = tryInt(report_url.split('/')[-1]) |
|---|
| 115 | self.component.log.debug("Report row filter: about to filter: %s not in %s : %s" % (id, self.billing_reports, not id in self.billing_reports) ) |
|---|
| 116 | show_row = not id in self.billing_reports |
|---|
| 117 | except Exception, e: |
|---|
| 118 | self.component.log.exception("Report row filter failed") |
|---|
| 119 | show_row = True #Dont Hide Error Rows? |
|---|
| 120 | if show_row: |
|---|
| 121 | for kind,data,pos in stream: |
|---|
| [6826] | 122 | yield kind,data,pos |
|---|
| [4260] | 123 | |
|---|
| 124 | class ReportsFilter(Component): |
|---|
| 125 | """Remove all billing reports from the reports list.""" |
|---|
| 126 | implements(ITemplateStreamFilter) |
|---|
| 127 | |
|---|
| 128 | def filter_stream(self, req, method, filename, stream, data): |
|---|
| [7011] | 129 | if not filename in ('report_view.html', 'report_list.html'): |
|---|
| [4260] | 130 | return stream |
|---|
| 131 | self.log.debug("Applying Reports Filter to remove T&E reports") |
|---|
| 132 | return stream | Transformer( |
|---|
| 133 | '//table[@class="listing reports"]/tbody/tr' |
|---|
| [6790] | 134 | ).apply(FilterTransformation(RowFilter(self))) |
|---|
| [4280] | 135 | |
|---|
| 136 | class ReportScreenFilter(Component): |
|---|
| [4293] | 137 | """Hides TandE reports even when you just go to the url""" |
|---|
| [4280] | 138 | implements(ITemplateStreamFilter) |
|---|
| 139 | def __init__(self): |
|---|
| 140 | self.billing_reports = get_billing_reports(self) |
|---|
| 141 | self.log.debug('ReportScreenFilter: self.billing_reports= %r' % self.billing_reports) |
|---|
| 142 | |
|---|
| 143 | def filter_stream(self, req, method, filename, stream, data): |
|---|
| [7011] | 144 | if not filename in ('report_view.html', 'report_list.html'): |
|---|
| [4280] | 145 | return stream |
|---|
| 146 | reportid = [None] |
|---|
| 147 | |
|---|
| 148 | def idhelper(strm): |
|---|
| 149 | header = strm[0][1] |
|---|
| 150 | if not reportid[0]: |
|---|
| 151 | self.log.debug("ReportScreenFilter: helper: %s %s %s"%(strm,header,report_id_from_text(header))) |
|---|
| [6790] | 152 | reportid[0] = report_id_from_text(header) |
|---|
| [4280] | 153 | for kind, data, pos in strm: |
|---|
| 154 | yield kind, data, pos |
|---|
| 155 | |
|---|
| 156 | def permhelper(strm): |
|---|
| 157 | id = reportid[0] |
|---|
| 158 | self.log.debug("ReportScreenFilter: id:%s, in bill: %s has perm:%s" % (id, id in self.billing_reports, req.perm.has_permission("TIME_VIEW"))) |
|---|
| 159 | if id and id in self.billing_reports and not req.perm.has_permission("TIME_VIEW"): |
|---|
| 160 | self.log.debug("ReportScreenFilter: No time view, prevent render") |
|---|
| 161 | msg = "YOU MUST HAVE TIME_VIEW PERMSSIONS TO VIEW THIS REPORT" |
|---|
| 162 | for kind, data, pos in tag.span(msg).generate(): |
|---|
| 163 | yield kind, data, pos |
|---|
| 164 | else: |
|---|
| 165 | for kind, data, pos in strm: |
|---|
| 166 | yield kind, data, pos |
|---|
| 167 | |
|---|
| [6790] | 168 | self.log.debug("ReportScreenFilter: About to begin filtering of billing reports without permissions") |
|---|
| 169 | stream = stream | Transformer('//div[@id="content"]/h1/text()').apply(FilterTransformation(idhelper)) |
|---|
| 170 | stream = stream | Transformer('//div[@id="content"]').apply(FilterTransformation(permhelper)) |
|---|
| [4280] | 171 | return stream |
|---|
| [4281] | 172 | |
|---|
| 173 | ## ENFORCE PERMISSIONS ON report exports |
|---|
| 174 | |
|---|
| 175 | billing_report_fname_regex = re.compile("report_(?P<reportid>\d*)") |
|---|
| 176 | def report_id_from_filename(text): |
|---|
| [5494] | 177 | if text: |
|---|
| 178 | m = billing_report_fname_regex.match(text) |
|---|
| 179 | if m: |
|---|
| 180 | return int(m.groupdict()["reportid"]) |
|---|
| 181 | return -1; |
|---|
| [4281] | 182 | |
|---|
| 183 | |
|---|
| 184 | def _send_csv(self, req, cols, rows, sep=',', mimetype='text/plain', |
|---|
| 185 | filename=None): |
|---|
| 186 | req.send_response(200) |
|---|
| 187 | req.send_header('Content-Type', mimetype + ';charset=utf-8') |
|---|
| 188 | if filename: |
|---|
| 189 | req.send_header('Content-Disposition', 'filename=' + filename) |
|---|
| 190 | req.end_headers() |
|---|
| 191 | |
|---|
| 192 | id = report_id_from_filename(filename) |
|---|
| 193 | reports = get_billing_reports(self) |
|---|
| [4282] | 194 | if id in reports and not req.perm.has_permission("TIME_VIEW"): |
|---|
| [4281] | 195 | raise RequestDone |
|---|
| 196 | |
|---|
| 197 | def iso_time(t): |
|---|
| 198 | return format_time(t, 'iso8601') |
|---|
| 199 | |
|---|
| 200 | def iso_datetime(dt): |
|---|
| 201 | return format_datetime(dt, 'iso8601') |
|---|
| 202 | |
|---|
| 203 | col_conversions = { |
|---|
| 204 | 'time': iso_time, |
|---|
| 205 | 'datetime': iso_datetime, |
|---|
| 206 | 'changetime': iso_datetime, |
|---|
| 207 | 'date': iso_datetime, |
|---|
| 208 | 'created': iso_datetime, |
|---|
| 209 | 'modified': iso_datetime, |
|---|
| 210 | } |
|---|
| 211 | |
|---|
| 212 | converters = [col_conversions.get(c.strip('_'), unicode) for c in cols] |
|---|
| 213 | |
|---|
| 214 | writer = csv.writer(req, delimiter=sep) |
|---|
| 215 | writer.writerow([unicode(c).encode('utf-8') for c in cols]) |
|---|
| 216 | for row in rows: |
|---|
| 217 | row = list(row) |
|---|
| 218 | for i in xrange(len(row)): |
|---|
| 219 | row[i] = converters[i](row[i]).encode('utf-8') |
|---|
| 220 | writer.writerow(row) |
|---|
| 221 | |
|---|
| 222 | raise RequestDone |
|---|
| 223 | |
|---|
| 224 | ReportModule._send_csv = _send_csv |
|---|