예제 #1
0
    def process_block(self, block_text, user, **params):
        """ Yields all query results at once. Only once.
        May return an exception.
        """
        try:
            tokens = self.parse_block(block_text)
            db_name = tokens.get(self.ATTR_DB,
                                 settings.DATABASE_DEFAULT_ALIAS).strip()
            con = None

            db = settings.DATABASES.get(db_name, None)
            if db == None:
                raise DwException(db_name, "Unknown database %s." % db_name)
            true_sql = tokens.sqltext
            p = re.compile(u'\$\{[a-z]+[0-9]*\}')
            lst = p.findall(true_sql)

            for item in lst:
                stripped_item = item[2:-1]
                # TODO check for default value here
                value = 'None'
                if stripped_item in params:
                    value = params[stripped_item]
                    value = urllib.unquote(value)
                # replace it
                true_sql = true_sql.replace(item, value)

            # get a connection from somewhere
            con = self.get_con(db)
            cur = con.cursor()
            cur.execute(true_sql)

            row = cur.fetchone()
            num_rows = 0
            while row is not None:
                if num_rows == db.get('LIMIT_ROWS', 1000):
                    # we are 1 row over limit
                    add_line = htmlconstants.SQL_OVER_LIMIT.format(num_rows)
                    row = None
                    break
                else:
                    retstr = u''
                    for val in row:
                        retstr = retstr + ' ' + self.format_value(val)
                    retstr += "\n"
                    yield retstr
                    num_rows += 1
                    row = cur.fetchone()
            con.close()
            con = None

        except DwException, e:
            if con is not None:
                con.close()
            raise e
예제 #2
0
    def process_block(self, block_text, user, block_id, **params):
        def int_or_none(val):
            """Returns an integer or none. May pass anything
            """
            try:
                ret = int(val)
            except ValueError:
                ret = None
            return ret

        def float_or_none(val):
            """Returns a float or none. May pass anything
            """
            if val is None:
                return val
            else:
                try:
                    ret = float(val)
                except ValueError:
                    ret = None
                return ret

        def int_or_default(val, default=0):
            """ Returns int or a given default
            """
            try:
                ret = int(val)
            except ValueError:
                ret = default

            return ret

        try:
            tokens = self.parse_block(block_text)
        except Exception, e:
            traceback.print_exc()
            # reraise it wrapped in our class
            raise DwException('', "Errpr in graph definition", e)
예제 #3
0
class SqlTableStreamer(dwstreamer.BaseDwStreamer):
    """Block streamer that creates a table out of sql query"""
    
    TAGNAME = 'sqltable'

    # re to find attributes for table cells
    ATTR_RE = re.compile('\{: *([ [a-z]+=\".+\" *]*)\}')

    # Attributes of {sqltable} tag
    ATTR_DB = u'db'
    ATTR_TOTALS = u'totals'
    ATTR_ORIENTATION = u'orientation'
    # available orientations
    ATTR_VAL_ORIENTATION_VERTICAL = u'vertical'
    ATTR_VAL_ORIENTATION_HORIZONTAL = u'horizontal'

    ATTR_DEFAULT_VAL_ORIENTATION = ATTR_VAL_ORIENTATION_VERTICAL

    # table style
    ATTR_STYLE = u'style'
    # available styles are there in css file
    ATTR_DEFAULT_VAL_STYLE = u'standard-report'

    # header or no header. Can be 'yes' or 'no'. Default - 'yes'
    ATTR_HEADER = u'header'
    ATTR_DEFAULT_VAL_HEADER = u'yes'
    ATTR_HEADER_VAL_YES = u'yes'
    ATTR_HEADER_VAL_NO = u'no'

    # debugging attribute for delaying rows output
    ATTR_DELAY = u'delay'
    ATTR_DELAY_DEFAULT_VAL = u'0'

    # value types that can be aggregated
    FIELD_TYPES_AGGREGATION = ['Decimal', 'int', 'long', 'float']

    def __init__(self):
        self.con = None
        self.sett = settings.CSV_DOWNLOAD_FORMAT['en']
        # separator for csv
        self.sep = unicode(self.sett.get('FIELD_SEPARATOR', ','))

    
    def factory(self):
        return SqlTableStreamer()


    def get_tagname(self):
        return self.TAGNAME

    def format_value(self, val, html=True):
        """default formatting of database values.
        returns <td>value</td> if html=True. Else returns just value"""

        #td_start = u'<td class='
        cell_style = self.table_css_class + '-cell'
        td_start = u'<td class="' + cell_style + u'"'
        td_align_left = ' '
        td_align_right = ' align="right" '
        align = td_align_left
        #res = td + unicode(val) + '</td>'
        tp = type(val).__name__

        #md = markdown.Markdown(extensions=['markdown.extensions.attr_list'])
        md = markdown.Markdown()
        #attr = markdown.extensions.attr_list
        #attr.extend(md, [])
        
        #md = markdown.Markdown(safe_mode='escape')
        attrs = ''
        if tp == 'str':
            res = val.decode('utf-8')
            match = self.ATTR_RE.search(res)
            if match != None:
                attrs = ' ' + match.group(1) + ' '
                res = re.sub(self.ATTR_RE, '', res)
            # markdown it
            res = md.convert(res)
            # remove <p></p>
            if res[:3] == '<p>':
                res = res[3:-4]
        elif tp == 'unicode':
            res = unicode(val)
            match = self.ATTR_RE.search(res)
            if match != None:
                attrs = ' ' + match.group(1) + ' '
                res = re.sub(self.ATTR_RE, '', res)
            # markdown it
            res = md.convert(res)
            # remove <p></p>
            if res[:3] == '<p>':
                res = res[3:-4]
        elif tp in ['Decimal', 'float']:
            res = "{0:0.2f}".format(val)
            align = td_align_right
        elif tp == 'date':
            res = val.strftime("%d.%m.%Y")
        elif tp in ['int', 'long']:
            res = "{0}".format(val)
            align = td_align_right
        elif tp in ['NoneType']:
            res = '&nbsp;'
        else:
            # unknown datatype
            res = unicode(val)
            match = self.ATTR_RE.search(res)
            if match != None:
                attrs = ' ' + match.group(1) + ' '
                res = re.sub(self.ATTR_RE, '', res)
            # markdown it
            res = md.convert(res)
            # remove <p></p>
            if res[:3] == '<p>':
                res = res[3:-4]

        if html:
            res = td_start + align + attrs + '>' + res + '</td>'
            

        # now we can possibly have a link in a column.
        # It may contain URL parameters with special characters.
        # Something like /myreport?name="Some long name in God knows what Language"
        # We should quote it
        return res

    def format_csv_value(self, val):
        """Format a value for csv output, according to standard csv rules
        (https://en.wikipedia.org/wiki/Comma-separated_values).
        Also settings are used. The value is returned as unicode.
        All html, attributes and markdown links are replaced by plain text"""

        def deal_with_str(strval):
            """Formats string as unicode with optional double quotes according
            to standard"""
            need_quotes = False
            result = strval
            # first commas or semicolons
            ind = result.find(self.sep)
            if ind >= 0:
                need_quotes = True
            # now double quotes
            # we need to add one to each occurence
            ind = result.find('"')
            if ind >= 0:
                need_quotes = True
                result = result.replace('"', '""')
            # newline characters.
            newline_repl = self.sett.get('NEWLINE_REPLACEMENT', None)
            if newline_repl is None:
                need_quotes = True
            else:
                # replace them
                result = result.replace("\r\n", newline_repl)
                result = result.replace("\n", newline_repl)
                result = result.replace("\r", newline_repl)
            # leading or trailing spaces require double quotes too
            if (result[0] == ' ') or (result[-1] == ' '):
                need_quotes = True
            # tabs require quotes or replacement
            ind = result.find("\t")
            if ind >= 0:
                tab_repl = self.sett.get('TAB_REPLACEMENT', None)
                if tab_repl is None:
                    need_quotes = True
                else:   
                    result = result.replace("\t", tab_repl)
            # finally
            if need_quotes:
                result = u'"' + result + u'"'
            return result


        sett = settings.CSV_DOWNLOAD_FORMAT['en']
        tp = type(val).__name__
        res = ''
        if tp == 'str':
            res = deal_with_str(val.decode('utf-8'))
        elif tp == 'unicode':
            res = deal_with_str(unicode(val))
        elif tp in ['Decimal', 'float', 'int', 'long']:
            # plain
            res = str(val)
        elif tp == 'date':
            res = val.strftime(sett['DATE_FORMAT'])
        elif tp in ['NoneType']:
            res = ''

        # attributes
        match = self.ATTR_RE.search(res)
        if match != None:
            attrs = ' ' + match.group(1) + ' '
            res = re.sub(self.ATTR_RE, '', res)

        return res
        

    # gets a database connection
    # it should work with several connections
    # throws a DWDatabaseError 
    def get_con(self):
        # TODO work around assigning a connection to a member var.
        # We really want to have a stateless object here
        self.con = dwwiki.connectors.get_connection(self.db)
#        #return con
#
#        #con = MySQLdb.connect(host=db['HOST'], user=db['USER'], passwd=db['PASSWORD'], db=db['DB'],
#        #    charset='utf8',
#        #    cursorclass = MySQLdb.cursors.SSCursor)
#        self.con = None
#        db = self.db
#        # TODO different connectors should live in different files
#        if db['ENGINE'] == 'mysql':
#            import mysql.connector
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            self.con = mysql.connector.connect(
#                user=db['USER'],
#                passwd=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                connection_timeout=timeout
#            )
#        elif db['ENGINE'] == 'postgresql':
#            import psycopg2
#            # set query timeout as options to psycopg
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            self.con = psycopg2.connect(
#                user=db['USER'],
#                password=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                port=db['PORT'],
#                options="-c statement_timeout=%d" % timeout
#            )
#            self.con.set_client_encoding('utf-8')
#        elif db['ENGINE'] == 'sqlite':
#            import sqlite3
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            self.con = sqlite3.connect(
#                database=db['DB'],
#                timeout=timeout
#            )


    # returns sql block parsed into tokens
    def parse_block(self, block_text):
        # make a grammar
        block_start = Literal("{")
        sql_start = Keyword("sqltable", caseless=True)
        colon = Literal(":")
        sql_end = Literal("}")
        separator = Literal("|")
        block_end = Keyword("{sqltable}", caseless=True)

        # params
        field_name = Word(alphanums)
        equal_sign = Suppress(Literal("="))

        # whatever value
        field_value = (CharsNotIn("|}"))

        # param name and value
        param_group = Group(field_name + equal_sign + field_value)

        # list of all params
        param_list = delimitedList(param_group, '|')

        # helper
        param_dict = Dict(param_list)

        # sql text
        sql_text = SkipTo(block_end)

        sqldecl = Forward()

        sqldecl << (block_start +
                    sql_start + 
                    Optional(colon) +
                    Optional(param_dict) +
                    sql_end +
                    sql_text.setResultsName('sqltext') +
                    block_end)

        block_str = "".join(block_text) 
        tokens = sqldecl.parseString( block_str )
        return tokens


    def prepare_for_query(self, block_text, user, **params):
        self.tokens = self.parse_block(block_text)
        # set all required parameters
        self.db_name = self.tokens.get(self.ATTR_DB, settings.DATABASE_DEFAULT_ALIAS).strip()
        # check orientation
        self.orientation = self.tokens.get(self.ATTR_ORIENTATION, self.ATTR_DEFAULT_VAL_ORIENTATION).strip()
        # string for totals
        self.totals_str = self.tokens.get(self.ATTR_TOTALS, '')
        # array of required totals like ,,sum,crap,avg
        self.totals_array = []
        # array to calculate real totals if possible
        self.totals_values = []
        if self.totals_str <> '':
            # user requires totals
            self.totals_array = self.totals_str.split(',')

        # table, row, footer and cell styles
        self.table_css_class = self.tokens.get(self.ATTR_STYLE, self.ATTR_DEFAULT_VAL_STYLE).lower().strip()

        # header
        self.header = self.tokens.get(self.ATTR_HEADER, self.ATTR_DEFAULT_VAL_HEADER).lower().strip()
        # avoid gibberish
        if not self.header in [self.ATTR_HEADER_VAL_YES, self.ATTR_HEADER_VAL_NO]:
            self.header = self.ATTR_HEADER_VAL_YES

        # delay for rows. in milliseconds. for debugging
        self.delay = 0
        s = self.tokens.get(self.ATTR_DELAY, self.ATTR_DELAY_DEFAULT_VAL)
        try:
            self.delay = int(s)
        except ValueError, e:
            self.delay = 0
        # beware for max and min: 0 to 10,000 = 10 seconds
        if (self.delay < 0):
            self.delay = 0
        elif self.delay > 10000:
            self.delay = 10000
            
        
        # database. real one
        self.db = settings.DATABASES.get(self.db_name, None)
        if self.db == None:
            raise DwException(self.db_name, "Unknown database %s." % self.db_name)
        
        self.true_sql = self.tokens.sqltext
        # now that we have a sql, try to replace parameters in it.
        # Parameter looks like ${param_name}         
        p = re.compile(u'\$\{[a-z]+[0-9]*\}')
        # get the list of all mentioned params
        lst = p.findall(self.true_sql)
        # loop through all these variables and try to replace
        # them with params passed to us
        # test with sql with parameters

        for item in lst:
            stripped_item = item[2:-1]
            # TODO check for default value here
            value = 'None'
            if stripped_item in params:                
                value = params[stripped_item]
                value = urllib.unquote(value)
            # replace it
            self.true_sql = self.true_sql.replace(item, value)
        
        # get a connection from somewhere
        self.get_con()
        self.cur = self.con.cursor()
        self.cur.execute(self.true_sql)
예제 #4
0
            #        download_link + '">csv</a></td></tr>' + "\n").format(col_count)
            # close table tag
            closing_line += u"</table>\n" + add_line

            yield closing_line
        except DwException, e:
            if self.con is not None:
                self.con.close()
            raise e
            
        except Exception, e:
            if self.con is not None:
                self.con.close()
            traceback.print_exc()
            # reraise it wrapped in our class
            raise DwException(self.db_name, "Some source exception", e)
            

    def process_block_as_csv(self, tempfile, block_text, user, **params):

        #tokens = self.parse_block(block_text)
        #db_name = tokens.get(self.ATTR_DB, settings.DATABASE_DEFAULT_ALIAS).strip()
        #con = None
        sett = settings.CSV_DOWNLOAD_FORMAT['en']
        sep = unicode(sett.get('FIELD_SEPARATOR', ','))
        enc = unicode(sett.get('ENCODING', 'utf8'))
        nl = sett.get('NEWLINE', "\n")
        try:
            self.prepare_for_query(block_text, user, **params)
            
            # process columns first
예제 #5
0
class SqlLineChartStreamer(dwstreamer.BaseDwStreamer):
    """Creates a simple line chart
    A query should return columns in a given order:
    1st - An x-axis tick marks
    2nd - y value
    3rd - etc. other lines if there is more than one
    """

    TAGNAME = 'sqllinechart'

    # === Attributes that can be used with tagname sqllinechart ===

    # this is as usual
    ATTR_DB = 'db'

    ATTR_TITLE = 'title'
    ATTR_WIDTH = 'width'
    ATTR_HEIGHT = 'height'
    ATTR_XLABEL = 'xlabel'
    ATTR_YLABEL = 'ylabel'
    ATTR_FONT_SIZE = 'fontsize'
    # grid lines to draw. Can be 'x', 'y', and 'both'
    ATTR_GRID = 'grid'
    # min and max y values. In floats or integers. Otherwise ignored.
    ATTR_MINY = 'miny'
    ATTR_MAXY = 'maxy'

    # Here we may specify several colors delimited by commas
    # Like green, red, black separated by commas
    ATTR_LINE_COLOR = 'linecolor'

    # line width in pixels
    ATTR_LINE_WIDTH = 'linewidth'

    # the line color if user hasn't specified one
    DEFAULT_LINE_COLOR = 'black'

    # fill color for line
    ATTR_FILL_COLOR = 'fillcolor'
    DEFAULT_FILL_COLOR = 'none'

    ATTR_MARKER = 'marker'
    ATTR_MARKER_DEFAULT = ''

    # the line color if user has specified it,
    # but made an error
    USER_ERROR_LINE_COLOR = 'grey'

    ATTR_LEGEND = 'legend'
    ATTR_VALUE_YES = 'yes'
    ATTR_VALUE_NO = 'no'
    ATTR_LEGEND_DEFAULT_VALUE = ATTR_VALUE_YES

    # maximum number of line points allowed
    # if there is more, just don't draw them
    MAX_X_POINTS = 20000

    # maximum number of parallel lines that can be shown
    # simultaneously.
    MAX_LINE_SETS = 20

    # default top margin over the top of the maximum y value
    # measured in fractions of the difference between min and max
    # values
    DEFAULT_TOP_Y_AXIS_MARGIN = 0.1

    # font size for text and tick marks
    DEFAULT_FONT_SIZE = 10

    def factory(self):
        return SqlLineChartStreamer()

    def make_unicode_or_none(self, val):
        """ Given a value try to make a unicode string
        out of it, transforming strings, numbers and dates.
        Or make it None. Assume non-unicode strings
        are in utf-8
        """
        ret = None
        if type(val) is str:
            ret = val.decode('utf-8')
        elif type(val) is unicode:
            ret = val
        else:
            ret = unicode(val)

        return ret

    def get_tagname(self):
        return self.TAGNAME

    # gets a database connection
    # it should work with several connections
    # throws a DWDatabaseError
    def get_con(self, db):
        con = dwwiki.connectors.get_connection(db)
        return con
#        # TODO work around if connection does not exist
#        #db = settings.DATABASES[dbname]
#
#        #con = MySQLdb.connect(host=db['HOST'], user=db['USER'], passwd=db['PASSWORD'], db=db['DB'],
#        #    charset='utf8',
#        #    cursorclass = MySQLdb.cursors.SSCursor)
#        con = None
#        # TODO different connectors should live in different files
#        if db['ENGINE'] == 'mysql':
#            import mysql.connector
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = mysql.connector.connect(
#                user=db['USER'],
#                passwd=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                connection_timeout=timeout
#            )
#        elif db['ENGINE'] == 'postgresql':
#            import psycopg2
#            # set query timeout as options to psycopg
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = psycopg2.connect(
#                user=db['USER'],
#                password=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                port=db['PORT'],
#                options="-c statement_timeout=%d" % timeout
#            )
#            con.set_client_encoding('utf-8')
#        elif db['ENGINE'] == 'sqlite':
#            import sqlite3
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = sqlite3.connect(
#                database=db['DB'],
#                timeout=timeout
#            )
#
#
#            #con.set_client_encoding('unicode')
#
#        return con

    def parse_block(self, block_text):
        """Parses sql block into tokens
        """

        # Valid grammar looks like this:
        #   {sqllinechart: title='Some string' | colors=green, yellow}

        # make a grammar
        block_start = Literal("{")
        sql_start = Keyword(self.TAGNAME, caseless=True)
        colon = Literal(":")
        sql_end = Literal("}")
        separator = Literal("|")
        block_end = Keyword("{" + self.TAGNAME + "}", caseless=True)

        # params
        field_name = Word(alphanums)
        equal_sign = Suppress(Literal("="))

        # whatever value
        field_value = (CharsNotIn("|}"))

        # param name and value
        param_group = Group(field_name + equal_sign + field_value)

        # list of all params
        param_list = delimitedList(param_group, '|')

        # helper
        param_dict = Dict(param_list)

        # sql text
        sql_text = SkipTo(block_end)

        sqldecl = Forward()

        sqldecl << (block_start + sql_start + Optional(colon) +
                    Optional(param_dict) + sql_end +
                    sql_text.setResultsName('sqltext') + block_end)

        block_str = "".join(block_text)
        tokens = sqldecl.parseString(block_str)
        return tokens

    def process_block(self, block_text, user, block_id, **params):
        def int_or_none(val):
            """Returns an integer or none. May pass anything
            """
            try:
                ret = int(val)
            except ValueError:
                ret = None
            return ret

        def float_or_none(val):
            """Returns a float or none. May pass anything
            """
            if val is None:
                return val
            else:
                try:
                    ret = float(val)
                except ValueError:
                    ret = None
                return ret

        def int_or_default(val, default=0):
            """ Returns int or a given default
            """
            try:
                ret = int(val)
            except ValueError:
                ret = default

            return ret

        try:
            tokens = self.parse_block(block_text)
        except Exception, e:
            traceback.print_exc()
            # reraise it wrapped in our class
            raise DwException('', "Errpr in graph definition", e)

        # get our tokens
        db_name = tokens.get(self.ATTR_DB,
                             settings.DATABASE_DEFAULT_ALIAS).strip()

        # chart title. empty by default
        chart_title = tokens.get(self.ATTR_TITLE, '').strip()

        # line colors
        line_color_str = tokens.get(self.ATTR_LINE_COLOR,
                                    self.DEFAULT_LINE_COLOR).strip()
        line_color_str = line_color_str.encode('utf-8')
        # separate into array
        # this is not final. Probably there are errors here.
        # We'll deal with it later, when we read the columns
        line_color_list = line_color_str.split(',')

        fill_color_str = tokens.get(self.ATTR_FILL_COLOR,
                                    self.DEFAULT_FILL_COLOR).strip()
        fill_color_str = fill_color_str.encode('utf-8')
        fill_color_list = fill_color_str.split(',')

        # line style.
        marker_str = tokens.get(self.ATTR_MARKER,
                                self.ATTR_MARKER_DEFAULT).strip()
        #line_style_list = line_style_str.split(',')
        # TODO check line styles

        # chart width and height in pixels. Beware, the user
        # inevitably will write anything but valid numbers
        chart_width = int_or_none(tokens.get(self.ATTR_WIDTH, '').strip())
        chart_height = int_or_none(tokens.get(self.ATTR_HEIGHT, '').strip())
        xlabel = tokens.get(self.ATTR_XLABEL, '').strip()
        ylabel = tokens.get(self.ATTR_YLABEL, '').strip()
        font_size = int_or_default(
            tokens.get(self.ATTR_FONT_SIZE, self.DEFAULT_FONT_SIZE))
        if font_size <= 0:
            font_size = self.DEFAULT_FONT_SIZE

        # max and min y
        miny = float_or_none(tokens.get(self.ATTR_MINY, None))
        maxy = float_or_none(tokens.get(self.ATTR_MAXY, None))

        # legend
        legend_str = tokens.get(self.ATTR_LEGEND,
                                self.ATTR_LEGEND_DEFAULT_VALUE).strip()
        # check for errors
        if legend_str not in (self.ATTR_VALUE_YES, self.ATTR_VALUE_NO):
            legend_str = self.ATTR_LEGEND_DEFAULT_VALUE

        # grid
        grid = tokens.get(self.ATTR_GRID, None)
        if grid is not None:
            grid = grid.strip()
            if grid not in ['x', 'y', 'both']:
                grid = None

        # line widths for all the lines in pixels separated by commas
        line_width_str = tokens.get(self.ATTR_LINE_WIDTH, '').strip()

        # also check for negative numbers or zeroes
        if (chart_width is not None) and (chart_height is not None):
            if (chart_width <= 0):
                chart_width = None
            if (chart_height <= 0):
                chart_height = None
        else:
            # some of them are none
            # set both to none
            chart_width = None
            chart_height = None

        con = None
        # if anything, we rethrow the exception
        try:
            db = settings.DATABASES.get(db_name, None)
            # non-existent database - exception
            if db == None:
                raise DwException(db_name, "Unknown database %s." % db_name)

            true_sql = tokens.sqltext

            # now that we have a sql, try to replace parameters in it.
            # Parameter looks like ${param_name}
            p = re.compile(u'\$\{[a-z]+[0-9]*\}')
            # get the list of all mentioned params
            lst = p.findall(true_sql)
            # loop through all these variables and try to replace
            # them with params passed to us
            for item in lst:
                stripped_item = item[2:-1]
                # TODO check for default value here
                value = 'None'
                if stripped_item in params:
                    value = params[stripped_item]
                    value = urllib.unquote(value)
                # replace it
                true_sql = true_sql.replace(item, value)

            # get a connection from somewhere
            con = self.get_con(db)
            cur = con.cursor()
            #print true_sql
            cur.execute(true_sql)
            row = cur.fetchone()

            # TODO Now if row is None it means the set is empty
            # in this case we have to generate a stub
            # for now assume the data is present

            # Get columns for labels
            # We need at least two columns
            # first one is x axis, column heading does not matter
            # the second is y axis values, so are 3rd and subsequent ones
            # their headings are the legend if we have to show it
            columns = cur.description
            col_count = len(columns)
            if col_count < 2:
                raise Exception("There should be at least two columns")

            # Now we can have multiple lines here.
            # col 0 is x axis marks. x axis values we count from zero to 1,2 etc.
            # always. If some rows have nulls in this column, it means
            # there will be no points here.
            # But the x value will be counted anyway.
            # The line will continue as usual.
            # on the previous row value. If a non-null is encountered again
            # on subsequent row, the line will start again there.
            # col 2,3 etc. are other lines.
            # Max allowed number for these columns is set to 20
            # For each line - the color and style is set in graph attributes

            # To plot a line for each column we need two arrays:
            # One - all the y values. If null is encountered, the y value is
            # set to the previous value. If first value is null, it is set to
            # miny value. if miny is none, then to zero.
            # Two - masked x points.
            # Two arrays for each column, arranged as a dictionary 'yvalues', 'xmasked'
            # We don't know beforehand how many columns there are,
            # so we create a list to keep them
            col_values_list = list()

            # x coordinates are the same for all cols, so we make one array
            x_values_list = list()

            # collect column titles for cols 1 - ... as labels
            legend_list = list()
            n = 0
            for col in columns:
                # that's column name
                s = col[0]
                if type(s) is str:
                    s = s.decode('utf-8')
                elif type(s) is unicode:
                    s = col[0]
                if n > 0:
                    legend_list.append(s)
                n += 1

            # now deal with line color for each column
            # TODO line colors should be specified exactly
            # like matplotlib understands
            color_converter = ColorConverter()

            final_color_list = list()
            final_fill_list = list()
            for i in range(0, col_count - 1):
                # Just in case the user specified less colors, than columns
                if i < len(line_color_list):
                    color_str = line_color_list[i]
                else:
                    # we have more columns, than colors specified by user
                    color_str = self.DEFAULT_LINE_COLOR

                # The color may well be gibberish
                try:
                    color_rgba = color_converter.to_rgba(color_str)
                except Exception, e:
                    #sys.stderr.write("Color error: %s\n" % color_str)
                    color_str = self.USER_ERROR_LINE_COLOR

                final_color_list.append(color_str)

                # fill color
                if i < len(fill_color_list):
                    fill_str = fill_color_list[i].strip()
                else:
                    # more columns than colors
                    fill_str = 'none'

                try:
                    color_rgba = color_converter.to_rgba(fill_str)
                except Exception, e:
                    #sys.stderr.write("Color error: %s\n" % color_str)
                    fill_str = 'none'

                final_fill_list.append(fill_str)

                # now add a dictionary for each column
                d = {'yvalues': list(), 'xmasked': list()}
                col_values_list.append(d)
예제 #6
0
class SqlInlineStreamer(dwstreamer.BaseDwStreamer):
    """Returns query results just as string separated by spaces"""

    ATTR_DB = 'db'

    TAGNAME = 'sqlinline'

    def factory(self):
        return SqlInlineStreamer()

    def get_tagname(self):
        return self.TAGNAME

    def get_con(self, db):
        con = dwwiki.connectors.get_connection(db)
        return con
#        # TODO work around if connection does not exist
#        #db = settings.DATABASES[dbname]
#
#        #con = MySQLdb.connect(host=db['HOST'], user=db['USER'], passwd=db['PASSWORD'], db=db['DB'],
#        #    charset='utf8',
#        #    cursorclass = MySQLdb.cursors.SSCursor)
#        con = None
#        # TODO different connectors should live in different files
#        if db['ENGINE'] == 'mysql':
#            import mysql.connector
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = mysql.connector.connect(
#                user=db['USER'],
#                passwd=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                connection_timeout=timeout
#            )
#        elif db['ENGINE'] == 'postgresql':
#            import psycopg2
#            # set query timeout as options to psycopg
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = psycopg2.connect(
#                user=db['USER'],
#                password=db['PASSWORD'],
#                host=db['HOST'],
#                database=db['DB'],
#                port=db['PORT'],
#                options="-c statement_timeout=%d" % timeout
#            )
#            con.set_client_encoding('utf-8')
#        elif db['ENGINE'] == 'sqlite':
#            import sqlite3
#            timeout = db.get('QUERY_TIMEOUT', 60) * 1000 # milliseconds
#            con = sqlite3.connect(
#                database=db['DB'],
#                timeout=timeout
#            )
#
#
#            #con.set_client_encoding('unicode')
#
#        return con

# returns sql block parsed into tokens

    def parse_block(self, block_text):
        # make a grammar
        block_start = Literal("{")
        sql_start = Keyword("sqlinline", caseless=True)
        colon = Literal(":")
        sql_end = Literal("}")
        separator = Literal("|")
        block_end = Keyword("{sqlinline}", caseless=True)

        # params
        field_name = Word(alphanums)
        equal_sign = Suppress(Literal("="))

        # whatever value
        field_value = (CharsNotIn("|}"))

        # param name and value
        param_group = Group(field_name + equal_sign + field_value)

        # list of all params
        param_list = delimitedList(param_group, '|')

        # helper
        param_dict = Dict(param_list)

        # sql text
        sql_text = SkipTo(block_end)

        sqldecl = Forward()

        sqldecl << (block_start + sql_start + Optional(colon) +
                    Optional(param_dict) + sql_end +
                    sql_text.setResultsName('sqltext') + block_end)

        block_str = "".join(block_text)
        tokens = sqldecl.parseString(block_str)
        return tokens

    def format_value(self, val):
        tp = type(val).__name__

        if tp == 'str':
            res = val.decode('utf-8')
        elif tp == 'unicode':
            res = unicode(val)
        elif tp in ['Decimal', 'float']:
            res = "{0:0.2f}".format(val)
        elif tp == 'date':
            res = val.strftime("%d.%m.%Y")
        elif tp in ['int', 'long']:
            res = "{0}".format(val)
        else:
            # unknown datatype
            res = unicode(val)

        return res

    def process_block(self, block_text, user, **params):
        """ Yields all query results at once. Only once.
        May return an exception.
        """
        try:
            tokens = self.parse_block(block_text)
            db_name = tokens.get(self.ATTR_DB,
                                 settings.DATABASE_DEFAULT_ALIAS).strip()
            con = None

            db = settings.DATABASES.get(db_name, None)
            if db == None:
                raise DwException(db_name, "Unknown database %s." % db_name)
            true_sql = tokens.sqltext
            p = re.compile(u'\$\{[a-z]+[0-9]*\}')
            lst = p.findall(true_sql)

            for item in lst:
                stripped_item = item[2:-1]
                # TODO check for default value here
                value = 'None'
                if stripped_item in params:
                    value = params[stripped_item]
                    value = urllib.unquote(value)
                # replace it
                true_sql = true_sql.replace(item, value)

            # get a connection from somewhere
            con = self.get_con(db)
            cur = con.cursor()
            cur.execute(true_sql)

            row = cur.fetchone()
            num_rows = 0
            while row is not None:
                if num_rows == db.get('LIMIT_ROWS', 1000):
                    # we are 1 row over limit
                    add_line = htmlconstants.SQL_OVER_LIMIT.format(num_rows)
                    row = None
                    break
                else:
                    retstr = u''
                    for val in row:
                        retstr = retstr + ' ' + self.format_value(val)
                    retstr += "\n"
                    yield retstr
                    num_rows += 1
                    row = cur.fetchone()
            con.close()
            con = None

        except DwException, e:
            if con is not None:
                con.close()
            raise e

        except Exception, e:
            if con is not None:
                con.close()

            # reraise it wrapped in our class
            raise DwException(db_name, "Some source exception", e)
예제 #7
0
    def process_block(self, block_text, user, block_id, **params):
        def int_or_none(val):
            """Returns an integer or none. May pass anything
            """
            try:
                ret = int(val)
            except ValueError:
                ret = None
            return ret

        def int_or_default(val, default=0):
            """ Returns int or a given default
            """
            try:
                ret = int(val)
            except ValueError:
                ret = default

            return ret

        def float_or_default(val, default=0.0):
            """ Returns float or a given default
            """
            try:
                ret = float(val)
            except ValueError:
                ret = default

            return ret

        tokens = self.parse_block(block_text)

        # get our tokens
        db_name = tokens.get(self.ATTR_DB,
                             settings.DATABASE_DEFAULT_ALIAS).strip()

        # chart title. empty by default
        chart_title = tokens.get(self.ATTR_TITLE, '').strip()

        # chart width and height in pixels. Beware, the user
        # inevitably will write anything but valid numbers
        chart_width = int_or_none(tokens.get(self.ATTR_WIDTH, '').strip())
        chart_height = int_or_none(tokens.get(self.ATTR_HEIGHT, '').strip())
        ylabel = tokens.get(self.ATTR_YLABEL, '').strip()
        xlabel = tokens.get(self.ATTR_XLABEL, '').strip()
        font_size = int_or_default(
            tokens.get(self.ATTR_FONT_SIZE, self.DEFAULT_FONT_SIZE))
        if font_size <= 1:
            font_size = self.DEFAULT_FONT_SIZE
        if font_size >= 100:
            font_size = self.DEFAULT_FONT_SIZE

        # grid
        grid = tokens.get(self.ATTR_GRID, 'none').strip().lower()
        if grid not in ('x', 'y', 'both', 'none'):
            grid = 'none'

        # also check for negative numbers or zeroes
        if (chart_width is not None) and (chart_height is not None):
            if (chart_width <= 0) or (chart_height <= 0):
                # both none
                chart_width = None
                chart_height = None
        else:
            # some of them are none
            # set both to none
            chart_width = None
            chart_height = None

        # xtick rotation
        x_tick_rotation = int_or_default(
            tokens.get(self.ATTR_XTICK_ROTATION, self.DEFAULT_XTICK_ROTATION))
        if x_tick_rotation > 90:
            x_tick_rotation = 90

        if x_tick_rotation < -90:
            x_tick_rotation = -90

        # legend
        legend_str = tokens.get(self.ATTR_LEGEND,
                                self.ATTR_VALUE_NO).strip().lower()
        if legend_str not in (self.ATTR_VALUE_NO, self.ATTR_VALUE_YES):
            legend_str = self.ATTR_VALUE_NO

        # legend location
        legend_location = tokens.get(
            self.ATTR_LEGEND_LOCATION,
            self.DEFAULT_LEGEND_LOCATION).strip().lower()
        # this is not necessary, matplotlib does it by himself,
        # yet we check it here to avoid warnings on stdout
        if legend_location not in self.VALID_LEGEND_LOCATIONS:
            legend_location = self.DEFAULT_LEGEND_LOCATION

        # bar width
        bar_width = float_or_default(
            tokens.get(self.ATTR_BAR_WIDTH, self.DEFAULT_BAR_WIDTH),
            self.DEFAULT_BAR_WIDTH)
        if bar_width > self.MAX_BAR_WIDTH:
            bar_width = self.MAX_BAR_WIDTH

        if bar_width < self.MIN_BAR_WIDTH:
            bar_width = self.MIN_BAR_WIDTH

        # stacked
        stacked = tokens.get(self.ATTR_STACKED,
                             self.ATTR_VALUE_NO).strip().lower()
        if stacked not in (self.ATTR_VALUE_YES, self.ATTR_VALUE_NO):
            stacked = self.ATTR_VALUE_NO

        con = None
        # if anything, we rethrow the exception
        try:
            db = settings.DATABASES.get(db_name, None)
            # non-existent database - exception
            if db == None:
                raise DwException(db_name, "Unknown database %s." % db_name)

            true_sql = tokens.sqltext

            # now that we have a sql, try to replace parameters in it.
            # Parameter looks like ${param_name}
            p = re.compile(u'\$\{[a-z]+[0-9]*\}')
            # get the list of all mentioned params
            lst = p.findall(true_sql)
            # loop through all these variables and try to replace
            # them with params passed to us
            for item in lst:
                stripped_item = item[2:-1]
                # TODO check for default value here
                value = 'None'
                if stripped_item in params:
                    value = params[stripped_item]
                    value = urllib.unquote(value)
                # replace it
                true_sql = true_sql.replace(item, value)

            # get a connection from somewhere
            con = self.get_con(db)
            cur = con.cursor()
            cur.execute(true_sql)
            row = cur.fetchone()

            # TODO Now if row is None it means the set is empty
            # in this case we have to generate a stub
            # for now assume the data is present

            # Get columns for labels
            # We need at least two columns
            # first one is x axis, column heading does not matter
            # the second is y axis values
            # the third one is the color of values
            # if there is no third column,  use default color
            # which is gray
            columns = cur.description
            col_count = len(columns)
            if col_count < 2:
                raise DwException(db_name,
                                  "There should be at least two columns")

            # second column should be a number. Don't test it here.
            # we will try to test each value

            # every odd column is value
            # every even column is color

            # Now traverse all the rows
            # x_tick_marks is col 0 values
            x_tick_marks = list()

            # y_values is an array of bar heights
            # stored in the second column, fourth etc. columns

            # this is array of arrays
            y_values_array = list()
            y_colors_array = list()
            for i in range(1, col_count):
                is_value = i % 2 == 1
                is_color = i % 2 == 0
                if is_value is True:
                    y_values = list()
                    y_values_array.append(y_values)
                    # add color array in any case
                    y_colors = list()
                    y_colors_array.append(y_colors)
                #if is_color is True:
                #y_colors_array.append(y_colors)

            #y_values = list()

            # bar_colors is an array of bar colors
            # stored in the third column
            # if there is no third column,
            # assume the color is grey
            #y_colors = list()

            # Now traverse. If row count is too big,
            # just stop where it exceeds the maximum allowed number
            # which we set to 100 for now
            curr_bar = 0

            color_converter = ColorConverter()

            while row is not None:
                # tick mark as unicode
                mark = row[0]
                if mark is None:
                    mark = ''
                if type(mark) is str:
                    mark = mark.decode('utf-8')
                elif type(mark) is unicode:
                    pass
                else:
                    # TODO maybe a number
                    # we need to convert it to a string
                    # according to it's type (integer, decimal, float)
                    # date, time, datetime
                    mark = unicode(mark)

                x_tick_marks.append(mark)

                # collect values and colors
                for i in range(1, col_count):
                    is_value = i % 2 == 1
                    is_color = i % 2 == 0

                    # value itself. plot does not support Decimal,
                    # need to convert to float
                    if is_value is True:
                        value_index = i / 2
                        y_values = y_values_array[value_index]

                        value = row[i]
                        tp = type(value).__name__
                        if tp is 'Decimal':
                            value = float(value)

                        # all nulls are replaced with zeroes
                        if value is None:
                            value = 0.0

                        y_values.append(value)

                        # now colors
                        color_col = i + 1

                        y_colors = y_colors_array[value_index]

                        # color himself +1 col if exists
                        color_str = None

                        if color_col < len(row):
                            color_str = self.make_unicode_or_none(row[i + 1])

                        if color_str is None:
                            color_str = self.DEFAULT_BAR_COLOR

                        # Now try to make a color. If it's unsuccessful,
                        # it returns an exception
                        try:
                            color_rgba = color_converter.to_rgba(color_str)
                        except Exception, e:
                            #sys.stderr.write("Color error: %s\n" % color_str)
                            color_rgba = color_converter.to_rgba(
                                self.USER_ERROR_BAR_COLOR)

                        # Hopefully, color_rgba is now set
                        # to a tuple of R, G, B and Alpha
                        y_colors.append(color_rgba)

                # increment row counter
                curr_bar += 1
                if curr_bar >= self.MAX_BARS:
                    break
                else:
                    row = cur.fetchone()

            # Now we have collected all the values, marks and colors

            con.close()
            con = None

            # Build a graph

            # 80 dots per inch. So 7 x 4 inches will make 560 x 320 pixels
            if (chart_width > 0) and (chart_height > 0):
                fig = plt.figure(figsize=[chart_width / 80, chart_height / 80],
                                 dpi=80)
            else:
                fig = plt.figure()

            ax = fig.add_subplot(111)

            # data size
            #N = len(curr_bar) # number of bar groups - scale units for x axis = number of query rows

            # arrange N between 0 and N-1. Returns an array
            ind = np.arange(curr_bar)  # distribute equally

            # todo expose bar width
            if stacked == self.ATTR_VALUE_NO:
                width = bar_width / len(
                    y_values_array)  # width of the bars (measures?)
            else:
                width = bar_width

            min_value = 0
            max_value = 0

            for y_values in y_values_array:
                # remember it may be negative
                temp_min_value = min(y_values)
                min_value = min(temp_min_value, min_value)
                if min_value > 0:
                    min_value = 0

                temp_max_value = max(y_values)
                max_value = max(temp_max_value, max_value)
                if max_value < 0:
                    max_value = 0

            # now add additional 10% of min-max range at the top
            value_range = max_value - min_value
            top_y_axis_margin = value_range * self.DEFAULT_TOP_Y_AXIS_MARGIN

            # no margin for bottom
            bottom_y_axis_margin = 0

            # if we have negative bars, set margin below too
            if min_value < 0:
                bottom_y_axis_margin = top_y_axis_margin

            # here we set final max and min y axis values to account for our margins
            max_y_axis_value = max_value + top_y_axis_margin
            min_y_axis_value = min_value - bottom_y_axis_margin

            # cycle through values and draw bars
            for i in range(len(y_values_array)):
                y_values = y_values_array[i]
                y_colors = y_colors_array[i]

                # bar labels
                # get a description
                # it's in columns 1,3,5 etc.
                # i=0 => col=1
                # i=1 => col=3
                col_index = i * 2 + 1
                col = columns[col_index]
                bar_label = col[0]
                if type(col[0]) is str:
                    bar_label = col[0].decode('utf-8')
                elif type(col[0]) is unicode:
                    bar_label = col[0]

                # The Drawing
                if stacked == self.ATTR_VALUE_NO:
                    rects1 = ax.bar(ind + i * width,
                                    y_values,
                                    width,
                                    color=y_colors,
                                    label=bar_label)
                else:
                    rects1 = ax.bar(ind,
                                    y_values,
                                    width,
                                    color=y_colors,
                                    label=bar_label)

            ax.set_xlim(-1 + bar_width, len(ind))

            ax.set_ylim(min_y_axis_value, max_y_axis_value)

            # draw horizontal grid lines
            if grid <> 'none':
                ax.grid(which='major', axis=grid)

            if ylabel <> '':
                plt.ylabel(ylabel, fontsize=font_size)

            if xlabel <> '':
                plt.xlabel(xlabel, fontsize=font_size)

            ax.set_title(chart_title, fontsize=font_size)

            # font for y major ticks
            for tick in ax.yaxis.get_major_ticks():
                tick.label.set_fontsize(font_size)

            # ticks on top and bottom
            ax.set_xticks(ind + bar_width / 2)

            #canvas = FigureCanvasAgg(fig)

            x_tick_labels = ax.set_xticklabels(x_tick_marks)

            plt.setp(x_tick_labels,
                     rotation=x_tick_rotation,
                     fontsize=font_size)

            # legend
            if legend_str == self.ATTR_VALUE_YES:
                ax.legend(loc=legend_location, prop={'size': font_size})

            plt.tight_layout()

            tempfiledir = cherrypy.config['dwwiki.tempfiledir']
            tempfile.tempdir = tempfiledir
            try:
                f = tempfile.NamedTemporaryFile(delete=False)
                # print graph to a temporary file
                # TODO we assume it prints png. Yet I don't know
                # why. Is it the default format?
                #canvas.print_figure(f)
                #canvas.print_png(f)
                fig.savefig(f)
                f.close()

                # Now all we need is to encode the file in base64,
                # then serve it as an <img>
                input_file = open(f.name, 'rb')
                output_file = cStringIO.StringIO()
                base64.encode(input_file, output_file)
                # we have it as a png string
                #yield '<img alt="graph" src="data:image/png;base64,'
                yield '<img alt="graph" src="data:image/png;base64,'
                yield output_file.getvalue()
                yield "\"/>\n"
                output_file.close()
                input_file.close()
            finally:
                os.remove(f.name)