| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.web.test.test_woven -*- | |
| 2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 3 # See LICENSE for details. | |
| 4 | |
| 5 | |
| 6 __version__ = "$Revision: 1.53 $"[11:-2] | |
| 7 | |
| 8 import types | |
| 9 import weakref | |
| 10 import warnings | |
| 11 | |
| 12 from zope.interface import implements | |
| 13 | |
| 14 from twisted.python import components, reflect | |
| 15 from twisted.internet import defer | |
| 16 | |
| 17 from twisted.web.woven import interfaces | |
| 18 | |
| 19 class _Nothing: pass | |
| 20 | |
| 21 def adaptToIModel(m, parent=None, submodel=None): | |
| 22 adapted = interfaces.IModel(m, None) | |
| 23 if adapted is None: | |
| 24 adapted = Wrapper(m) | |
| 25 adapted.parent = parent | |
| 26 adapted.name = submodel | |
| 27 return adapted | |
| 28 | |
| 29 | |
| 30 class Model: | |
| 31 """ | |
| 32 A Model which keeps track of views which are looking at it in order | |
| 33 to notify them when the model changes. | |
| 34 """ | |
| 35 implements(interfaces.IModel) | |
| 36 | |
| 37 def __init__(self, *args, **kwargs): | |
| 38 if len(args): | |
| 39 self.original = args[0] | |
| 40 else: | |
| 41 self.original = self | |
| 42 self.name = '' | |
| 43 self.parent = None | |
| 44 self.views = [] | |
| 45 self.subviews = {} | |
| 46 self.submodels = {} | |
| 47 self._getter = kwargs.get('getter') | |
| 48 self._setter = kwargs.get('setter') | |
| 49 self.cachedFor = None | |
| 50 self.initialize(*args, **kwargs) | |
| 51 | |
| 52 def __getstate__(self): | |
| 53 self.views = [] | |
| 54 self.subviews = {} | |
| 55 self.submodels = {} | |
| 56 return self.__dict__ | |
| 57 | |
| 58 def invalidateCache(self): | |
| 59 """Invalidate the cache for this object, so the next time | |
| 60 getData is called, it's getter method is called again. | |
| 61 """ | |
| 62 self.cachedFor = None | |
| 63 | |
| 64 def initialize(self, *args, **kwargs): | |
| 65 """ | |
| 66 Hook for subclasses to initialize themselves without having to | |
| 67 mess with the __init__ chain. | |
| 68 """ | |
| 69 pass | |
| 70 | |
| 71 def addView(self, view): | |
| 72 """ | |
| 73 Add a view for the model to keep track of. | |
| 74 """ | |
| 75 if view not in [ref() for ref in self.views]: | |
| 76 self.views.append(weakref.ref(view)) | |
| 77 | |
| 78 def addSubview(self, name, subview): | |
| 79 subviewList = self.subviews.get(name, []) | |
| 80 subviewList.append(weakref.ref(subview)) | |
| 81 self.subviews[name] = subviewList | |
| 82 | |
| 83 def removeView(self, view): | |
| 84 """ | |
| 85 Remove a view that the model no longer should keep track of. | |
| 86 """ | |
| 87 # AM: loop on a _copy_ of the list, since we're changing it!!! | |
| 88 for weakref in list(self.views): | |
| 89 ref = weakref() | |
| 90 if ref is view or ref is None: | |
| 91 self.views.remove(weakref) | |
| 92 | |
| 93 def setGetter(self, getter): | |
| 94 self._getter = getter | |
| 95 | |
| 96 def setSetter(self, setter): | |
| 97 self._setter = setter | |
| 98 | |
| 99 def notify(self, changed=None): | |
| 100 """ | |
| 101 Notify all views that something was changed on me. | |
| 102 Passing a dictionary of {'attribute': 'new value'} in changed | |
| 103 will pass this dictionary to the view for increased performance. | |
| 104 If you don't want to do this, don't, and just use the traditional | |
| 105 MVC paradigm of querying the model for things you're interested | |
| 106 in. | |
| 107 """ | |
| 108 self.cachedFor = None | |
| 109 if changed is None: changed = {} | |
| 110 retVal = [] | |
| 111 # AM: loop on a _copy_ of the list, since we're changing it!!! | |
| 112 for view in list(self.views): | |
| 113 ref = view() | |
| 114 if ref is not None: | |
| 115 retVal.append((ref, ref.modelChanged(changed))) | |
| 116 else: | |
| 117 self.views.remove(view) | |
| 118 for key, value in self.subviews.items(): | |
| 119 if value.wantsAllNotifications or changed.has_key(key): | |
| 120 for item in list(value): | |
| 121 ref = item() | |
| 122 if ref is not None: | |
| 123 retVal.append((ref, ref.modelChanged(changed))) | |
| 124 else: | |
| 125 value.remove(item) | |
| 126 return retVal | |
| 127 | |
| 128 protected_names = ['initialize', 'addView', 'addSubview', 'removeView', 'not
ify', 'getSubmodel', 'setSubmodel', 'getData', 'setData'] | |
| 129 allowed_names = [] | |
| 130 | |
| 131 def lookupSubmodel(self, request, submodelName): | |
| 132 """ | |
| 133 Look up a full submodel name. I will split on `/' and call | |
| 134 L{getSubmodel} on each element in the 'path'. | |
| 135 | |
| 136 Override me if you don't want 'traversing'-style lookup, but | |
| 137 would rather like to look up a model based on the entire model | |
| 138 name specified. | |
| 139 | |
| 140 If you override me to return Deferreds, make sure I look up | |
| 141 values in a cache (created by L{setSubmodel}) before doing a | |
| 142 regular Deferred lookup. | |
| 143 | |
| 144 XXX: Move bits of this docstring to interfaces.py | |
| 145 """ | |
| 146 if not submodelName: | |
| 147 return None | |
| 148 | |
| 149 # Special case: If the first character is / | |
| 150 # Start at the bottom of the model stack | |
| 151 currentModel = self | |
| 152 if submodelName[0] == '/': | |
| 153 while currentModel.parent is not None: | |
| 154 currentModel = currentModel.parent | |
| 155 submodelName = submodelName[1:] | |
| 156 | |
| 157 submodelList = submodelName.split('/') #[:-1] | |
| 158 # print "submodelList", submodelList | |
| 159 for element in submodelList: | |
| 160 if element == '.' or element == '': | |
| 161 continue | |
| 162 elif element == '..': | |
| 163 currentModel = currentModel.parent | |
| 164 else: | |
| 165 currentModel = currentModel.getSubmodel(request, element) | |
| 166 if currentModel is None: | |
| 167 return None | |
| 168 return currentModel | |
| 169 | |
| 170 def submodelCheck(self, request, name): | |
| 171 """Check if a submodel name is allowed. Subclass me to implement a | |
| 172 name security policy. | |
| 173 """ | |
| 174 if self.allowed_names: | |
| 175 return (name in self.allowed_names) | |
| 176 else: | |
| 177 return (name and name[0] != '_' and name not in self.protected_names
) | |
| 178 | |
| 179 | |
| 180 def submodelFactory(self, request, name): | |
| 181 warnings.warn("Warning: default Model lookup strategy is changing:" | |
| 182 "use either AttributeModel or MethodModel for now.", | |
| 183 DeprecationWarning) | |
| 184 if hasattr(self, name): | |
| 185 return getattr(self, name) | |
| 186 else: | |
| 187 return None | |
| 188 | |
| 189 def getSubmodel(self, request, name): | |
| 190 """ | |
| 191 Get the submodel `name' of this model. If I ever return a | |
| 192 Deferred, then I ought to check for cached values (created by | |
| 193 L{setSubmodel}) before doing a regular Deferred lookup. | |
| 194 """ | |
| 195 if self.submodels.has_key(name): | |
| 196 return self.submodels[name] | |
| 197 if not self.submodelCheck(request, name): | |
| 198 return None | |
| 199 m = self.submodelFactory(request, name) | |
| 200 if m is None: | |
| 201 return None | |
| 202 sm = adaptToIModel(m, self, name) | |
| 203 self.submodels[name] = sm | |
| 204 return sm | |
| 205 | |
| 206 def setSubmodel(self, request=None, name=None, value=None): | |
| 207 """ | |
| 208 Set a submodel on this model. If getSubmodel or lookupSubmodel | |
| 209 ever return a Deferred, I ought to set this in a place that | |
| 210 lookupSubmodel/getSubmodel know about, so they can use it as a | |
| 211 cache. | |
| 212 """ | |
| 213 if self.submodelCheck(request, name): | |
| 214 if self.submodels.has_key(name): | |
| 215 del self.submodels[name] | |
| 216 setattr(self, name, value) | |
| 217 | |
| 218 def dataWillChange(self): | |
| 219 pass | |
| 220 | |
| 221 def getData(self, request): | |
| 222 if self.cachedFor != id(request) and self._getter is not None: | |
| 223 self.cachedFor = id(request) | |
| 224 self.dataWillChange() | |
| 225 self.orig = self.original = self._getter(request) | |
| 226 return self.original | |
| 227 | |
| 228 def setData(self, request, data): | |
| 229 if self._setter is not None: | |
| 230 self.cachedFor = None | |
| 231 return self._setter(request, data) | |
| 232 else: | |
| 233 if hasattr(self, 'parent') and self.parent: | |
| 234 self.parent.setSubmodel(request, self.name, data) | |
| 235 self.orig = self.original = data | |
| 236 | |
| 237 | |
| 238 class MethodModel(Model): | |
| 239 """Look up submodels with wmfactory_* methods. | |
| 240 """ | |
| 241 | |
| 242 def submodelCheck(self, request, name): | |
| 243 """Allow any submodel for which I have a submodel. | |
| 244 """ | |
| 245 return hasattr(self, "wmfactory_"+name) | |
| 246 | |
| 247 def submodelFactory(self, request, name): | |
| 248 """Call a wmfactory_name method on this model. | |
| 249 """ | |
| 250 meth = getattr(self, "wmfactory_"+name) | |
| 251 return meth(request) | |
| 252 | |
| 253 def getSubmodel(self, request=None, name=None): | |
| 254 if name is None: | |
| 255 warnings.warn("Warning! getSubmodel should now take the request as t
he first argument") | |
| 256 name = request | |
| 257 request = None | |
| 258 | |
| 259 cached = self.submodels.has_key(name) | |
| 260 sm = Model.getSubmodel(self, request, name) | |
| 261 if sm is not None: | |
| 262 if not cached: | |
| 263 sm.cachedFor = id(request) | |
| 264 sm._getter = getattr(self, "wmfactory_"+name) | |
| 265 return sm | |
| 266 | |
| 267 | |
| 268 class AttributeModel(Model): | |
| 269 """Look up submodels as attributes with hosts.allow/deny-style security. | |
| 270 """ | |
| 271 def submodelFactory(self, request, name): | |
| 272 if hasattr(self, name): | |
| 273 return getattr(self, name) | |
| 274 else: | |
| 275 return None | |
| 276 | |
| 277 | |
| 278 #backwards compatibility | |
| 279 WModel = Model | |
| 280 | |
| 281 | |
| 282 class Wrapper(Model): | |
| 283 """ | |
| 284 I'm a generic wrapper to provide limited interaction with the | |
| 285 Woven models and submodels. | |
| 286 """ | |
| 287 parent = None | |
| 288 name = None | |
| 289 def __init__(self, orig): | |
| 290 Model.__init__(self) | |
| 291 self.orig = self.original = orig | |
| 292 | |
| 293 def dataWillChange(self): | |
| 294 pass | |
| 295 | |
| 296 def __repr__(self): | |
| 297 myLongName = reflect.qual(self.__class__) | |
| 298 return "<%s instance at 0x%x: wrapped data: %s>" % (myLongName, | |
| 299 id(self), self.origi
nal) | |
| 300 | |
| 301 | |
| 302 class ListModel(Wrapper): | |
| 303 """ | |
| 304 I wrap a Python list and allow it to interact with the Woven | |
| 305 models and submodels. | |
| 306 """ | |
| 307 def dataWillChange(self): | |
| 308 self.submodels = {} | |
| 309 | |
| 310 def getSubmodel(self, request=None, name=None): | |
| 311 if name is None and type(request) is type(""): | |
| 312 warnings.warn("Warning!") | |
| 313 name = request | |
| 314 request = None | |
| 315 if self.submodels.has_key(name): | |
| 316 return self.submodels[name] | |
| 317 orig = self.original | |
| 318 try: | |
| 319 i = int(name) | |
| 320 except: | |
| 321 return None | |
| 322 if i > len(orig): | |
| 323 return None | |
| 324 sm = adaptToIModel(orig[i], self, name) | |
| 325 self.submodels[name] = sm | |
| 326 return sm | |
| 327 | |
| 328 def setSubmodel(self, request=None, name=None, value=None): | |
| 329 if value is None: | |
| 330 warnings.warn("Warning!") | |
| 331 value = name | |
| 332 name = request | |
| 333 request = None | |
| 334 self.original[int(name)] = value | |
| 335 | |
| 336 def __len__(self): | |
| 337 return len(self.original) | |
| 338 | |
| 339 def __getitem__(self, name): | |
| 340 return self.getSubmodel(None, str(name)) | |
| 341 | |
| 342 def __setitem__(self, name, value): | |
| 343 self.setSubmodel(None, str(name), value) | |
| 344 | |
| 345 def __repr__(self): | |
| 346 myLongName = reflect.qual(self.__class__) | |
| 347 return "<%s instance at 0x%x: wrapped data: %s>" % (myLongName, | |
| 348 id(self), self.origi
nal) | |
| 349 | |
| 350 | |
| 351 class StringModel(ListModel): | |
| 352 | |
| 353 """ I wrap a Python string and allow it to interact with the Woven models | |
| 354 and submodels. """ | |
| 355 | |
| 356 def setSubmodel(self, request=None, name=None, value=None): | |
| 357 raise ValueError("Strings are immutable.") | |
| 358 | |
| 359 | |
| 360 # pyPgSQL returns "PgResultSet" instances instead of lists, which look, act | |
| 361 # and breathe just like lists. pyPgSQL really shouldn't do this, but this works | |
| 362 try: | |
| 363 from pyPgSQL import PgSQL | |
| 364 components.registerAdapter(ListModel, PgSQL.PgResultSet, interfaces.IModel) | |
| 365 except: | |
| 366 pass | |
| 367 | |
| 368 class DictionaryModel(Wrapper): | |
| 369 """ | |
| 370 I wrap a Python dictionary and allow it to interact with the Woven | |
| 371 models and submodels. | |
| 372 """ | |
| 373 def dataWillChange(self): | |
| 374 self.submodels = {} | |
| 375 | |
| 376 def getSubmodel(self, request=None, name=None): | |
| 377 if name is None and type(request) is type(""): | |
| 378 warnings.warn("getSubmodel must get a request argument now") | |
| 379 name = request | |
| 380 request = None | |
| 381 if self.submodels.has_key(name): | |
| 382 return self.submodels[name] | |
| 383 orig = self.original | |
| 384 if name not in orig: | |
| 385 return None | |
| 386 sm = adaptToIModel(orig[name], self, name) | |
| 387 self.submodels[name] = sm | |
| 388 return sm | |
| 389 | |
| 390 def setSubmodel(self, request=None, name=None, value=None): | |
| 391 if value is None: | |
| 392 warnings.warn("Warning!") | |
| 393 value = name | |
| 394 name = request | |
| 395 request = None | |
| 396 self.original[name] = value | |
| 397 | |
| 398 | |
| 399 class AttributeWrapper(Wrapper): | |
| 400 """ | |
| 401 I wrap an attribute named "name" of the given parent object. | |
| 402 """ | |
| 403 def __init__(self, parent, name): | |
| 404 self.original = None | |
| 405 parent = ObjectWrapper(parent) | |
| 406 Wrapper.__init__(self, parent.getSubmodel(None, name)) | |
| 407 self.parent = parent | |
| 408 self.name = name | |
| 409 | |
| 410 | |
| 411 class ObjectWrapper(Wrapper): | |
| 412 """ | |
| 413 I may wrap an object and allow it to interact with the Woven models | |
| 414 and submodels. By default, I am not registered for use with anything. | |
| 415 """ | |
| 416 def getSubmodel(self, request=None, name=None): | |
| 417 if name is None and type(request) is type(""): | |
| 418 warnings.warn("Warning!") | |
| 419 name = request | |
| 420 request = None | |
| 421 if self.submodels.has_key(name): | |
| 422 return self.submodels[name] | |
| 423 sm = adaptToIModel(getattr(self.original, name), self, name) | |
| 424 self.submodels[name] = sm | |
| 425 return sm | |
| 426 | |
| 427 def setSubmodel(self, request=None, name=None, value=None): | |
| 428 if value is None: | |
| 429 warnings.warn("Warning!") | |
| 430 value = name | |
| 431 name = request | |
| 432 request = None | |
| 433 setattr(self.original, name, value) | |
| 434 | |
| 435 class UnsafeObjectWrapper(ObjectWrapper): | |
| 436 """ | |
| 437 I may wrap an object and allow it to interact with the Woven models | |
| 438 and submodels. By default, I am not registered for use with anything. | |
| 439 I am unsafe because I allow methods to be called. In fact, I am | |
| 440 dangerously unsafe. Be wary or I will kill your security model! | |
| 441 """ | |
| 442 def getSubmodel(self, request=None, name=None): | |
| 443 if name is None and type(request) is type(""): | |
| 444 warnings.warn("Warning!") | |
| 445 name = request | |
| 446 request = None | |
| 447 if self.submodels.has_key(name): | |
| 448 return self.submodels[name] | |
| 449 value = getattr(self.original, name) | |
| 450 if callable(value): | |
| 451 return value() | |
| 452 sm = adaptToIModel(value, self, name) | |
| 453 self.submodels = sm | |
| 454 return sm | |
| 455 | |
| 456 | |
| 457 class DeferredWrapper(Wrapper): | |
| 458 def setData(self, request=None, data=_Nothing): | |
| 459 if data is _Nothing: | |
| 460 warnings.warn("setData should be called with request as first arg") | |
| 461 data = request | |
| 462 request = None | |
| 463 if isinstance(data, defer.Deferred): | |
| 464 self.original = data | |
| 465 else: | |
| 466 views, subviews = self.views, self.subviews | |
| 467 new = adaptToIModel(data, self.parent, self.name) | |
| 468 self.__class__ = new.__class__ | |
| 469 self.__dict__ = new.__dict__ | |
| 470 self.views, self.subviews = views, subviews | |
| 471 | |
| 472 class Link(AttributeModel): | |
| 473 def __init__(self, href, text): | |
| 474 AttributeModel.__init__(self) | |
| 475 self.href = href | |
| 476 self.text = text | |
| 477 | |
| 478 try: | |
| 479 components.registerAdapter(StringModel, types.StringType, interfaces.IModel) | |
| 480 components.registerAdapter(ListModel, types.ListType, interfaces.IModel) | |
| 481 components.registerAdapter(ListModel, types.TupleType, interfaces.IModel) | |
| 482 components.registerAdapter(DictionaryModel, types.DictionaryType, interfaces
.IModel) | |
| 483 components.registerAdapter(DeferredWrapper, defer.Deferred, interfaces.IMode
l) | |
| 484 components.registerAdapter(DeferredWrapper, defer.DeferredList, interfaces.I
Model) | |
| 485 except ValueError: | |
| 486 # The adapters were already registered | |
| 487 pass | |
| OLD | NEW |