| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ |
|---|
| 3 | = Watchlist Plugin for Trac = |
|---|
| 4 | Plugin Website: http://trac-hacks.org/wiki/WatchlistPlugin |
|---|
| 5 | Trac website: http://trac.edgewall.org/ |
|---|
| 6 | |
|---|
| 7 | Copyright (c) 2008-2010 by Martin Scharrer <martin@scharrer-online.de> |
|---|
| 8 | All rights reserved. |
|---|
| 9 | |
|---|
| 10 | The i18n support was added by Steffen Hoffmann <hoff.st@web.de>. |
|---|
| 11 | |
|---|
| 12 | This program is free software: you can redistribute it and/or modify |
|---|
| 13 | it under the terms of the GNU General Public License as published by |
|---|
| 14 | the Free Software Foundation, either version 3 of the License, or |
|---|
| 15 | (at your option) any later version. |
|---|
| 16 | |
|---|
| 17 | This program is distributed in the hope that it will be useful, |
|---|
| 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 20 | GNU General Public License for more details. |
|---|
| 21 | |
|---|
| 22 | For a copy of the GNU General Public License see |
|---|
| 23 | <http://www.gnu.org/licenses/>. |
|---|
| 24 | |
|---|
| 25 | $Id: util.py 15264 2016-02-11 04:22:34Z rjollos $ |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | __url__ = ur"$URL: //trac-hacks.org/svn/watchlistplugin/0.12/tracwatchlist/util.py $"[6:-2] |
|---|
| 29 | __author__ = ur"$Author: rjollos $"[9:-2] |
|---|
| 30 | __revision__ = int("0" + ur"$Rev: 15264 $"[6:-2].strip('M')) |
|---|
| 31 | __date__ = ur"$Date: 2016-02-11 04:22:34 +0000 (Thu, 11 Feb 2016) $"[7:-2] |
|---|
| 32 | |
|---|
| 33 | from trac.core import * |
|---|
| 34 | from genshi.builder import tag, Markup |
|---|
| 35 | from trac.util.datefmt import datetime, utc |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | # Try to use babels format_datetime to localise date-times if possible. |
|---|
| 39 | # A fall back to tracs implementation strips the unsupported `locale` argument. |
|---|
| 40 | from trac.util.datefmt import format_datetime as trac_format_datetime |
|---|
| 41 | try: |
|---|
| 42 | from babel.dates import format_datetime, LC_TIME |
|---|
| 43 | except ImportError: |
|---|
| 44 | LC_TIME = None |
|---|
| 45 | def format_datetime(t=None, format='%x %X', tzinfo=None, locale=None): |
|---|
| 46 | return trac_format_datetime(t, format, tzinfo) |
|---|
| 47 | |
|---|
| 48 | |
|---|
| 49 | def ldml_patterns( ldml_pattern ): |
|---|
| 50 | """Takes a LDML date/time format pattern and breaks it down into its |
|---|
| 51 | elements""" |
|---|
| 52 | last = None |
|---|
| 53 | num = 1 |
|---|
| 54 | patterns = [] |
|---|
| 55 | verbatim = False |
|---|
| 56 | verbtext = '' |
|---|
| 57 | for s in ldml_pattern: |
|---|
| 58 | if verbatim: |
|---|
| 59 | if last == "'": |
|---|
| 60 | if s == "'": |
|---|
| 61 | # Inside quote (quote character already added) |
|---|
| 62 | last = None |
|---|
| 63 | continue |
|---|
| 64 | else: |
|---|
| 65 | # Last character ended verbatim |
|---|
| 66 | if verbtext != "'": |
|---|
| 67 | verbtext = verbtext[:-1] |
|---|
| 68 | patterns.append( [verbtext] ) |
|---|
| 69 | last = None |
|---|
| 70 | verbatim = False |
|---|
| 71 | else: |
|---|
| 72 | verbtext += s |
|---|
| 73 | last = s |
|---|
| 74 | continue |
|---|
| 75 | if s == "'": |
|---|
| 76 | verbatim = True |
|---|
| 77 | verbtext = '' |
|---|
| 78 | if not last is None: |
|---|
| 79 | patterns.append( last * num ) |
|---|
| 80 | num = 1 |
|---|
| 81 | last = None |
|---|
| 82 | elif s == last: |
|---|
| 83 | num+=1 |
|---|
| 84 | else: |
|---|
| 85 | if not last is None: |
|---|
| 86 | patterns.append( last * num ) |
|---|
| 87 | num = 1 |
|---|
| 88 | last = s |
|---|
| 89 | # Flush buffers |
|---|
| 90 | if verbatim: |
|---|
| 91 | if last == "'" and verbtext: |
|---|
| 92 | verbtext = verbtext[:-1] |
|---|
| 93 | patterns.append( [verbtext ] ) |
|---|
| 94 | else: |
|---|
| 95 | if not last is None: |
|---|
| 96 | patterns.append( last * num ) |
|---|
| 97 | return patterns |
|---|
| 98 | |
|---|
| 99 | _PATTERN_TRANSLATION = { |
|---|
| 100 | 'h' : '%l', |
|---|
| 101 | 'hh' : '%h', |
|---|
| 102 | 'H' : '%k', |
|---|
| 103 | 'HH' : '%H', |
|---|
| 104 | 'K' : '%l', # fuzzy |
|---|
| 105 | 'KK' : '%h', # fuzzy |
|---|
| 106 | 'k' : '%k', # fuzzy |
|---|
| 107 | 'kk' : '%H', # fuzzy |
|---|
| 108 | 'j' : '?', |
|---|
| 109 | 'jj' : '?', |
|---|
| 110 | 'a' : '%p', |
|---|
| 111 | 'm' : '%i', # fuzzy |
|---|
| 112 | 'mm' : '%i', |
|---|
| 113 | 's' : '%S', |
|---|
| 114 | 'ss' : '%S', |
|---|
| 115 | 'S' : '?', |
|---|
| 116 | 'A' : '?', |
|---|
| 117 | 'z' : '%@', |
|---|
| 118 | 'zz' : '%@', |
|---|
| 119 | 'zzz' : '%@', |
|---|
| 120 | 'zzzz' : '%@', # fuzzy |
|---|
| 121 | 'Z' : '%+', |
|---|
| 122 | 'ZZ' : '%+', |
|---|
| 123 | 'ZZZ' : '%+', |
|---|
| 124 | 'ZZZZ' : 'GMT%+', |
|---|
| 125 | 'v' : '%@', # fuzzy |
|---|
| 126 | 'vvvv' : '%@', # fuzzy |
|---|
| 127 | 'V' : '%@', # fuzzy |
|---|
| 128 | 'VVVV' : '%@', # fuzzy |
|---|
| 129 | 'Q' : '?', # quarter |
|---|
| 130 | 'q' : '?', # quarter |
|---|
| 131 | 'yyyy' : '%Y', |
|---|
| 132 | 'yy' : '%y', |
|---|
| 133 | 'M' : '%c', |
|---|
| 134 | 'MM' : '%m', |
|---|
| 135 | 'MMM' : '%b', |
|---|
| 136 | 'MMMM' : '%M', |
|---|
| 137 | 'MMMMM' : '?', |
|---|
| 138 | 'L' : '%c', |
|---|
| 139 | 'LL' : '%m', |
|---|
| 140 | 'LLL' : '%b', |
|---|
| 141 | 'LLLL' : '%M', |
|---|
| 142 | 'LLLLL' : '?', |
|---|
| 143 | 'l' : '*', |
|---|
| 144 | 'w' : '?', # week of the year |
|---|
| 145 | 'ww' : '??', |
|---|
| 146 | 'W' : '?', # week of the month |
|---|
| 147 | 'd' : '%e', |
|---|
| 148 | 'dd' : '%d', |
|---|
| 149 | 'D' : '?', |
|---|
| 150 | 'DD' : '??', |
|---|
| 151 | 'DDD' : '???', |
|---|
| 152 | 'F' : '%w', |
|---|
| 153 | 'g' : '?', |
|---|
| 154 | 'E' : '%a', |
|---|
| 155 | 'EE' : '%a', |
|---|
| 156 | 'EEE' : '%a', |
|---|
| 157 | 'EEEE' : '%W', |
|---|
| 158 | 'EEEEE' : '?', |
|---|
| 159 | 'e' : '%w', |
|---|
| 160 | 'ee' : '%w', |
|---|
| 161 | 'eee' : '%a', |
|---|
| 162 | 'eeee' : '%W', |
|---|
| 163 | 'eeeee' : '?', |
|---|
| 164 | 'c' : '%w', |
|---|
| 165 | 'cc' : '%w', |
|---|
| 166 | 'ccc' : '%a', |
|---|
| 167 | 'cccc' : '%W', |
|---|
| 168 | 'ccccc' : '?', |
|---|
| 169 | 'G' : '%E', # Era abbreviation* |
|---|
| 170 | 'GG' : '%E', # Era abbreviation* |
|---|
| 171 | 'GGG' : '%E', # Era abbreviation* |
|---|
| 172 | 'GGGG' : '%E', # Era abbreviation* |
|---|
| 173 | 'GGGGG' : '%E', # Era abbreviation* |
|---|
| 174 | } |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | |
|---|
| 178 | def convert_LDML_to_MySQL( ldml_pattern ): |
|---|
| 179 | """Converts from LDML date/time format patterns to the one used by MySQL. |
|---|
| 180 | That is the same format used by the Any+Time datepicker currently used. |
|---|
| 181 | See http://www.ama3.com/anytime/#AnyTime.Converter.format |
|---|
| 182 | and http://unicode.org/reports/tr35/#Date_Format_Patterns . |
|---|
| 183 | """ |
|---|
| 184 | import re |
|---|
| 185 | result = '' |
|---|
| 186 | for pattern in ldml_patterns(ldml_pattern): |
|---|
| 187 | if isinstance(pattern,list): |
|---|
| 188 | # Verbatim |
|---|
| 189 | result += ''.join(pattern).replace('%','%%') |
|---|
| 190 | else: |
|---|
| 191 | tp = _PATTERN_TRANSLATION.get(pattern,None) |
|---|
| 192 | if tp is None: # Replace unknown letters with '?' |
|---|
| 193 | tp = re.sub(r'[A-Za-z]','?', pattern) |
|---|
| 194 | result += tp |
|---|
| 195 | return result |
|---|
| 196 | |
|---|
| 197 | |
|---|
| 198 | try: |
|---|
| 199 | from babel.dates import get_datetime_format, get_date_format, get_time_format |
|---|
| 200 | def datetime_format(format='medium', locale=LC_TIME): |
|---|
| 201 | time_format = unicode(get_time_format(format, locale)) |
|---|
| 202 | date_format = unicode(get_date_format(format, locale)) |
|---|
| 203 | return convert_LDML_to_MySQL( get_datetime_format(format, locale)\ |
|---|
| 204 | .replace('{0}', time_format)\ |
|---|
| 205 | .replace('{1}', date_format) ) |
|---|
| 206 | except ImportError: |
|---|
| 207 | def datetime_format(format='medium', locale=LC_TIME): |
|---|
| 208 | return u"%Y-%m-%d %H:%i:%s" |
|---|
| 209 | |
|---|
| 210 | try: |
|---|
| 211 | from trac.util.datefmt import to_utimestamp |
|---|
| 212 | def current_timestamp(): |
|---|
| 213 | return to_utimestamp( datetime.now(utc) ) |
|---|
| 214 | except ImportError: |
|---|
| 215 | from trac.util.datefmt import to_timestamp |
|---|
| 216 | def current_timestamp(): |
|---|
| 217 | return to_timestamp( datetime.now(utc) ) |
|---|
| 218 | |
|---|
| 219 | |
|---|
| 220 | def moreless(text, length): |
|---|
| 221 | """Turns `text` into HTML code where everything behind `length` can be uncovered using a ''show more'' link |
|---|
| 222 | and later covered again with a ''show less'' link.""" |
|---|
| 223 | try: |
|---|
| 224 | if len(text) <= length: |
|---|
| 225 | return tag(text) |
|---|
| 226 | except: |
|---|
| 227 | return tag(text) |
|---|
| 228 | return tag(tag.span(text[:length]),tag.a(' [', tag.strong(Markup('…')), ']', class_="more"), |
|---|
| 229 | tag.span(text[length:],class_="moretext"),tag.a(' [', tag.strong('-'), ']', class_="less")) |
|---|
| 230 | |
|---|
| 231 | |
|---|
| 232 | def ensure_iter( var ): |
|---|
| 233 | """Ensures that variable is iterable. If it's not by itself, return it |
|---|
| 234 | as an element of a tuple""" |
|---|
| 235 | if getattr(var, '__iter__', False): |
|---|
| 236 | return var |
|---|
| 237 | return (var,) |
|---|
| 238 | |
|---|
| 239 | |
|---|
| 240 | def ensure_tuple( var ): |
|---|
| 241 | """Ensures that variable is a tuple, even if its a scalar""" |
|---|
| 242 | if getattr(var, '__iter__', False): |
|---|
| 243 | return tuple(var) |
|---|
| 244 | return (var,) |
|---|
| 245 | |
|---|
| 246 | |
|---|
| 247 | def ensure_string( var, sep=u',' ): |
|---|
| 248 | """Ensures that variable is a string""" |
|---|
| 249 | if getattr(var, '__iter__', False): |
|---|
| 250 | return unicode(sep.join(var)) |
|---|
| 251 | else: |
|---|
| 252 | return var |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | def decode_range( str ): |
|---|
| 256 | """Decodes given string with integer ranges like `a-b,c-d` and yields a list |
|---|
| 257 | of tuples: [(a,b),(c,d)] in this ranges.""" |
|---|
| 258 | for irange in unicode(str).split(','): |
|---|
| 259 | irange = irange.strip() |
|---|
| 260 | try: |
|---|
| 261 | index = irange.index('-') |
|---|
| 262 | except: |
|---|
| 263 | if irange == '*': |
|---|
| 264 | a, b = 0, None |
|---|
| 265 | else: |
|---|
| 266 | a = b = irange |
|---|
| 267 | else: |
|---|
| 268 | b = irange[index+1:] |
|---|
| 269 | a = irange[:index] |
|---|
| 270 | try: |
|---|
| 271 | a = int(a) |
|---|
| 272 | except: |
|---|
| 273 | a = None |
|---|
| 274 | try: |
|---|
| 275 | b = int(b) |
|---|
| 276 | except: |
|---|
| 277 | b = None |
|---|
| 278 | if not (a is None and b is None): |
|---|
| 279 | yield (a,b) |
|---|
| 280 | |
|---|
| 281 | |
|---|
| 282 | def decode_range_sql( str ): |
|---|
| 283 | """Decodes given string with ranges like `a-b,c-d` and returns a SQL |
|---|
| 284 | command fragment to much this ranges.""" |
|---|
| 285 | cmd = [] |
|---|
| 286 | for (a,b) in decode_range( str ): |
|---|
| 287 | if a is None: |
|---|
| 288 | if b is None: |
|---|
| 289 | continue |
|---|
| 290 | cmd.append( ' ( %%(var)s <= %i ) ' % (b) ) |
|---|
| 291 | elif b is None: |
|---|
| 292 | cmd.append( ' ( %%(var)s >= %i ) ' % (a) ) |
|---|
| 293 | else: |
|---|
| 294 | cmd.append( ' ( %%(var)s BETWEEN %i AND %i ) ' % (a,b) ) |
|---|
| 295 | return ' OR '.join(cmd) |
|---|
| 296 | |
|---|
| 297 | |
|---|
| 298 | def convert_to_sql_wildcards( pattern, sql_escape = '|' ): |
|---|
| 299 | r"""Converts wildcards '*' and '?' to SQL versions '%' and '_'. |
|---|
| 300 | A state machine is used to allow for using the backslash to |
|---|
| 301 | escape this characters: |
|---|
| 302 | t\e\s\t -> test |
|---|
| 303 | test -> test |
|---|
| 304 | test* -> test% |
|---|
| 305 | test\* -> test* |
|---|
| 306 | test\\* -> test\% |
|---|
| 307 | test\\\* -> test\* |
|---|
| 308 | test\\\\* -> test\\% |
|---|
| 309 | test\\\\\* -> test\\* |
|---|
| 310 | % -> \% |
|---|
| 311 | _ -> \_ |
|---|
| 312 | test_test% -> test\_test\% |
|---|
| 313 | test__test% -> test\_\_test\% |
|---|
| 314 | test\_test\% -> test\_test\% |
|---|
| 315 | test\\_test\\% -> test\\_test\\% |
|---|
| 316 | test\\\_test\\\% -> test\\_test\\% |
|---|
| 317 | test\\\\_test\\\% -> test\\\_test\\% |
|---|
| 318 | """ |
|---|
| 319 | pat = '' |
|---|
| 320 | esc = False |
|---|
| 321 | for p in pattern: |
|---|
| 322 | if not p in '*?\\': |
|---|
| 323 | esc = False |
|---|
| 324 | if p in '%_': |
|---|
| 325 | # Escaped for SQL |
|---|
| 326 | pat += sql_escape |
|---|
| 327 | pat += p |
|---|
| 328 | else: |
|---|
| 329 | if esc: |
|---|
| 330 | esc = False |
|---|
| 331 | pat += p |
|---|
| 332 | else: |
|---|
| 333 | if p == '\\': |
|---|
| 334 | esc = True |
|---|
| 335 | elif p == '*': |
|---|
| 336 | pat += '%' |
|---|
| 337 | elif p == '?': |
|---|
| 338 | pat += '_' |
|---|
| 339 | return pat |
|---|
| 340 | |
|---|
| 341 | # EOF |
|---|