Package madrona :: Package common :: Module utils
[hide private]

Source Code for Module madrona.common.utils

  1  import os 
  2  from django.contrib.gis.geos import Point, LinearRing, fromstr 
  3  from math import pi, sin, tan, sqrt, pow 
  4  from django.conf import settings 
  5  from django.db import connection 
  6  from django.core.cache import cache 
  7  from madrona.common.models import KmlCache 
  8  import zipfile 
  9  import re 
 10  import logging 
 11  import inspect 
 12  import tempfile 
 13   
14 -def get_logger(caller_name=None):
15 try: 16 fh = open(settings.LOG_FILE,'a') 17 logfile = settings.LOG_FILE 18 except: 19 import warnings 20 warnings.warn(" NOTICE: settings.LOG_FILE not specified or is not writeable; logging to stderr instead\n") 21 logfile = None 22 23 try: 24 level = settings.LOG_LEVEL 25 except AttributeError: 26 if settings.DEBUG: 27 level = logging.DEBUG 28 else: 29 level = logging.WARNING 30 31 format = '%(asctime)s %(name)s %(levelname)s %(message)s' 32 if logfile: 33 logging.basicConfig(level=level, format=format, filename=logfile) 34 else: 35 logging.basicConfig(level=level, format=format) 36 37 if not caller_name: 38 caller = inspect.currentframe().f_back 39 caller_name = caller.f_globals['__name__'] 40 41 logger = logging.getLogger(caller_name) 42 43 if logfile and settings.DEBUG: 44 import sys 45 strm_out = logging.StreamHandler(sys.__stderr__) 46 logger.addHandler(strm_out) 47 48 return logger
49 50 log = get_logger() 51
52 -def KmlWrap(string):
53 return '<?xml version="1.0" encoding="UTF-8"?> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">' + string + '</kml>'
54 55
56 -def LookAtKml(geometry):
57 lookAtParams = ComputeLookAt(geometry) 58 return '<LookAt><latitude>%f</latitude><longitude>%f</longitude><range>%f</range><tilt>%f</tilt><heading>%f</heading><altitudeMode>clampToGround</altitudeMode></LookAt>' % (lookAtParams['latitude'], lookAtParams['longitude'], lookAtParams['range'], lookAtParams['tilt'], lookAtParams['heading'])
59
60 -def LargestPolyFromMulti(geom):
61 """ takes a polygon or a multipolygon geometry and returns only the largest polygon geometry""" 62 if geom.num_geom > 1: 63 largest_area = 0.0 64 for g in geom: # find the largest polygon in the multi polygon 65 if g.area > largest_area: 66 largest_geom = g 67 largest_area = g.area 68 else: 69 largest_geom = geom 70 return largest_geom 71
72 -def LargestLineFromMulti(geom):
73 """ takes a line or a multiline geometry and returns only the longest line geometry""" 74 if geom.num_geom > 1: 75 largest_length = 0.0 76 for g in geom: # find the largest polygon in the multi polygon 77 if g.length > largest_length: 78 largest_geom = g 79 largest_length = g.length 80 else: 81 largest_geom = geom 82 return largest_geom 83
84 -def angle(pnt1,pnt2,pnt3):
85 """ 86 Return the angle in radians between line(pnt2,pnt1) and line(pnt2,pnt3) 87 """ 88 cursor = connection.cursor() 89 if pnt1.srid: 90 query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i) ) - ST_Azimuth(ST_PointFromText(\'%s\',%i), ST_PointFromText(\'%s\',%i)) )" % (pnt2.wkt,pnt2.srid,pnt1.wkt,pnt1.srid,pnt2.wkt,pnt2.srid,pnt3.wkt,pnt3.srid) 91 else: 92 query = "SELECT abs(ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\') ) - ST_Azimuth(ST_PointFromText(\'%s\'), ST_PointFromText(\'%s\')) )" % (pnt2.wkt,pnt1.wkt,pnt2.wkt,pnt3.wkt) 93 #print query 94 cursor.execute(query) 95 row = cursor.fetchone() 96 return row[0]
97
98 -def angle_degrees(pnt1,pnt2,pnt3):
99 """ 100 Return the angle in degrees between line(pnt2,pnt1) and line(pnt2,pnt3) 101 """ 102 rads = angle(pnt1,pnt2,pnt3) 103 return rads * (180 / pi)
104
105 -def spike_ring_indecies(line_ring,threshold=0.01):
106 """ 107 Returns a list of point indexes if ring contains spikes (angles of less than threshold degrees). 108 Otherwise, an empty list. 109 """ 110 radian_thresh = threshold * (pi / 180) 111 spike_indecies = [] 112 for i,pnt in enumerate(line_ring.coords): 113 if(i == 0 and line_ring.num_points > 3): # The first point ...which also equals the last point 114 p1_coords = line_ring.coords[len(line_ring.coords) - 2] 115 elif(i == line_ring.num_points - 1): # The first and last point are the same in a line ring so we're done 116 break 117 else: 118 p1_coords = line_ring.coords[i - 1] 119 120 # set up the points for the angle test. 121 p1_str = 'POINT (%f %f), %i' % (p1_coords[0], p1_coords[1], settings.GEOMETRY_DB_SRID) 122 p1 = fromstr(p1_str) 123 p2_str = 'POINT (%f %f), %i' % (pnt[0],pnt[1],settings.GEOMETRY_DB_SRID) 124 p2 = fromstr(p2_str) 125 p3_coords = line_ring.coords[i + 1] 126 p3_str = 'POINT (%f %f), %i' % (p3_coords[0], p3_coords[1], settings.GEOMETRY_DB_SRID) 127 p3 = fromstr(p3_str) 128 if(angle(p1,p2,p3) <= radian_thresh): 129 spike_indecies.append(i) 130 131 return spike_indecies
132
133 -def remove_spikes(poly,threshold=0.01):
134 """ 135 Looks for spikes (angles < threshold degrees) in the polygons exterior ring. If there are spikes, 136 they will be removed and a polygon (without spikes) will be returned. If no spikes are found, method 137 will return original geometry. 138 139 NOTE: This method does not examine or fix interior rings. So far those haven't seemed to have been a problem. 140 """ 141 line_ring = poly.exterior_ring 142 spike_indecies = spike_ring_indecies(line_ring,threshold=threshold) 143 if(spike_indecies): 144 for i,org_index in enumerate(spike_indecies): 145 if(org_index == 0): # special case, must remove first and last point, and add end point that overlaps new first point 146 # get the list of points 147 pnts = list(line_ring.coords) 148 # remove the first point 149 pnts.remove(pnts[0]) 150 # remove the last point 151 pnts.remove(pnts[-1]) 152 # append a copy of the new first point (old second point) onto the end so it makes a closed ring 153 pnts.append(pnts[0]) 154 # replace the old line ring 155 line_ring = LinearRing(pnts) 156 else: 157 line_ring.remove(line_ring.coords[org_index]) 158 poly.exterior_ring = line_ring 159 return poly
160
161 -def clean_geometry(geom):
162 """Send a geometry to the cleanGeometry stored procedure and get the cleaned geom back.""" 163 cursor = connection.cursor() 164 query = "select cleangeometry(st_geomfromewkt(\'%s\')) as geometry" % geom.ewkt 165 cursor.execute(query) 166 row = cursor.fetchone() 167 newgeom = fromstr(row[0]) 168 # sometimes, clean returns a multipolygon 169 geometry = LargestPolyFromMulti(newgeom) 170 171 if not geometry.valid or (geometry.geom_type != 'Point' and geometry.num_coords < 2): 172 raise Exception("I can't clean this geometry. Dirty, filthy geometry. This geometry should be ashamed.") 173 else: 174 return geometry
175 176 # transforms the geometry to the given srid, checks it's validity and 177 # cleans it if necessary, transforms it back into the original srid and 178 # cleans again if needed before returning 179 # Note, it does not scrub the geometry before transforming, so if needed 180 # call check_validity(geo, geo.srid) first.
181 -def ensure_clean(geo, srid):
182 old_srid = geo.srid 183 if geo.srid is not srid: 184 geo.transform(srid) 185 geo = clean_geometry(geo) 186 if not geo.valid: 187 raise Exception("ensure_clean could not produce a valid geometry.") 188 if geo.srid is not old_srid: 189 geo.transform(old_srid) 190 geo = clean_geometry(geo) 191 if not geo.valid: 192 raise Exception("ensure_clean could not produce a valid geometry.") 193 return geo
194
195 -def ComputeLookAt(geometry):
196 197 lookAtParams = {} 198 199 DEGREES = pi / 180.0 200 EARTH_RADIUS = 6378137.0 201 202 trans_geom = geometry.clone() 203 trans_geom.transform(settings.GEOMETRY_DB_SRID) # assuming this is an equal area projection measure in meters 204 205 w = trans_geom.extent[0] 206 s = trans_geom.extent[1] 207 e = trans_geom.extent[2] 208 n = trans_geom.extent[3] 209 210 center_lon = trans_geom.centroid.y 211 center_lat = trans_geom.centroid.x 212 213 lngSpan = (Point(w, center_lat)).distance(Point(e, center_lat)) 214 latSpan = (Point(center_lon, n)).distance(Point(center_lon, s)) 215 216 aspectRatio = 1.0 217 218 PAD_FACTOR = 1.5 # add 50% to the computed range for padding 219 220 aspectUse = max(aspectRatio, min((lngSpan / latSpan),1.0)) 221 alpha = (45.0 / (aspectUse + 0.4) - 2.0) * DEGREES # computed experimentally; 222 223 # create LookAt using distance formula 224 if lngSpan > latSpan: 225 # polygon is wide 226 beta = min(DEGREES * 90.0, alpha + lngSpan / 2.0 / EARTH_RADIUS) 227 else: 228 # polygon is taller 229 beta = min(DEGREES * 90.0, alpha + latSpan / 2.0 / EARTH_RADIUS) 230 231 lookAtParams['range'] = PAD_FACTOR * EARTH_RADIUS * (sin(beta) * 232 sqrt(1.0 / pow(tan(alpha),2.0) + 1.0) - 1.0) 233 234 trans_geom.transform(4326) 235 236 lookAtParams['latitude'] = trans_geom.centroid.y 237 lookAtParams['longitude'] = trans_geom.centroid.x 238 lookAtParams['tilt'] = 0 239 lookAtParams['heading'] = 0 240 241 return lookAtParams
242
243 -def get_class(path):
244 from django.utils import importlib 245 module,dot,klass = path.rpartition('.') 246 m = importlib.import_module(module) 247 return m.__getattribute__(klass)
248
249 -def kml_errors(kmlstring):
250 from madrona.common import feedvalidator 251 from madrona.common.feedvalidator import compatibility 252 events = feedvalidator.validateString(kmlstring, firstOccurrenceOnly=1)['loggedEvents'] 253 254 # Three levels of compatibility 255 # "A" is most basic level 256 # "AA" mimics online validator 257 # "AAA" is experimental; these rules WILL change or disappear in future versions 258 filterFunc = getattr(compatibility, "AA") 259 events = filterFunc(events) 260 261 # there are a few annoyances with feedvalidator; specifically it doesn't recognize 262 # KML ExtendedData element 263 # or our custom 'mm' namespance 264 # or our custom atom link relation 265 # or space-delimited Icon states 266 # so we ignore all related events 267 events = [x for x in events if not ( 268 (isinstance(x,feedvalidator.logging.UndefinedElement) 269 and x.params['element'] == u'ExtendedData') or 270 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 271 and x.params['value'] == u'madrona.update_form') or 272 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 273 and x.params['value'] == u'madrona.create_form') or 274 (isinstance(x,feedvalidator.logging.UnknownNamespace) 275 and x.params['namespace'] == u'http://madrona.org') or 276 (isinstance(x,feedvalidator.logging.UnknownNamespace) 277 and x.params['namespace'] == u'http://www.google.com/kml/ext/2.2') or 278 (isinstance(x,feedvalidator.logging.InvalidItemIconState) 279 and x.params['element'] == u'state' and ' ' in x.params['value']) or 280 (isinstance(x,feedvalidator.logging.UnregisteredAtomLinkRel) 281 and x.params['element'] == u'atom:link' and 'workspace' in x.params['value']) 282 )] 283 284 from madrona.common.feedvalidator.formatter.text_plain import Formatter 285 output = Formatter(events) 286 287 if output: 288 errors = [] 289 for i in range(len(output)): 290 errors.append((events[i],events[i].params,output[i],kmlstring.splitlines()[events[i].params['backupline']])) 291 return errors 292 else: 293 return None
294
295 -def hex8_to_rgba(hex8):
296 """ 297 Takes an 8 digit hex color string (used by Google Earth) and converts it to RGBA colorspace 298 * 8-digit hex codes use AABBGGRR (R - red, G - green, B - blue, A - alpha transparency) 299 """ 300 hex8 = str(hex8.replace('#','')) 301 if len(hex8) != 8: 302 raise Exception("Hex8 value must be exactly 8 digits") 303 hex_values = [hex8[i:i + 2:1] for i in xrange(0, len(hex8), 2)] 304 rgba_values = [int(x,16) for x in hex_values] 305 rgba_values.reverse() 306 return rgba_values
307 308 from django.utils.importlib import import_module 309
310 -def load_session(request, session_key):
311 if session_key and session_key != '0': 312 engine = import_module(settings.SESSION_ENGINE) 313 request.session = engine.SessionStore(session_key)
314
315 -def valid_browser(ua):
316 """ 317 Returns boolean depending on whether we support their browser 318 based on their HTTP_USER_AGENT 319 320 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 321 Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 322 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b7) Gecko/20100101 Firefox/4.0b7 323 """ 324 supported_browsers = [ 325 ('Firefox', 3, 5, 'Mac'), 326 ('Firefox', 4, 0, 'Mac'), 327 ('Safari', 3, 1, 'Mac'), 328 ('Chrome', 6, 0, 'Mac'), 329 ('Firefox', 3, 5, 'Windows'), 330 ('Firefox', 4, 0, 'Windows'), 331 ('Chrome', 1, 0, 'Windows'), 332 ('IE', 8, 0, 'Windows'), 333 ] 334 335 from madrona.common import uaparser 336 337 bp = uaparser.browser_platform(ua) 338 if not bp.platform: 339 log.warn("Platform is None: UA String is '%s'" % ua) 340 341 for sb in supported_browsers: 342 if bp.family == sb[0] and \ 343 ((bp.v1 == sb[1] and bp.v2 >= sb[2]) or bp.v1 > sb[1]) and \ 344 bp.platform == sb[3]: 345 return True 346 347 return False
348
349 -class KMZUtil:
350 """ 351 Recursively adds a directory to a zipfile 352 modified from http://stackoverflow.com/questions/458436/adding-folders-to-a-zip-file-using-python 353 354 from madrona.common.utils import ZipUtil 355 zu = ZipUtil() 356 filename = 'TEMP.zip' 357 directory = 'kmldir' # containing doc.kml, etc 358 zu.toZip(directory, filename) 359 """
360 - def toZip(self, file, filename):
361 zip_file = zipfile.ZipFile(filename, 'w') 362 if os.path.isfile(file): 363 zip_file.write(file) 364 else: 365 self.addFolderToZip(zip_file, file) 366 zip_file.close()
367
368 - def addFolderToZip(self, zip_file, folder):
369 if not folder or folder == '': 370 folder_path = '.' 371 else: 372 folder_path = folder 373 374 # first add doc.kml - IMPORTANT that it be the first file added! 375 doc = os.path.join(folder,'doc.kml') 376 if os.path.exists(doc): 377 #print 'File added: ' + str(doc) 378 zip_file.write(doc) 379 380 for file in os.listdir(folder_path): 381 full_path = os.path.join(folder, file) 382 if os.path.isfile(full_path) and not full_path.endswith("doc.kml"): 383 #print 'File added: ' + str(full_path) 384 zip_file.write(full_path) 385 elif os.path.isdir(full_path): 386 #print 'Entering folder: ' + str(full_path) 387 self.addFolderToZip(zip_file, full_path)
388
389 -def isCCW(ring):
390 """ 391 Determines if a LinearRing is oriented counter-clockwise or not 392 """ 393 area = 0.0 394 for i in range(0,len(ring) - 1): 395 p1 = ring[i] 396 p2 = ring[i + 1] 397 area += (p1[1] * p2[0]) - (p1[0] * p2[1]) 398 399 if area > 0: 400 return False 401 else: 402 return True
403 404 405 from django.contrib.gis.geos import Polygon
406 -def forceRHR(polygon):
407 """ 408 reverses rings so that polygon follows the Right-hand rule 409 exterior ring = clockwise 410 interior rings = counter-clockwise 411 """ 412 assert polygon.geom_type == 'Polygon' 413 if polygon.empty: 414 return poly 415 exterior = True 416 rings = [] 417 for ring in polygon: 418 assert ring.ring # Must be a linear ring at this point 419 if exterior: 420 if isCCW(ring): 421 ring.reverse() 422 exterior = False 423 else: 424 if not isCCW(ring): 425 ring.reverse() 426 rings.append(ring) 427 poly = Polygon(*rings) 428 return poly
429
430 -def forceLHR(polygon):
431 """ 432 reverses rings so that geometry complies with the LEFT-hand rule 433 Google Earth KML requires this oddity 434 exterior ring = counter-clockwise 435 interior rings = clockwise 436 """ 437 assert polygon.geom_type == 'Polygon' 438 assert not polygon.empty 439 exterior = True 440 rings = [] 441 for ring in polygon: 442 assert ring.ring # Must be a linear ring at this point 443 if exterior: 444 if not isCCW(ring): 445 ring.reverse() 446 exterior = False 447 else: 448 if isCCW(ring): 449 ring.reverse() 450 rings.append(ring) 451 poly = Polygon(*rings) 452 return poly
453
454 -def asKml(input_geom, altitudeMode=None, uid=''):
455 """ 456 Performs three critical functions for creating suitable KML geometries: 457 - simplifies the geoms (lines, polygons only) 458 - forces left-hand rule orientation 459 - sets the altitudeMode shape 460 (usually one of: absolute, clampToGround, relativeToGround) 461 """ 462 if altitudeMode is None: 463 try: 464 altitudeMode = settings.KML_ALTITUDEMODE_DEFAULT 465 except: 466 altitudeMode = None 467 468 key = "asKml_%s_%s_%s" % (input_geom.wkt.__hash__(), altitudeMode, uid) 469 kmlcache, created = KmlCache.objects.get_or_create(key=key) 470 kml = kmlcache.kml_text 471 if not created and kml: 472 return kml 473 474 log.debug("%s ...no kml cache found...seeding" % key) 475 476 latlon_geom = input_geom.transform(4326, clone=True) 477 478 if latlon_geom.geom_type in ['Polygon','LineString']: 479 geom = latlon_geom.simplify(settings.KML_SIMPLIFY_TOLERANCE_DEGREES) 480 # Gaurd against invalid geometries due to bad simplification 481 # Keep reducing the tolerance til we get a good one 482 if geom.empty or not geom.valid: 483 toler = settings.KML_SIMPLIFY_TOLERANCE_DEGREES 484 maxruns = 20 485 for i in range(maxruns): 486 toler = toler / 3.0 487 geom = latlon_geom.simplify(toler) 488 log.debug("%s ... Simplification failed ... tolerance=%s" % (key,toler)) 489 if not geom.empty and geom.valid: 490 break 491 if i == maxruns - 1: 492 geom = latlon_geom 493 else: 494 geom = latlon_geom 495 496 if geom.geom_type == 'Polygon': 497 geom = forceLHR(geom) 498 499 kml = geom.kml 500 501 if altitudeMode and geom.geom_type == 'Polygon': 502 kml = kml.replace('<Polygon>', '<Polygon><altitudeMode>%s</altitudeMode><extrude>1</extrude>' % altitudeMode) 503 # The GEOSGeometry.kml() method always adds a z dim = 0 504 kml = kml.replace(',0 ', ',%s ' % settings.KML_EXTRUDE_HEIGHT) 505 506 kmlcache.kml_text = kml 507 kmlcache.save() 508 return kml
509
510 -def enable_sharing(group=None):
511 """ 512 Give group permission to share models 513 Permissions are attached to models but we want this perm to be 'global' 514 Fake it by attaching the perm to the Group model (from the auth app) 515 We check for this perm like: user1.has_perm("auth.can_share_features") 516 """ 517 from django.contrib.auth.models import Permission, Group 518 from django.contrib.contenttypes.models import ContentType 519 520 try: 521 p = Permission.objects.get(codename='can_share_features') 522 except Permission.DoesNotExist: 523 gct = ContentType.objects.get(name="group") 524 p = Permission.objects.create(codename='can_share_features',name='Can Share Features',content_type=gct) 525 p.save() 526 527 # Set up default sharing groups 528 for groupname in settings.SHARING_TO_PUBLIC_GROUPS: 529 g, created = Group.objects.get_or_create(name=groupname) 530 g.permissions.add(p) 531 g.save() 532 533 for groupname in settings.SHARING_TO_STAFF_GROUPS: 534 g, created = Group.objects.get_or_create(name=groupname) 535 g.permissions.add(p) 536 g.save() 537 538 if group: 539 # Set up specified group 540 group.permissions.add(p) 541 group.save() 542 return True
543 544 545 ''' 546 Returns a path to desired resource (image file) 547 Called from within pisaDocument via link_callback parameter (from pdf_report) 548 '''
549 -def fetch_resources(uri, rel):
550 import os 551 import settings 552 import datetime 553 import random 554 import tempfile 555 import urllib2 556 from django.test.client import Client 557 558 if uri.startswith('http'): 559 # An external address assumed to require no authentication 560 req = urllib2.Request(uri) 561 response = urllib2.urlopen(req) 562 content = response.read() 563 elif 'staticmap' in uri: 564 # A staticmap url .. gets special treatment due to permissions 565 from madrona.staticmap.temp_save import img_from_params 566 params = get_params_from_uri(uri) 567 content = img_from_params(params, None) 568 else: 569 # An internal address assumed; use the django test client 570 client = Client() 571 response = client.get(uri) 572 content = response.content 573 # alternate way 574 # path = os.path.join(settings.MEDIA_ROOT, uri.replace(settings.MEDIA_URL, "")) 575 576 randnum = random.randint(0, 1000000000) 577 timestamp = datetime.datetime.now().strftime('%m_%d_%y_%H%M') 578 filename = 'resource_%s_%s.tmp' % (timestamp,randnum) 579 pathname = os.path.join(tempfile.gettempdir(),filename) 580 fh = open(pathname,'wb') 581 fh.write(content) 582 fh.close() 583 return pathname
584 585 ''' 586 Returns a dictionary representation of the parameters attached to the given uri 587 Called by fetch_resources 588 '''
589 -def get_params_from_uri(uri):
590 from urlparse import urlparse 591 results = urlparse(uri) 592 params = {} 593 if results.query == '': 594 return params 595 params_list = results.query.split('&') 596 for param in params_list: 597 pair = param.split('=') 598 params[pair[0]] = pair[1] 599 return params
600
601 -def is_text(s):
602 """ 603 Tests a string to see if it's binary 604 borrowed from http://code.activestate.com/recipes/173220-test-if-a-file-or-string-is-text-or-binary/ 605 """ 606 import string 607 from django.utils.encoding import smart_str 608 s = smart_str(s) 609 text_characters = "".join(map(chr, range(32, 127)) + list("\n\r\t\b")) 610 _null_trans = string.maketrans("", "") 611 612 if "\0" in s: 613 return False 614 if not s: 615 return True 616 617 # Get the non-text characters (maps a character to itself then 618 # use the 'remove' option to get rid of the text characters.) 619 t = s.translate(_null_trans, text_characters) 620 621 # If more than 30% non-text characters, then 622 # this is considered a binary file 623 if float(len(t)) / len(s) > 0.30: 624 return False 625 return True
626 627 from django.core.cache import cache
628 -def cachemethod(cache_key, timeout=3600):
629 ''' 630 http://djangosnippets.org/snippets/1130/ 631 Cacheable class method decorator 632 from madrona.common.utils import cachemethod 633 @cachemethod("SomeClass_get_some_result_%(id)s") 634 ''' 635 def paramed_decorator(func): 636 def decorated(self): 637 key = cache_key % self.__dict__ 638 res = cache.get(key) 639 if res == None: 640 res = func(self) 641 cache.set(key, res, timeout) 642 return res
643 decorated.__doc__ = func.__doc__ 644 decorated.__dict__ = func.__dict__ 645 return decorated 646 return paramed_decorator 647