PK9Ftn[xmantissa/_recordattr.py# -*- test-case-name: xmantissa.test.test_recordattr -*- """ Utility support for attributes on items which compose multiple Axiom attributes into a single epsilon.structlike.record attribute. This can be handy when composing a simple, common set of columns that several tables share into a recognizable object that is not an item itself. For example, the pair of 'localpart', 'domain' into a user object, or the triple of 'realname', 'nickname', 'hostmask', 'network' into an IRC nickname. This functionality is currently used to make L{sharing.Identifier} objects. This is a handy utility that should really be moved to L{axiom.attributes} and made public as soon as a few conditions are met: * L{WithRecordAttributes} needs to be integrated into L{Item}, or otherwise made obsolete such that normal item instantiation works and users don't need to call a bogus classmethod. * L{RecordAttribute} needs to implement the full set of comparison operators required by the informal axiom constraint language (__gt__, __lt__, __ge__, __le__, probably some other stuff). It would also be great if that informal language got documented somewhere. """ from axiom.attributes import AND class RecordAttribute(object): """ A descriptor which maps a group of axiom attributes into a single attribute which returns a record composing them all. Use this within an Item class definition, like so:: class Address(record('localpart domain')): 'An email address.' class Email(Item, WithRecordAttributes): senderLocalpart = text() senderDomain = text() receipientLocalpart = text() recipientDomain = text() body = text() sender = RecordAttribute(Address, senderLocalpart, senderDomain) recipient = RecordAttribute(Address, recipientLocalpart, recipientDomain) # ... myEmail = Email._recordCreate(sender=Address(localpart=u'hello', domain=u'example.com'), recipient=Address(localpart=u'goodbye', domain=u'example.com')) print myEmail.sender.localpart Note: the ugly _recordCreate method is required to create items which use this feature due to some problems with Axiom's initialization order. See L{WithRecordAttributes} for details. """ def __init__(self, recordType, attrs): """ Create a L{RecordAttribute} for a certain record type and set of Axiom attributes. @param recordType: the result, or a subclass of the result, of L{axiom.structlike.record}. @param attrs: a tuple of L{axiom.attributes.SQLAttribute} instances that were defined as part of the schema on the same item type. """ self.recordType = recordType self.attrs = attrs def __get__(self, oself, type=None): """ Retrieve this compound attribute from the given item. @param oself: an L{axiom.item.Item} instance, of a type which has this L{RecordAttribute}'s L{attrs} defined in its schema. """ if oself is None: return self constructData = {} for n, attr in zip(self.recordType.__names__, self.attrs): constructData[n] = attr.__get__(oself, type) return self.recordType(**constructData) def _decompose(self, value): """ Decompose an instance of our record type into a dictionary mapping attribute names to values. @param value: an instance of self.recordType @return: L{dict} containing the keys declared on L{record}. """ data = {} for n, attr in zip(self.recordType.__names__, self.attrs): data[attr.attrname] = getattr(value, n) return data def __set__(self, oself, value): """ Set each component attribute of this L{RecordAttribute} in turn. @param oself: an instance of the type where this attribute is defined. @param value: an instance of self.recordType whose values should be used. """ for n, attr in zip(self.recordType.__names__, self.attrs): attr.__set__(oself, getattr(value, n)) def __eq__(self, other): """ @return: a comparison object resulting in all of the component attributes of this attribute being equal to all of the attribute values on the other object. @rtype: L{IComparison} """ return AND(*[attr == getattr(other, name) for attr, name in zip(self.attrs, self.recordType.__names__)]) def __ne__(self, other): """ @return: a comparison object resulting in all of the component attributes of this attribute being unequal to all of the attribute values on the other object. @rtype: L{IComparison} """ return AND(*[attr != getattr(other, name) for attr, name in zip(self.attrs, self.recordType.__names__)]) class WithRecordAttributes(object): """ Axiom has an unfortunate behavior, which is a rather deep-seated bug in the way Item objects are initialized. Default parameters are processed before the attributes in the constructor's dictionary are actually set. In other words, if you have a custom descriptor like L{RecordAttribute}, it can't be passed in the constructor; if the public way to fill in a required attribute's value is via such an API, it becomes impossible to properly construct an object. This mixin implements a temporary workaround, by adding a classmethod for creating instances of classes that use L{RecordAttribute} by explicitly decomposing the structured record instances into their constitutent values before actually passing them on to L{Item.__init__}. This workaround needs to be promoted to a proper resolution before this can be a public API; users should be able to create their own descriptors that modify underlying database state and have them behave in the expected way during item creation. """ def create(cls, **kw): """ Create an instance of this class, first cleaning up the keyword arguments so they will fill in any required values. @return: an instance of C{cls} """ for k, v in kw.items(): attr = getattr(cls, k, None) if isinstance(attr, RecordAttribute): kw.pop(k) kw.update(attr._decompose(v)) return cls(**kw) create = classmethod(create) PK9FT  xmantissa/_webidgen.pyimport random maxLongInt = (2**63)-1 def genkey(): """ Generate a key (an int representing 16 bytes of random data). """ return random.randint(0, maxLongInt) def _swapat(key, n): # n: 0-8; a swap is an 8-bit value; 2 ints between 0-15 return (key >> ((8 * (n))+4)) & 0xf, (key >> (8 * n)) & 0xf def _swap(l, a, b): l[a], l[b] = l[b], l[a] def webIDToStoreID(key, webid): """ Takes a webid (a 16-character str suitable for including in URLs) and a key (an int, a private key for decoding it) and produces a storeID. """ if len(webid) != 16: return None try: int(webid, 16) except TypeError: return None except ValueError: return None l = list(webid) for nybbleid in range(7, -1, -1): a, b = _swapat(key, nybbleid) _swap(l, b, a) i = int(''.join(l), 16) return i ^ key def storeIDToWebID(key, storeid): """ Takes a key (int) and storeid (int) and produces a webid (a 16-character str suitable for including in URLs) """ i = key ^ storeid l = list('%0.16x' % (i,)) for nybbleid in range(0, 8): a, b = _swapat(key, nybbleid) _swap(l, a, b) return ''.join(l) def _test(): for y in range(100): key = genkey() print 'starting with key', key, hex(key) for sid in range(1000): wid = storeIDToWebID(key, sid) sid2 = webIDToStoreID(key, wid) assert sid == sid2, '%s != %s [%r %r]' % (sid, sid2, wid, key) #print wid, '<=>', sid, ':', hex(key) def _simpletest(): key = 0xfedcba9876543210 web = storeIDToWebID(key, 100) print 'web', repr(web) sid = webIDToStoreID(key, web) print 'store', hex(sid) if __name__ == '__main__': _test() PK9F֟N!!xmantissa/_webutil.py# Copyright 2008 Divmod, Inc. See LICENSE file for details # -*- test-case-name: xmantissa.test.test_webapp,xmantissa.test.test_publicweb,xmantissa.test.test_website -*- """ This unfortunate module exists to contain code that would create an ugly dependency loop if it were somewhere else. """ from zope.interface import implements from twisted.cred.portal import IRealm from epsilon.structlike import record from axiom.userbase import getDomainNames from nevow import athena from nevow.rend import NotFound from nevow.inevow import IResource, IRequest from xmantissa.ixmantissa import (IWebViewer, INavigableFragment, ISiteRootPlugin) from xmantissa.websharing import UserIndexPage from xmantissa.error import CouldNotLoadFromThemes class WebViewerHelper(object): """ This is a mixin for the common logic in the two providers of L{IWebViewer} included with Mantissa, L{xmantissa.publicweb._AnonymousWebViewer} and L{xmantissa.webapp._AuthenticatedWebViewer}. @ivar _getDocFactory: a 1-arg callable which returns a nevow loader. @ivar _preferredThemes: a 0-arg callable which returns a list of nevow themes. """ def __init__(self, _getDocFactory, _preferredThemes): """ """ self._getDocFactory = _getDocFactory self._preferredThemes = _preferredThemes def _wrapNavFrag(self, fragment, useAthena): """ Subclasses must implement this to wrap a fragment. @param fragment: an L{INavigableFragment} provider that should be wrapped in the resulting page. @param useAthena: Whether the resulting L{IResource} should be a L{LivePage}. @type useAthena: L{bool} @return: a fragment to display to the user. @rtype: L{IResource} """ def wrapModel(self, model): """ Converts application-provided model objects to L{IResource} providers. """ res = IResource(model, None) if res is None: frag = INavigableFragment(model) fragmentName = getattr(frag, 'fragmentName', None) if fragmentName is not None: fragDocFactory = self._getDocFactory(fragmentName) if fragDocFactory is not None: frag.docFactory = fragDocFactory if frag.docFactory is None: raise CouldNotLoadFromThemes(frag, self._preferredThemes()) useAthena = isinstance(frag, (athena.LiveFragment, athena.LiveElement)) return self._wrapNavFrag(frag, useAthena) else: return res class MantissaViewHelper(object): """ This is the superclass of all Mantissa resources which act as a wrapper around an L{INavigableFragment} provider. This must be mixed in to some hierarchy with a C{locateChild} method, since it expects to cooperate in such a hierarchy. Due to infelicities in the implementation of some (pre-existing) subclasses, there is no __init__; but subclasses must set the 'fragment' attribute in theirs. """ fragment = None def locateChild(self, ctx, segments): """ Attempt to locate the child via the '.fragment' attribute, then fall back to normal locateChild behavior. """ if self.fragment is not None: # There are still a bunch of bogus subclasses of this class, which # are used in a variety of distasteful ways. 'fragment' *should* # always be set to something that isn't None, but there's no way to # make sure that it will be for the moment. Every effort should be # made to reduce public use of subclasses of this class (instead # preferring to wrap content objects with # IWebViewer.wrapModel()), so that the above check can be # removed. -glyph lc = getattr(self.fragment, 'locateChild', None) if lc is not None: x = lc(ctx, segments) if x is not NotFound: return x return super(MantissaViewHelper, self).locateChild(ctx, segments) class SiteRootMixin(object): """ Common functionality for L{AnonymousSite} and L{WebSite}. """ def locateChild(self, context, segments): """ Return a statically defined child or a child defined by a site root plugin or an avatar from guard. """ request = IRequest(context) webViewer = IWebViewer(self.store, None) childAndSegments = self.siteProduceResource(request, segments, webViewer) if childAndSegments is not None: return childAndSegments return NotFound # IMantissaSite def siteProduceResource(self, req, segments, webViewer): """ Retrieve a child resource and segments from rootChild_ methods on this object and SiteRootPlugins. @return: a 2-tuple of (resource, segments), suitable for return from locateChild. @param req: an L{IRequest} provider. @param segments: a tuple of L{str}s, the segments from the request. @param webViewer: an L{IWebViewer}, to be propagated through the child lookup process. """ # rootChild_* is not the same as child_, because its signature is # different. Maybe this should be done some other way. shortcut = getattr(self, 'rootChild_' + segments[0], None) if shortcut: res = shortcut(req, webViewer) if res is not None: return res, segments[1:] for plg in self.store.powerupsFor(ISiteRootPlugin): produceResource = getattr(plg, 'produceResource', None) if produceResource is not None: childAndSegments = produceResource(req, segments, webViewer) else: childAndSegments = plg.resourceFactory(segments) if childAndSegments is not None: return childAndSegments return None # IPowerupIndirector def indirect(self, interface): """ Create a L{VirtualHostWrapper} so it can have the first chance to handle web requests. """ if interface is IResource: siteStore = self.store.parent if self.store.parent is None: siteStore = self.store return VirtualHostWrapper( siteStore, IWebViewer(self.store), self) return self class VirtualHostWrapper(record('siteStore webViewer wrapped')): """ Resource wrapper which implements per-user virtual subdomains. This should be wrapped around any resource which sits at the root of the hierarchy. It will examine requests for their hostname and, when appropriate, redirect handling of the query to the appropriate sharing resource. @type siteStore: L{Store} @ivar siteStore: The site store which will be queried to determine which hostnames are associated with this server. @type webViewer: L{IWebViewer} @ivar webViewer: The web viewer representing the user. @type wrapped: L{IResource} provider @ivar wrapped: A resource to which traversal will be delegated if the request is not for a user subdomain. """ implements(IResource) def subdomain(self, hostname): """ Determine of which known domain the given hostname is a subdomain. @return: A two-tuple giving the subdomain part and the domain part or C{None} if the domain is not a subdomain of any known domain. """ hostname = hostname.split(":")[0] for domain in getDomainNames(self.siteStore): if hostname.endswith("." + domain): username = hostname[:-len(domain) - 1] if username != "www": return username, domain return None def locateChild(self, context, segments): """ Delegate dispatch to a sharing resource if the request is for a user subdomain, otherwise fall back to the wrapped resource's C{locateChild} implementation. """ request = IRequest(context) hostname = request.getHeader('host') info = self.subdomain(hostname) if info is not None: username, domain = info index = UserIndexPage(IRealm(self.siteStore), self.webViewer) resource = index.locateChild(None, [username])[0] return resource, segments return self.wrapped.locateChild(context, segments) PK9F?\$$xmantissa/ampserver.py# -*- test-case-name: xmantissa.test.test_ampserver -*- # Copyright (c) 2008 Divmod. See LICENSE for details. """ This module provides an extensible L{AMP} server for Mantissa. The server supports authentication and allows interaction with one or more L{IBoxReceiver} implementations. L{IBoxReceiverFactory} powerups are used to get L{IBoxReceiver} providers by name, routing AMP boxes for multiple receivers over a single AMP connection. """ from zope.interface import implements from twisted.internet.protocol import ServerFactory from twisted.internet import reactor from twisted.cred.portal import Portal from twisted.protocols.amp import IBoxReceiver, Unicode from twisted.protocols.amp import BoxDispatcher, CommandLocator, Command from twisted.python.randbytes import secureRandom from epsilon.ampauth import CredReceiver, OneTimePadChecker from epsilon.amprouter import Router from axiom.iaxiom import IPowerupIndirector from axiom.item import Item from axiom.attributes import integer, inmemory from axiom.dependency import dependsOn from axiom.userbase import LoginSystem, getLoginMethods from axiom.upgrade import registerAttributeCopyingUpgrader from xmantissa.ixmantissa import ( IProtocolFactoryFactory, IBoxReceiverFactory, IOneTimePadGenerator) __metaclass__ = type class AMPConfiguration(Item): """ Configuration object for a Mantissa AMP server. @ivar ONE_TIME_PAD_DURATION: The duration of each one-time pad, in seconds. @type ONE_TIME_PAD_DURATION: C{int} """ powerupInterfaces = (IProtocolFactoryFactory, IOneTimePadGenerator) implements(*powerupInterfaces) schemaVersion = 2 loginSystem = dependsOn(LoginSystem) _oneTimePads = inmemory() ONE_TIME_PAD_DURATION = 60 * 2 callLater = staticmethod(reactor.callLater) def activate(self): """ Initialize L{_oneTimePads} """ self._oneTimePads = {} # IOneTimePadGenerator def generateOneTimePad(self, userStore): """ Generate a pad which can be used to authenticate via AMP. This pad will expire in L{ONE_TIME_PAD_DURATION} seconds. """ pad = secureRandom(16).encode('hex') self._oneTimePads[pad] = userStore.idInParent def expirePad(): self._oneTimePads.pop(pad, None) self.callLater(self.ONE_TIME_PAD_DURATION, expirePad) return pad # IProtocolFactoryFactory def getFactory(self): """ Return a server factory which creates AMP protocol instances. """ factory = ServerFactory() def protocol(): proto = CredReceiver() proto.portal = Portal( self.loginSystem, [self.loginSystem, OneTimePadChecker(self._oneTimePads)]) return proto factory.protocol = protocol return factory registerAttributeCopyingUpgrader( AMPConfiguration, 1, 2, postCopy=lambda new: new.store.powerUp(new, IOneTimePadGenerator)) class ProtocolUnknown(Exception): """ An attempt was made to establish a connection over an AMP route via the L{Connect} command to a protocol name for which no factory could be located. """ class Connect(Command): """ Command to establish a new route to an L{IBoxReceiver} as specified by a protocol name. """ arguments = [('origin', Unicode()), ('protocol', Unicode())] response = [('route', Unicode())] errors = {ProtocolUnknown: 'PROTOCOL_UNKNOWN'} class _RouteConnector(BoxDispatcher, CommandLocator): """ The actual L{IBoxReceiver} implementation supplied by L{AMPRouter} to be used as the avatar. There is one L{_RouteConnector} instance per connection. @ivar reactor: An L{IReactorTime} provider which will be used to schedule the actual route connection. This is a workaround for a missing AMP API: the ability to indicate a response without returning a value from a responder. The application L{IBoxReceiver} being associated with a new route cannot be allowed to send any boxes before the I{Connect} response box is sent. The simplest way to do this is to schedule a timed call to do the route connection so that it happens after the responder returns. @ivar store: The L{Store} which contained the L{AMPRouter} which created this object and which will be used to find L{IBoxReceiverFactory} powerups. @ivar router: The L{Router} which will be used to create new routes. """ def __init__(self, reactor, store, router): BoxDispatcher.__init__(self, self) self.reactor = reactor self.store = store self.router = router @Connect.responder def accept(self, origin, protocol): """ Create a new route attached to a L{IBoxReceiver} created by the L{IBoxReceiverFactory} with the indicated protocol. @type origin: C{unicode} @param origin: The identifier of a route on the peer which will be associated with this connection. Boxes sent back by the protocol which is created in this call will be sent back to this route. @type protocol: C{unicode} @param protocol: The name of the protocol to which to establish a connection. @raise ProtocolUnknown: If no factory can be found for the named protocol. @return: A newly created C{unicode} route identifier for this connection (as the value of a C{dict} with a C{'route'} key). """ for factory in self.store.powerupsFor(IBoxReceiverFactory): # XXX What if there's a duplicate somewhere? if factory.protocol == protocol: receiver = factory.getBoxReceiver() route = self.router.bindRoute(receiver) # This might be better implemented using a hook on the box. # See Twisted ticket #3479. self.reactor.callLater(0, route.connectTo, origin) return {'route': route.localRouteName} raise ProtocolUnknown() class AMPAvatar(Item): """ An L{IBoxReceiver} avatar which multiplexes AMP boxes for other receiver powerups on this item's store. """ powerupInterfaces = (IBoxReceiver,) implements(IPowerupIndirector) garbage = integer( doc=""" This class is stateless. This attribute satisfies the Axiom requirement that L{Item} subclasses have at least one attribute. """) def connectorFactory(self, router): """ Create the default receiver to use with the L{Router} returned by C{indirect}. """ return _RouteConnector(reactor, self.store, router) def indirect(self, interface): """ Create a L{Router} to handle AMP boxes received over an AMP connection. """ if interface is IBoxReceiver: router = Router() connector = self.connectorFactory(router) router.bindRoute(connector, None).connectTo(None) return router raise NotImplementedError() def connectRoute(amp, router, receiver, protocol): """ Connect the given receiver to a new box receiver for the given protocol. After connecting this router to an AMP server, use this method similarly to how you would use C{reactor.connectTCP} to establish a new connection to an HTTP, SMTP, or IRC server. @param receiver: An L{IBoxReceiver} which will be started when a route to a receiver for the given protocol is found. @param protocol: The name of a protocol which the AMP peer to which this router is connected has an L{IBoxReceiverFactory}. @return: A L{Deferred} which fires with C{receiver} when the route is established. """ route = router.bindRoute(receiver) d = amp.callRemote( Connect, origin=route.localRouteName, protocol=protocol) def cbGotRoute(result): route.connectTo(result['route']) return receiver d.addCallback(cbGotRoute) return d class EchoFactory(Item): """ Box receiver factory for an AMP protocol which just echoes AMP boxes back to the sender. This is primarily useful as an example of a box receiver factory and as a way to test whether it is possible to connect to protocols over a Mantissa AMP server (similar to VoIP echo tests). """ powerupInterfaces = (IBoxReceiverFactory,) implements(*powerupInterfaces) protocol = u"http://divmod.org/ns/echo" _garbage = integer( doc=""" meaningless attribute, only here to satisfy Axiom requirement for at least one attribute. """) def getBoxReceiver(self): return EchoReceiver() class EchoReceiver: """ An AMP box echoer. """ implements(IBoxReceiver) def startReceivingBoxes(self, sender): self.sender = sender def ampBoxReceived(self, box): self.sender.sendBox(box) def stopReceivingBoxes(self, reason): pass __all__ = [ 'ProtocolUnknown', 'RouteNotConnected', 'Connect', 'connectRoute', 'AMPConfiguration', 'AMPAvatar', 'Router', 'MantissaRouter', 'EchoFactory', 'EchoReceiver'] PK9F䊯00xmantissa/cachejs.py# -*- test-case-name: xmantissa.test.test_cachejs -*- """ This module implements a strategy for allowing the browser to cache JavaScript modules served by Athena. It's not entirely standalone, as it requires some cooperation from L{xmantissa.website}, specifically L{xmantissa.website.MantissaLivePage}. """ import hashlib from zope.interface import implements from twisted.python.filepath import FilePath from nevow.inevow import IResource from nevow import athena from nevow import static from nevow.rend import NotFound, FourOhFour class CachedJSModule(object): """ Various bits of cached information about a JavaScript module. @ivar moduleName: A JavaScript module name, e.g. 'Foo.Bar'. @type moduleName: C{str}. @ivar filePath: The path to the module on the filesystem. @type filePath: L{FilePath}. @ivar lastModified: The mtime of L{filePath}, as a POSIX timestamp. @type lastModified: C{int}. """ def __init__(self, moduleName, filePath): """ Create a CachedJSModule. """ self.moduleName = moduleName self.filePath = filePath self.lastModified = 0 self.maybeUpdate() def wasModified(self): """ Check to see if this module has been modified on disk since the last time it was cached. @return: True if it has been modified, False if not. """ self.filePath.restat() mtime = self.filePath.getmtime() if mtime >= self.lastModified: return True else: return False def maybeUpdate(self): """ Check this cache entry and update it if any filesystem information has changed. """ if self.wasModified(): self.lastModified = self.filePath.getmtime() self.fileContents = self.filePath.getContent() self.hashValue = hashlib.sha1(self.fileContents).hexdigest() class HashedJSModuleProvider(object): """ An Athena module-serving resource which handles hashed names instead of regular module names. @ivar moduleCache: a map of JS module names to CachedJSModule objects, representing the filesystem locations and contents of the modules. @ivar depsMemo: A memo of module dependencies. @type depsMemo: C{dict} of C{module name: dependent modules} """ implements(IResource) def __init__(self): """ Create a HashedJSModuleProvider. """ self.moduleCache = {} self.depsMemo = {} def getModule(self, moduleName): """ Retrieve a JavaScript module cache from the file path cache. @returns: Module cache for the named module. @rtype: L{CachedJSModule} """ if moduleName not in self.moduleCache: modulePath = FilePath( athena.jsDeps.getModuleForName(moduleName)._cache.path) cachedModule = self.moduleCache[moduleName] = CachedJSModule( moduleName, modulePath) else: cachedModule = self.moduleCache[moduleName] return cachedModule # IResource def locateChild(self, ctx, segments): """ Retrieve an L{inevow.IResource} to render the contents of the given module. """ if len(segments) != 2: return NotFound hashCode, moduleName = segments cachedModule = self.getModule(moduleName) return static.Data( cachedModule.fileContents, 'text/javascript', expires=(60 * 60 * 24 * 365 * 5)), [] def renderHTTP(self, ctx): """ There is no index of javascript modules; this resource is not renderable. """ return FourOhFour() theHashModuleProvider = HashedJSModuleProvider() __all__ = ['HashedJSModuleProvider', 'theHashModuleProvider'] PK9F9rxmantissa/endpoint.py# -*- test-case-name: xmantissa.test.test_q2q -*- from axiom import item, attributes from vertex import q2qclient from xmantissa import ixmantissa class UniversalEndpointService(item.Item): """ """ typeName = 'mantissa_q2q_service' schemaVersion = 1 q2qPortNumber = attributes.integer(default=8787) inboundTCPPortNumber = attributes.integer(default=None) publicIP = attributes.integer(default=None) udpEnabled = attributes.integer(default=False) certificatePath = attributes.path(default=None) _svcInst = attributes.inmemory() def __init__(self, **kw): super(UniversalEndpointService, self).__init__(**kw) if self.certificatePath is None: self.certificatePath = self.store.newDirectory(str(self.storeID), 'certificates') if not self.certificatePath.exists(): self.certificatePath.makedirs() def activate(self): self._svcInst = None def _makeService(self): self._svcInst = q2qclient.ClientQ2QService( self.certificatePath.path, publicIP=self.publicIP, inboundTCPPortnum=self.inboundTCPPortNumber, udpEnabled=self.udpEnabled, ) def _getService(self): if self._svcInst is None: self._makeService() return self._svcInst def installOn(self, other): other.powerUp(self, ixmantissa.IQ2QService) def listenQ2Q(self, fromAddress, protocolsToFactories, serverDescription): return self._getService().listenQ2Q(fromAddress, protocolsToFactories, serverDescription) def connectQ2Q(self, fromAddress, toAddress, protocolName, protocolFactory, usePrivateCertificate=None, fakeFromDomain=None, chooser=None): return self._getService().connectQ2Q( fromAddress, toAddress, protocolName, protocolFactory, usePrivateCertificate, fakeFromDomain, chooser) PK9F\xmantissa/error.py# -*- test-case-name: xmantissa.test -*- """ Exception definitions for Mantissa. """ from nevow.errors import MissingDocumentFactory class ArgumentError(Exception): """ Base class for all exceptions raised by the address parser due to malformed input. """ class AddressTooLong(ArgumentError): """ Exception raised when an address which exceeds the maximum allowed length is given to the parser. """ class InvalidAddress(ArgumentError): """ Exception raised when an address is syntactically invalid. """ class InvalidTrailingBytes(ArgumentError): """ Exception raised when there are extra bytes at the end of an address. """ class Unsortable(Exception): """ This exception is raised when a client invalidly attempts to sort a table view by a column that is not available for sorting. """ class MessageTransportError(Exception): """ A message transport failed in some unrecoverable way; the transmission needs to be retried. """ class BadSender(Exception): """ A substore attempted to send a message for a username / domain pair for which it was not authorized. @ivar attemptedSender: A unicode string, formatted as user@host, that indicates the user ID that the system attempted to send a message from. @ivar allowedSenders: A list of unicode strings, formatted like C{attemptedSender}, indicating the senders that the system is allowed to send messages as. """ def __init__(self, attemptedSender, allowedSenders): """ Create a L{BadSender} exception with the list of attempted senders and the list of allowed senders. """ self.attemptedSender = attemptedSender self.allowedSenders = allowedSenders Exception.__init__(self, allowedSenders[0].encode('utf-8') + " attempted to send message as " + self.attemptedSender.encode('utf-8')) class UnknownMessageType(Exception): """ A message of an unknown type was received. """ class MalformedMessage(Exception): """ A message of the AMP message type was received, but its body could not be parsed as a single AMP box. """ class RevertAndRespond(Exception): """ This special exception type allows an L{xmantissa.ixmantissa.IMessageReceiver.messageReceived} implementation to generate an answer, but revert the transaction where the application is processing the application logic. @ivar value: the L{Value} of the answer to be issued. """ def __init__(self, value): """ Create a L{RevertAndRespond} with an answer that will be provided to the message sender. """ self.value = value class CouldNotLoadFromThemes(MissingDocumentFactory): """ An L{INavigableFragment} didn't have a C{docFactory}, and also didn't have a C{fragmentName} that could be used to load one from a theme. This exception should help developers with debugging when they forget to give a docFactory or a fragmentName to an element. """ def __init__(self, element, themes): """ @param element: The L{INavigableFragment} that was being rendered. @param themes: A list of L{ITemplateNameResolver}s. """ MissingDocumentFactory.__init__(self, element) self.themes = themes def __repr__(self): """ A string representation including helpful information about the themes that were searched and the element which could not be themed. """ return ("%s: %r (fragment name %r) has no docFactory. Searched these themes: %r" % (self.__class__.__name__, self.element, self.element.fragmentName, self.themes)) PK9F11xmantissa/fragmentutils.pyfrom zope.interface import implements from nevow import inevow, athena from nevow.taglibrary import tabbedPane from xmantissa import ixmantissa class PatternDictionary(object): def __init__(self, docFactory): self.docFactory = inevow.IQ(docFactory) self.patterns = dict() def __getitem__(self, i): if i not in self.patterns: self.patterns[i] = self.docFactory.patternGenerator(i) return self.patterns[i] def dictFillSlots(tag, slotmap): for (k, v) in slotmap.iteritems(): tag = tag.fillSlots(k, v) return tag class FragmentCollector(athena.LiveFragment): implements(ixmantissa.INavigableFragment) fragmentName = None live = 'athena' title = None def __init__(self, translator, docFactory=None, collect=(), name='default'): self.name = name athena.LiveFragment.__init__(self, None, docFactory) tabs = [] for frag in collect: if frag.docFactory is None: frag.docFactory = translator.getDocFactory(frag.fragmentName, None) tabs.append((frag.title, frag)) self.tabs = tabs def head(self): for (tabTitle, fragment) in self.tabs: fragment.setFragmentParent(self) content = fragment.head() if content is not None: yield content yield tabbedPane.tabbedPaneGlue.inlineCSS def render_tabbedPane(self, ctx, data): tpf = tabbedPane.TabbedPaneFragment(self.tabs, name=self.name) tpf.setFragmentParent(self) return tpf PK9FxEb4f4fxmantissa/fulltext.py# -*- test-case-name: xmantissa.test.test_fulltext -*- """ General functionality re-usable by various concrete fulltext indexing systems. """ import atexit, os, weakref, warnings from zope.interface import implements from twisted.python import log, reflect from twisted.internet import defer from epsilon.structlike import record from epsilon.view import SlicedView from axiom import item, attributes, iaxiom, batch from axiom.upgrade import registerUpgrader, registerAttributeCopyingUpgrader from axiom.store import Store, AttributeQuery from axiom.attributes import AttributeValueComparison, SimpleOrdering from xmantissa import ixmantissa HYPE_INDEX_DIR = u'hype.index' XAPIAN_INDEX_DIR = u'xap.index' LUCENE_INDEX_DIR = u'lucene.index' VERBOSE = True class IndexCorrupt(Exception): """ An attempt was made to open an index which has had unrecoverable data corruption. """ class _IndexerInputSource(item.Item): """ Tracks L{IBatchProcessor}s which have had an indexer added to them as a listener. """ indexer = attributes.reference(doc=""" The indexer item with which this input source is associated. """, whenDeleted=attributes.reference.CASCADE) source = attributes.reference(doc=""" The L{IBatchProcessor} which acts as the input source. """, whenDeleted=attributes.reference.CASCADE) class _RemoveDocument(item.Item): """ Tracks a document deletion which should occur before the next search is performed. """ indexer = attributes.reference(doc=""" The indexer item with which this deletion is associated. """, whenDeleted=attributes.reference.CASCADE) documentIdentifier = attributes.bytes(doc=""" The identifier, as returned by L{IFulltextIndexable.uniqueIdentifier}, for the document which should be removed from the index. """, allowNone=False) class RemoteIndexer(object): """ Implements most of a full-text indexer. This uses L{axiom.batch} to perform indexing out of process and presents an asynchronous interface to in-process searching of that indexing. """ implements(iaxiom.IReliableListener, ixmantissa.ISearchProvider, ixmantissa.IFulltextIndexer) def installOn(self, other): super(RemoteIndexer, self).installOn(other) other.powerUp(self, ixmantissa.IFulltextIndexer) def openReadIndex(self): """ Return an object usable to search this index. Subclasses should implement this. """ raise NotImplementedError def openWriteIndex(self): """ Return an object usable to add documents to this index. Subclasses should implement this. """ raise NotImplementedError def __finalizer__(self): d = self.__dict__ id = self.storeID s = self.store def finalize(): idx = d.get('_index', None) if idx is not None: if VERBOSE: log.msg("Closing %r from finalizer of %s/%d" % (idx, s, id)) idx.close() return finalize def activate(self): assert not hasattr(self, '_index') self._index = None if VERBOSE: log.msg("Activating %s/%d with null index" % (self.store, self.storeID)) def addSource(self, itemSource): """ Add the given L{IBatchProcessor} as a source of input for this indexer. """ _IndexerInputSource(store=self.store, indexer=self, source=itemSource) itemSource.addReliableListener(self, style=iaxiom.REMOTE) def getSources(self): return self.store.query(_IndexerInputSource, _IndexerInputSource.indexer == self).getColumn("source") def reset(self): """ Process everything all over again. """ self.indexCount = 0 indexDir = self.store.newDirectory(self.indexDirectory) if indexDir.exists(): indexDir.remove() for src in self.getSources(): src.removeReliableListener(self) src.addReliableListener(self, style=iaxiom.REMOTE) def _closeIndex(self): if VERBOSE: log.msg("%s/%d closing index" % (self.store, self.storeID)) if self._index is not None: if VERBOSE: log.msg("%s/%d *really* closing index" % (self.store, self.storeID)) self._index.close() self._index = None # IFulltextIndexer def add(self, item): if self._index is None: try: self._index = self.openWriteIndex() except IndexCorrupt: self.reset() return if VERBOSE: log.msg("Opened %s %s/%d for writing" % (self._index, self.store, self.storeID)) if VERBOSE: log.msg("%s/%d indexing document" % (self.store, self.storeID)) self._index.add(ixmantissa.IFulltextIndexable(item)) self.indexCount += 1 def remove(self, item): identifier = ixmantissa.IFulltextIndexable(item).uniqueIdentifier() if VERBOSE: log.msg("%s/%d scheduling %r for removal." % (self.store, self.storeID, identifier)) _RemoveDocument(store=self.store, indexer=self, documentIdentifier=identifier) def _flush(self): """ Deal with pending result-affecting things. This should always be called before issuing a search. """ remove = self.store.query(_RemoveDocument) documentIdentifiers = list(remove.getColumn("documentIdentifier")) if VERBOSE: log.msg("%s/%d removing %r" % (self.store, self.storeID, documentIdentifiers)) reader = self.openReadIndex() map(reader.remove, documentIdentifiers) reader.close() remove.deleteFromStore() # IReliableListener def suspend(self): self._flush() # Make sure any pending deletes are processed. if VERBOSE: log.msg("%s/%d suspending" % (self.store, self.storeID)) self._closeIndex() return defer.succeed(None) def resume(self): if VERBOSE: log.msg("%s/%d resuming" % (self.store, self.storeID)) return defer.succeed(None) def processItem(self, item): return self.add(item) # ISearchProvider def search(self, aString, keywords=None, count=None, offset=0, sortAscending=True, retry=3): ident = "%s/%d" % (self.store, self.storeID) b = iaxiom.IBatchService(self.store) if VERBOSE: log.msg("%s issuing suspend" % (ident,)) d = b.suspend(self.storeID) def reallySearch(ign): if VERBOSE: log.msg("%s getting reader index" % (ident,)) idx = self.openReadIndex() if VERBOSE: log.msg("%s searching for %s" % ( ident, aString.encode('utf-8'))) results = idx.search(aString, keywords, sortAscending) if VERBOSE: log.msg("%s found %d results" % (ident, len(results))) if count is None: end = None else: end = offset + count results = results[offset:end] if VERBOSE: log.msg("%s sliced from %s to %s, leaving %d results" % ( ident, offset, end, len(results))) return results d.addCallback(reallySearch) def resumeIndexing(results): if VERBOSE: log.msg("%s issuing resume" % (ident,)) b.resume(self.storeID).addErrback(log.err) return results d.addBoth(resumeIndexing) def searchFailed(err): log.msg("Search failed somehow:") log.err(err) if retry: log.msg("Re-issuing search") return self.search(aString, keywords, count, offset, retry=retry-1) else: log.msg("Wow, lots of failures searching. Giving up and " "returning (probably wrong!) no results to user.") return [] d.addErrback(searchFailed) return d try: import hype except ImportError: hype = None class _HypeIndex(object): def __init__(self, index): self.index = index self.close = index.close def add(self, message): doc = hype.Document() for (k, v) in message.valueParts(): doc.add_hidden_text(v.encode('utf-8')) doc['@uri'] = message.uniqueIdentifier() for part in message.textParts(): doc.add_text(part.encode('utf-8')) self.index.put_doc(doc) def search(self, term, keywords=None, sortAscending=True): return [int(d.uri) for d in self.index.search(term)] class HypeIndexer(RemoteIndexer, item.Item): schemaVersion = 3 indexCount = attributes.integer(default=0) installedOn = attributes.reference() indexDirectory = attributes.text(default=HYPE_INDEX_DIR) _index = attributes.inmemory() if hype is None: def openReadIndex(self): raise NotImplementedError("hype is unavailable") def openWriteIndex(self): raise NotImplementedError("hype is unavailable") else: def openReadIndex(self): hypedir = self.store.newDirectory(self.indexDirectory) return _HypeIndex(hype.Database(hypedir.path, hype.ESTDBREADER | hype.ESTDBLCKNB | hype.ESTDBCREAT)) def openWriteIndex(self): hypedir = self.store.newDirectory(self.indexDirectory) return _HypeIndex(hype.Database(hypedir.path, hype.ESTDBWRITER | hype.ESTDBCREAT)) class XapianIndexer(RemoteIndexer, item.Item): """ The remnants of an indexer based on Xapian (by way of Xapwrap). This indexing back end is no longer supported. This item remains defined for schema compatibility only. It should be upgraded out of existence eventually and then the class deleted. """ schemaVersion = 3 indexCount = attributes.integer(default=0) installedOn = attributes.reference() indexDirectory = attributes.text(default=XAPIAN_INDEX_DIR) _index = attributes.inmemory() def openReadIndex(self): raise NotImplementedError("xapian is no longer supported") def openWriteIndex(self): raise NotImplementedError("xapian is no longer supported") try: import PyLucene except ImportError: PyLucene = None _hitsWrapperWeakrefs = weakref.WeakKeyDictionary() class _PyLuceneHitsWrapper(record('index hits')): """ Container for a C{Hits} instance and the L{_PyLuceneIndex} from which it came. This gives the C{Hits} instance a sequence-like interface and when a _PyLuceneHitsWrapper is garbage collected, it closes the L{_PyLuceneIndex} it has a reference to. """ def __init__(self, *a, **kw): super(_PyLuceneHitsWrapper, self).__init__(*a, **kw) def close(ref, index=self.index): log.msg("Hits wrapper expiring, closing index.") index.close() _hitsWrapperWeakrefs[self] = weakref.ref(self, close) def __len__(self): return len(self.hits) def __getitem__(self, index): """ Retrieve the storeID field of the requested hit, converting it to an integer before returning it. This handles integer indexes as well as slices. """ if isinstance(index, slice): return SlicedView(self, index) if index >= len(self.hits): raise IndexError(index) return _PyLuceneHitWrapper(self.hits[index]) class _PyLuceneHitWrapper: """ Wrapper around a single pylucene hit @ivar keywordParts: dictionary mapping keyword names to values. should be the same as the result of calling the L{IFulltextIndexable.keywordParts} on the item corresponding to the hit @ivar documentType: the document type. return value of L{IFulltextIndexable.documentType} called on the item corresponding to the hit @ivar uniqueIdentifier: an opaque unique identifier. return value of L{IFulltextIndexable.uniqueIdentifier} called on the item corresponding to the hit @ivar sortKey: the key to sort on. return value of L{IFulltextIndexable.sortKey} called on the item corresponding to the hit """ def __init__(self, hit): self.keywordParts = self._getKeywords(hit) self.documentType = hit['documentType'] self.uniqueIdentifier = hit['storeID'] self.sortKey = hit['sortKey'] def _getKeywords(self, hit): keywords = {} systemKeywords = set(('storeID', 'documentType', 'sortKey')) for field in hit.fields(): if field.name() not in systemKeywords: keywords[field.name()] = hit[field.name()] return keywords def __int__(self): warnings.warn( '_PyLuceneHitWrapper is not an integer', DeprecationWarning) return int(self.uniqueIdentifier) def __cmp__(self, other): warnings.warn( '_PyLuceneHitWrapper is not an integer', DeprecationWarning) return int(self).__cmp__(other) class _PyLuceneBase(object): closed = False def __init__(self, fsdir, analyzer): _closeObjects.append(self) self.fsdir = fsdir self.analyzer = analyzer def close(self): if not self.closed: self._reallyClose() self.fsdir.close() self.closed = True try: _closeObjects.remove(self) except ValueError: pass _closeObjects = [] def _closeIndexes(): """ Helper for _PyLuceneIndex to make sure FSDirectory and IndexWriter instances always get closed. This gets registered with atexit and closes any _PyLuceneIndex objects still in _closeObjects when it gets run. """ while _closeObjects: _closeObjects[-1].close() atexit.register(_closeIndexes) class _PyLuceneReader(_PyLuceneBase): """ Searches and deletes from a Lucene index. """ def __init__(self, fsdir, analyzer, reader, searcher): self.reader = reader self.searcher = searcher super(_PyLuceneReader, self).__init__(fsdir, analyzer) def _reallyClose(self): self.reader.close() self.searcher.close() def remove(self, documentIdentifier): self.reader.deleteDocuments( PyLucene.Term('storeID', documentIdentifier)) def search(self, phrase, keywords=None, sortAscending=True): if not phrase and not keywords: return [] # XXX Colons in phrase will screw stuff up. Can they be quoted or # escaped somehow? Probably by using a different QueryParser. if keywords: fieldPhrase = u' '.join(u':'.join((k, v)) for (k, v) in keywords.iteritems()) if phrase: phrase = phrase + u' ' + fieldPhrase else: phrase = fieldPhrase phrase = phrase.translate({ord(u'@'): u' ', ord(u'-'): u' ', ord(u'.'): u' '}) qp = PyLucene.QueryParser('text', self.analyzer) qp.setDefaultOperator(qp.Operator.AND) query = qp.parseQuery(phrase) sort = PyLucene.Sort(PyLucene.SortField('sortKey', not sortAscending)) try: hits = self.searcher.search(query, sort) except PyLucene.JavaError, err: if 'no terms in field sortKey' in str(err): hits = [] else: raise return _PyLuceneHitsWrapper(self, hits) class _PyLuceneWriter(_PyLuceneBase): """ Adds documents to a Lucene index. """ def __init__(self, fsdir, analyzer, writer): self.writer = writer super(_PyLuceneWriter, self).__init__(fsdir, analyzer) def _reallyClose(self): self.writer.close() def add(self, message): doc = PyLucene.Document() for part in message.textParts(): doc.add( PyLucene.Field('text', part.translate({ ord(u'@'): u' ', ord(u'-'): u' ', ord(u'.'): u' '}).encode('utf-8'), PyLucene.Field.Store.NO, PyLucene.Field.Index.TOKENIZED)) for (k, v) in message.keywordParts().iteritems(): doc.add( PyLucene.Field(k, v.translate({ ord(u'@'): u' ', ord(u'-'): u' ', ord(u'.'): u' '}).encode('utf-8'), PyLucene.Field.Store.YES, PyLucene.Field.Index.TOKENIZED)) doc.add( PyLucene.Field('documentType', message.documentType(), PyLucene.Field.Store.YES, PyLucene.Field.Index.TOKENIZED)) doc.add( PyLucene.Field('storeID', message.uniqueIdentifier(), PyLucene.Field.Store.YES, PyLucene.Field.Index.UN_TOKENIZED)) doc.add( PyLucene.Field('sortKey', message.sortKey(), PyLucene.Field.Store.YES, PyLucene.Field.Index.UN_TOKENIZED)) # Deprecated. use Field(name, value, Field.Store.YES, Field.Index.UN_TOKENIZED) instead self.writer.addDocument(doc) class PyLuceneIndexer(RemoteIndexer, item.Item): schemaVersion = 5 indexCount = attributes.integer(default=0) installedOn = attributes.reference() indexDirectory = attributes.text(default=LUCENE_INDEX_DIR) _index = attributes.inmemory() _lockfile = attributes.inmemory() def reset(self): """ In addition to the behavior of the superclass, delete any dangling lockfiles which may prevent this index from being opened. With the tested version of PyLucene (something pre-2.0), this appears to not actually be necessary: deleting the entire index directory but leaving the lockfile in place seems to still allow the index to be recreated (perhaps because when the directory does not exist, we pass True as the create flag when opening the FSDirectory, I am uncertain). Nevertheless, do this anyway for now. """ RemoteIndexer.reset(self) if hasattr(self, '_lockfile'): os.remove(self._lockfile) del self._lockfile def _analyzer(self): return PyLucene.StandardAnalyzer([]) if PyLucene is None: def openReadIndex(self): raise NotImplementedError("PyLucene is unavailable") def openWriteIndex(self): raise NotImplementedError("PyLucene is unavailable") else: def openReadIndex(self): luceneDir = self.store.newDirectory(self.indexDirectory) if not luceneDir.exists(): self.openWriteIndex().close() fsdir = PyLucene.FSDirectory.getDirectory(luceneDir.path, False) try: searcher = PyLucene.IndexSearcher(fsdir) except PyLucene.JavaError, e: raise IndexCorrupt() try: reader = PyLucene.IndexReader.open(fsdir) except PyLucene.JavaError, e: raise IndexCorrupt() return _PyLuceneReader(fsdir, self._analyzer(), reader, searcher) def openWriteIndex(self): luceneDir = self.store.newDirectory(self.indexDirectory) create = not luceneDir.exists() analyzer = self._analyzer() fsdir = PyLucene.FSDirectory.getDirectory(luceneDir.path, create) try: writer = PyLucene.IndexWriter(fsdir, analyzer, create) except PyLucene.JavaError, e: lockTimeout = u'Lock obtain timed out: Lock@' msg = e.getJavaException().getMessage() if msg.startswith(lockTimeout): self._lockfile = msg[len(lockTimeout):] raise IndexCorrupt() return _PyLuceneWriter(fsdir, analyzer, writer) def remoteIndexer1to2(oldIndexer): """ Previously external application code was responsible for adding a RemoteListener to a batch work source as a reliable listener. This precluded the possibility of the RemoteListener resetting itself unilaterally. With version 2, RemoteListener takes control of adding itself as a reliable listener and keeps track of the sources with which it is associated. This upgrader creates that tracking state. """ newIndexer = oldIndexer.upgradeVersion( oldIndexer.typeName, 1, 2, indexCount=oldIndexer.indexCount, installedOn=oldIndexer.installedOn, indexDirectory=oldIndexer.indexDirectory) listeners = newIndexer.store.query( batch._ReliableListener, batch._ReliableListener.listener == newIndexer) for listener in listeners: _IndexerInputSource( store=newIndexer.store, indexer=newIndexer, source=listener.processor) return newIndexer def remoteIndexer2to3(oldIndexer): """ The documentType keyword was added to all indexable items. Indexes need to be regenerated for this to take effect. Also, PyLucene no longer stores the text of messages it indexes, so deleting and re-creating the indexes will make them much smaller. """ newIndexer = oldIndexer.upgradeVersion( oldIndexer.typeName, 2, 3, indexCount=oldIndexer.indexCount, installedOn=oldIndexer.installedOn, indexDirectory=oldIndexer.indexDirectory) # the 3->4 upgrader for PyLuceneIndexer calls reset(), so don't do it # here. also, it won't work because it's a DummyItem if oldIndexer.typeName != PyLuceneIndexer.typeName: newIndexer.reset() return newIndexer def _declareLegacyIndexerItem(typeClass, version): item.declareLegacyItem(typeClass.typeName, version, dict(indexCount=attributes.integer(), installedOn=attributes.reference(), indexDirectory=attributes.text())) for cls in [HypeIndexer, XapianIndexer, PyLuceneIndexer]: _declareLegacyIndexerItem(cls, 2) registerUpgrader( remoteIndexer1to2, item.normalize(reflect.qual(cls)), 1, 2) registerUpgrader( remoteIndexer2to3, item.normalize(reflect.qual(cls)), 2, 3) del cls _declareLegacyIndexerItem(PyLuceneIndexer, 3) # Copy attributes. Rely on pyLuceneIndexer4to5 to reset the index due to # sorting changes. registerAttributeCopyingUpgrader(PyLuceneIndexer, 3, 4) _declareLegacyIndexerItem(PyLuceneIndexer, 4) def pyLuceneIndexer4to5(old): """ Copy attributes, reset index due because information about deleted documents has been lost, and power up for IFulltextIndexer so other code can find this item. """ new = old.upgradeVersion(PyLuceneIndexer.typeName, 4, 5, indexCount=old.indexCount, installedOn=old.installedOn, indexDirectory=old.indexDirectory) new.reset() new.store.powerUp(new, ixmantissa.IFulltextIndexer) return new registerUpgrader(pyLuceneIndexer4to5, PyLuceneIndexer.typeName, 4, 5) class _SQLiteResultWrapper(object): """ Trivial wrapper around SQLite FTS search results. """ def __init__(self, docId): self.uniqueIdentifier = docId class _SQLiteIndex(object): """ FTS3 index interface. """ addSQL = """ INSERT INTO fts (docid, content) VALUES (?, ?) """ removeSQL = """ DELETE FROM fts WHERE docid = ? """ searchSQL = """ SELECT docid FROM fts WHERE content MATCH ? ORDER BY docid %s """ def __init__(self, store): self.store = store self.close = self.store.close def add(self, document): """ Add a document to the database. """ docid = int(document.uniqueIdentifier()) text = u' '.join(document.textParts()) self.store.executeSQL(self.addSQL, (docid, text)) def remove(self, docid): """ Remove a document from the database. """ docid = int(docid) self.store.executeSQL(self.removeSQL, (docid,)) def search(self, term, keywords=None, sortAscending=True): """ Search the database. """ if sortAscending: direction = 'ASC' else: direction = 'DESC' return [_SQLiteResultWrapper(r[0]) for r in self.store.querySQL(self.searchSQL % (direction,), (term,))] class SQLiteIndexer(RemoteIndexer, item.Item): """ Indexer implementation using SQLite FTS3. XXX: Keywords are currently not supported; see #2877 """ indexCount = attributes.integer(default=0) indexDirectory = attributes.text(default=u'sqlite.index') _index = attributes.inmemory() schemaSQL = """ CREATE VIRTUAL TABLE fts USING fts3(content) """ def _getStore(self): """ Get the Store used for FTS. If it does not exist, it is created and initialised. """ storeDir = self.store.newDirectory(self.indexDirectory) if not storeDir.exists(): store = Store(storeDir) self._initStore(store) return store else: return Store(storeDir) def _initStore(self, store): """ Initialise a store for FTS use. """ store.createSQL('CREATE VIRTUAL TABLE fts USING fts3') def openReadIndex(self): return self.openWriteIndex() def openWriteIndex(self): return _SQLiteIndex(self._getStore()) PK9FO88xmantissa/interstore.py# -*- test-case-name: xmantissa.test.test_interstore -*- """ This module provides an implementation-agnostic mechanism for routing messages between users. Mantissa requires such a mechanism for any truly multi-user application, because Axiom partitions each user's account into its own store. For more detail on specifics of that separation, see the module documentation for L{axiom.userbase}. Let's say we wanted to write an application which allowed Alice to make an appointment with Bob . We'll call it 'Example App'. - Write the code for your Calendar item. It should implement L{IMessageReceiver} such that it can receive appointment requests. - Create these hypothetical calendar items in both Bob and Alice's stores. These should also be shared - specifically its IMessageReceiver interface must be shared, at least from Alice to Bob and from Bob to Alice. Give it a shareID qualified with the name of the application - for example, "exampleapp.root.calendar" - to avoid conflicts with other applications. - Install a L{MessageQueue} powerup for both alice and bob. - Now, Alice wants to request a meeting with bob. She should send a message to a target like C{Identifier(u'exampleapp.root.calendar', u'bob', u'elsewhere.example.com')}, and of course the sender should be C{Identifier(u'exampleapp.root.calendar', u'alice', u'somewhere.example.com')}, using her L{MessageQueue} powerup's L{queueMessage} method. Most applications will want to call this indirectly, via the high-level interface in L{AMPMessenger.messageRemote}. - Bob's Calendar item will receive this message. It parses the contents of the message into a structured meeting request and performs the appropriate transaction. You can use AMP formatting and argument parsing to format and parse message bodies by using the L{AMPMessenger} and L{AMPReceiver} classes provided here, respectively. - If Bob's calendar needs to respond to Alice's calendar to confirm, it can use the queueMessage function to reply. This model of inter-user communciation is a bit more work than simply manipulating a shared database, but the loose coupling it enforces provides a long list of advantages. System upgrades, for example, can be performed incrementally, one account at a time, and newer versions can talk to older versions using a compatible protocol. The message routing mechanism is pluggable, and it is possible to implement versions which talk to multiple Mantissa processes to take advantage of multiple CPU cores, multiple mounted disks, different Mantissa deployments across the internet, or multiple Mantissa nodes of the same application within a cluster. Although these things don't come "out of the box", the programming model here allows all of these changes to be made without changing any of your application code, just the high-level routing glue. """ from datetime import timedelta from zope.interface import implements from twisted.python import log from twisted.python.failure import Failure from twisted.internet import defer from twisted.protocols.amp import COMMAND, ERROR, Argument, Box, parseString from epsilon.structlike import record from epsilon.expose import Exposer from axiom.iaxiom import IScheduler from axiom.item import Item, declareLegacyItem from axiom.errors import UnsatisfiedRequirement from axiom.attributes import text, bytes, integer, AND, reference, inmemory from axiom.dependency import dependsOn, requiresFromSite from axiom.userbase import LoginSystem, LoginMethod from axiom.upgrade import registerUpgrader from xmantissa.ixmantissa import ( IMessageRouter, IDeliveryConsequence, IMessageReceiver) from xmantissa.error import ( MessageTransportError, BadSender, UnknownMessageType, RevertAndRespond, MalformedMessage) from xmantissa.sharing import getPrimaryRole, NoSuchShare, Identifier from xmantissa._recordattr import RecordAttribute, WithRecordAttributes DELIVERY_ERROR = u'mantissa.delivery.error' ERROR_NO_SHARE = 'no-share' ERROR_NO_USER = 'no-user' ERROR_REMOTE_EXCEPTION = 'remote-exception' ERROR_BAD_SENDER = 'bad-sender' _RETRANSMIT_DELAY = 120 class Value(record('type data')): """ A L{Value} is a combination of a data type and some data of that type. It is the content of a message. @ivar type: A short string describing the type of the data. Possible values include L{DELIVERY_ERROR}, L{AMP_MESSAGE_TYPE}, or L{AMP_ANSWER_TYPE}. @type type: L{unicode} @ivar data: The payload of the value, parsed according to rules identified by C{self.type}. @type data: L{str} """ class _QueuedMessage(Item, WithRecordAttributes): """ This is a message, queued in the sender's store, awaiting delivery to the target. """ senderUsername = text( """ This is the username of the user who is sending the message. """, allowNone=False) senderDomain = text( """ This is the domain name of the user who is sending the message. """, allowNone=False) senderShareID = text( """ This is the shareID of the shared item which is sending the message. """) sender = RecordAttribute(Identifier, [senderShareID, senderUsername, senderDomain]) targetUsername = text( """ This is the username of the user which is intended to receive the message. """, allowNone=False) targetDomain = text( """ This is the domain name fo the user which is intended to receive the message. """, allowNone=False) targetShareID = text( """ This is the target shareID object that the message will be delivered to in the foreign store. """, allowNone=False) target = RecordAttribute(Identifier, [targetShareID, targetUsername, targetDomain]) messageType = text( """ The content-type of the data stored in my L{messageData} attribute. """, allowNone=False) messageData = bytes( """ The data of the message. """, allowNone=False) value = RecordAttribute(Value, [messageType, messageData]) messageID = integer( """ An identifier for this message, unique to this store. """, allowNone=False) consequence = reference( """ A provider of L{IDeliveryConsequence} which will be invoked when the answer to this message is received. """, allowNone=True) class _FailedAnswer(Item, WithRecordAttributes): """ A record of an L{answerReceived} method raising an exception. There is no way for the system to report the failed processing of an answer to its peer (nor should there be), so this class allows a buggy answer receiver to remember the answer for later. """ consequence = reference( """ A provider of L{IDeliveryConsequence} which will be invoked when this _FailedAnswer is redelivered. """, allowNone=False) messageType = text( """ The content-type of the data stored in my L{messageData} attribute. """, allowNone=False) messageData = bytes( """ The data of the message. """, allowNone=False) messageValue = RecordAttribute(Value, [messageType, messageData]) answerType = text( """ The content-type of the data stored in my L{answerData} attribute. """, allowNone=False) answerData = bytes( """ The data of the answer. """, allowNone=False) answerValue = RecordAttribute(Value, [answerType, answerData]) senderUsername = text( """ This is the username of the user who is sending the message. """, allowNone=False) senderDomain = text( """ This is the domain name of the user who is sending the message. """, allowNone=False) senderShareID = text( """ This is the shareID of the shared item which is sending the message. """) sender = RecordAttribute(Identifier, [senderShareID, senderUsername, senderDomain]) targetUsername = text( """ This is the username of the user which is intended to receive the message. """, allowNone=False) targetDomain = text( """ This is the domain name fo the user which is intended to receive the message. """, allowNone=False) targetShareID = text( """ This is the target shareID object that the message will be delivered to in the foreign store. """, allowNone=False) target = RecordAttribute(Identifier, [targetShareID, targetUsername, targetDomain]) def redeliver(self): """ Re-deliver the answer to the consequence which previously handled it by raising an exception. This method is intended to be invoked after the code in question has been upgraded. Since there are no buggy answer receivers in production, nothing calls it yet. """ self.consequence.answerReceived(self.answerValue, self.messageValue, self.sender, self.target) self.deleteFromStore() class _AlreadyAnswered(Item, WithRecordAttributes): """ An L{AlreadyAnswered} is a persistent record of an answer to a message delivered via L{queueMessage}. This stays in the database until the original sender has sent a definitive acknowledgement to this answer, so that duplicate L{routeMessage} invocations do not cause duplicate application-level processing of the message. @ivar deliveryDeferred: a L{Deferred} in memory, representing a currently pending delivery attempt. This is L{None} if no delivery attempt is currently pending. """ deliveryDeferred = inmemory() def activate(self): """ Initialize in-memory state. """ self.deliveryDeferred = None originalSenderShareID = text( """ This is the shareID of the item which originally sent the message that this answer is in response to; the one that the answer is being delivered to. """, allowNone=True) originalSenderUsername = text( """ This is the localpart of the user's account which originally sent the message that this answer is in response to; the one that the answer is being delivered to. """, allowNone=False) originalSenderDomain = text( """ This is the domain name of the user's account which originally sent the message that this answer is in response to; the one that the answer is being delivered to. """, allowNone=False) originalSender = RecordAttribute(Identifier, [originalSenderShareID, originalSenderUsername, originalSenderDomain]) originalTargetShareID = text( """ This is the shareID of the original item which received the message that this answer in response to. This is the item that the response is being sent from. """, allowNone=False) originalTargetUsername = text( """ This is the localpart of the original account which received the message that this answer in response to. This is the user that the response is being sent from. """, allowNone=False) originalTargetDomain = text( """ This is the domain name of the original account which received the message that this answer in response to. This is the user that the response is being sent from. """, allowNone=False) originalTarget = RecordAttribute(Identifier, [originalTargetShareID, originalTargetUsername, originalTargetDomain]) messageID = integer( """ An identifier, unique to the original sender account name (username@domain) for this message. """, allowNone=False) answerType = text( """ Some text, identifying the type of the answer. This refers to the data in L{answerData} - this attribute will be set to L{DELIVERY_ERROR} if the answer could not be delivered. """, allowNone=False) answerData = bytes( """ The data returned from the original receiver of this answer. """, allowNone=False) value = RecordAttribute(Value, [answerType, answerData]) class _NullRouter(object): """ A null L{IMessageRouter} implementation, which drops messages on the floor. This is only used in the case where an L{IMessageRouter} powerup cannot be found on the site store to route messages to their recipients. """ implements(IMessageRouter) def routeAnswer(self, *a): """ Route an answer, but drop it on the floor. """ return defer.fail(MessageTransportError()) def routeMessage(self, *a): """ Route a message by dropping it on the floor. """ class LocalMessageRouter(record("loginSystem")): """ This is an item installed on the site store to route messages to appropriate users. This only routes messages between different stores that are referred to as accounts by a L{LoginSystem} on a single node. In other words it implements only local delivery. """ implements(IMessageRouter) def _routerForAccount(self, identifier): """ Locate an avatar by the username and domain portions of an L{Identifier}, so that we can deliver a message to the appropriate user. """ acct = self.loginSystem.accountByAddress(identifier.localpart, identifier.domain) return IMessageRouter(acct, None) def routeMessage(self, sender, target, value, messageID): """ Implement L{IMessageRouter.routeMessage} by synchronously locating an account via L{axiom.userbase.LoginSystem.accountByAddress}, and delivering a message to it by calling a method on it. """ router = self._routerForAccount(target) if router is not None: router.routeMessage(sender, target, value, messageID) else: reverseRouter = self._routerForAccount(sender) reverseRouter.routeAnswer(sender, target, Value(DELIVERY_ERROR, ERROR_NO_USER), messageID) def routeAnswer(self, originalSender, originalTarget, value, messageID): """ Implement L{IMessageRouter.routeMessage} by synchronously locating an account via L{axiom.userbase.LoginSystem.accountByAddress}, and delivering a response to it by calling a method on it and returning a deferred containing its answer. """ router = self._routerForAccount(originalSender) return router.routeAnswer(originalSender, originalTarget, value, messageID) def _accidentalSiteRouter(siteStore): """ Create an L{IMessageRouter} provider for an item in a user store accidentally opened as a site store. """ try: raise UnsatisfiedRequirement() except UnsatisfiedRequirement: log.err(Failure(), "You have opened a user's store as if it were a site store. " "Message routing is disabled.") return _NullRouter() def _createLocalRouter(siteStore): """ Create an L{IMessageRouter} provider for the default case, where no L{IMessageRouter} powerup is installed on the top-level store. It wraps a L{LocalMessageRouter} around the L{LoginSystem} installed on the given site store. If no L{LoginSystem} is present, this returns a null router which will simply log an error but not deliver the message anywhere, until this configuration error can be corrected. @rtype: L{IMessageRouter} """ ls = siteStore.findUnique(LoginSystem, default=None) if ls is None: try: raise UnsatisfiedRequirement() except UnsatisfiedRequirement: log.err(Failure(), "You have opened a substore from a site store with no " "LoginSystem. Message routing is disabled.") return _NullRouter() return LocalMessageRouter(ls) class MessageQueue(Item): """ A queue of outgoing L{QueuedMessage} objects. """ schemaVersion = 2 powerupInterfaces = (IMessageRouter,) siteRouter = requiresFromSite(IMessageRouter, _createLocalRouter, _accidentalSiteRouter) messageCounter = integer( """ This counter generates identifiers for outgoing messages. """, default=0, allowNone=False) def _scheduleMePlease(self): """ This queue needs to have its run() method invoked at some point in the future. Tell the dependent scheduler to schedule it if it isn't already pending execution. """ sched = IScheduler(self.store) if len(list(sched.scheduledTimes(self))) == 0: sched.schedule(self, sched.now()) def routeMessage(self, sender, target, value, messageID): """ Implement L{IMessageRouter.routeMessage} by locating a shared item which provides L{IMessageReceiver}, identified by L{target} in this L{MessageQueue}'s L{Store}, as shared to the specified C{sender}, then invoke its L{messageReceived} method. Then, take the results of that L{messageReceived} invocation and deliver them as an answer to the object specified by L{sender}. If any of these steps fail such that no L{IMessageReceiver.messageReceived} method may be invoked, generate a L{DELIVERY_ERROR} response instead. """ avatarName = sender.localpart + u"@" + sender.domain # Look for the sender. answer = self.store.findUnique( _AlreadyAnswered, AND(_AlreadyAnswered.originalSender == sender, _AlreadyAnswered.messageID == messageID), default=None) if answer is None: role = getPrimaryRole(self.store, avatarName) try: receiver = role.getShare(target.shareID) except NoSuchShare: response = Value(DELIVERY_ERROR, ERROR_NO_SHARE) else: try: def txn(): output = receiver.messageReceived(value, sender, target) if not isinstance(output, Value): raise TypeError("%r returned non-Value %r" % (receiver, output)) return output response = self.store.transact(txn) except RevertAndRespond, rar: response = rar.value except: log.err(Failure(), "An error occurred during inter-store " "message delivery.") response = Value(DELIVERY_ERROR, ERROR_REMOTE_EXCEPTION) answer = _AlreadyAnswered.create(store=self.store, originalSender=sender, originalTarget=target, messageID=messageID, value=response) self._deliverAnswer(answer) self._scheduleMePlease() def _deliverAnswer(self, answer): """ Attempt to deliver an answer to a message sent to this store, via my store's parent's L{IMessageRouter} powerup. @param answer: an L{AlreadyAnswered} that contains an answer to a message sent to this store. """ router = self.siteRouter if answer.deliveryDeferred is None: d = answer.deliveryDeferred = router.routeAnswer( answer.originalSender, answer.originalTarget, answer.value, answer.messageID) def destroyAnswer(result): answer.deleteFromStore() def transportErrorCheck(f): answer.deliveryDeferred = None f.trap(MessageTransportError) d.addCallbacks(destroyAnswer, transportErrorCheck) d.addErrback(log.err) def routeAnswer(self, originalSender, originalTarget, value, messageID): """ Route an incoming answer to a message originally sent by this queue. """ def txn(): qm = self._messageFromSender(originalSender, messageID) if qm is None: return c = qm.consequence if c is not None: c.answerReceived(value, qm.value, qm.sender, qm.target) elif value.type == DELIVERY_ERROR: try: raise MessageTransportError(value.data) except MessageTransportError: log.err(Failure(), "An unhandled delivery error occurred on a message" " with no consequence.") qm.deleteFromStore() try: self.store.transact(txn) except: log.err(Failure(), "An unhandled error occurred while handling a response to " "an inter-store message.") def answerProcessingFailure(): qm = self._messageFromSender(originalSender, messageID) _FailedAnswer.create(store=qm.store, consequence=qm.consequence, sender=originalSender, target=originalTarget, messageValue=qm.value, answerValue=value) qm.deleteFromStore() self.store.transact(answerProcessingFailure) return defer.succeed(None) def _messageFromSender(self, sender, messageID): """ Locate a previously queued message by a given sender and messageID. """ return self.store.findUnique( _QueuedMessage, AND(_QueuedMessage.senderUsername == sender.localpart, _QueuedMessage.senderDomain == sender.domain, _QueuedMessage.messageID == messageID), default=None) def _verifySender(self, sender): """ Verify that this sender is valid. """ if self.store.findFirst( LoginMethod, AND(LoginMethod.localpart == sender.localpart, LoginMethod.domain == sender.domain, LoginMethod.internal == True)) is None: raise BadSender(sender.localpart + u'@' + sender.domain, [lm.localpart + u'@' + lm.domain for lm in self.store.query( LoginMethod, LoginMethod.internal == True)]) def queueMessage(self, sender, target, value, consequence=None): """ Queue a persistent outgoing message. @param sender: The a description of the shared item that is the sender of the message. @type sender: L{xmantissa.sharing.Identifier} @param target: The a description of the shared item that is the target of the message. @type target: L{xmantissa.sharing.Identifier} @param consequence: an item stored in the same database as this L{MessageQueue} implementing L{IDeliveryConsequence}. """ self.messageCounter += 1 _QueuedMessage.create(store=self.store, sender=sender, target=target, value=value, messageID=self.messageCounter, consequence=consequence) self._scheduleMePlease() def run(self): """ Attmept to deliver the first outgoing L{QueuedMessage}; return a time to reschedule if there are still more retries or outgoing messages to send. """ delay = None router = self.siteRouter for qmsg in self.store.query(_QueuedMessage, sort=_QueuedMessage.storeID.ascending): try: self._verifySender(qmsg.sender) except: self.routeAnswer(qmsg.sender, qmsg.target, Value(DELIVERY_ERROR, ERROR_BAD_SENDER), qmsg.messageID) log.err(Failure(), "Could not verify sender for sending message.") else: router.routeMessage(qmsg.sender, qmsg.target, qmsg.value, qmsg.messageID) for answer in self.store.query(_AlreadyAnswered, sort=_AlreadyAnswered.storeID.ascending): self._deliverAnswer(answer) nextmsg = self.store.findFirst(_QueuedMessage, default=None) if nextmsg is not None: delay = _RETRANSMIT_DELAY else: nextanswer = self.store.findFirst(_AlreadyAnswered, default=None) if nextanswer is not None: delay = _RETRANSMIT_DELAY if delay is not None: return IScheduler(self.store).now() + timedelta(seconds=delay) declareLegacyItem( MessageQueue.typeName, 1, dict(messageCounter=integer(default=0, allowNone=False), scheduler=reference())) def upgradeMessageQueue1to2(old): """ Copy the C{messageCounter} attribute to the upgraded MessageQueue. """ return old.upgradeVersion( MessageQueue.typeName, 1, 2, messageCounter=old.messageCounter) registerUpgrader(upgradeMessageQueue1to2, MessageQueue.typeName, 1, 2) #### High-level convenience API #### AMP_MESSAGE_TYPE = u'mantissa.amp.message' AMP_ANSWER_TYPE = u'mantissa.amp.answer' class _ProtoAttributeArgument(Argument): """ Common factoring of L{TargetArgument} and L{SenderArgument}, for reading an attribute from the 'proto' attribute. @ivar attr: the name of the attribute to retrieve from the C{proto} argument to L{_ProtoAttributeArgument.fromBox}. """ def fromBox(self, name, strings, objects, proto): """ Retreive an attribute from the C{proto} parameter. """ objects[name] = getattr(proto, self.attr) def toBox(self, name, strings, objects, proto): """ Do nothing; these argument types are for specifying out-of-band information not in the message body, so leave the message body alone when sending. """ class TargetArgument(_ProtoAttributeArgument): """ An AMP L{Argument} which places an L{Identifier} for the target (for commands) or original target (for answers) of the message being processed into the argument list. """ attr = "target" class SenderArgument(_ProtoAttributeArgument): """ An AMP L{Argument} which places an L{Identifier} for the sender (for commands) or original sender (for answers) of the message being processed into the argument list. """ attr = "sender" class _ProtocolPlaceholder(record("sender target")): """ This placeholder object is passed as the 'proto' object to AMP parsing methods, and has the two out-of-band attributes required to support L{SenderArgument} and L{TargetArgument}, which are about all you can use it for. """ class AMPMessenger(record("queue sender target")): """ An L{AMPMessenger} is a conduit between an object sending a message (identified by the C{queue} and C{sender} arguments) and a recipient of that message (represented by the C{target}) argument. @ivar queue: a L{MessageQueue} that will be used to queue messages. @ivar sender: an L{Identifier} that will be used as the sender of the messages. @ivar target: an L{Identifier} that will be used as the target of messages. """ def messageRemote(self, cmdObj, consequence=None, **args): """ Send a message to the peer identified by the target, via the given L{Command} object and arguments. @param cmdObj: a L{twisted.protocols.amp.Command}, whose serialized form will be the message. @param consequence: an L{IDeliveryConsequence} provider which will handle the result of this message (or None, if no response processing is desired). @param args: keyword arguments which match the C{cmdObj}'s arguments list. @return: L{None} """ messageBox = cmdObj.makeArguments(args, self) messageBox[COMMAND] = cmdObj.commandName messageData = messageBox.serialize() self.queue.queueMessage(self.sender, self.target, Value(AMP_MESSAGE_TYPE, messageData), consequence) class _AMPExposer(Exposer): """ An L{Exposer} whose purpose is to expose objects via L{Command} objects. """ def expose(self, commandObject): """ Declare a method as being related to the given command object. @param commandObject: a L{Command} subclass. """ thunk = super(_AMPExposer, self).expose(commandObject.commandName) def thunkplus(function): result = thunk(function) result.command = commandObject return result return thunkplus def responderForName(self, instance, commandName): """ When resolving a command to a method from the wire, the information available is the command's name; look up a command. @param instance: an instance of a class who has methods exposed via this exposer's L{_AMPExposer.expose} method. @param commandName: the C{commandName} attribute of a L{Command} exposed on the given instance. @return: a bound method with a C{command} attribute. """ method = super(_AMPExposer, self).get(instance, commandName) return method class _AMPErrorExposer(Exposer): """ An L{Exposer} whose purpose is to expose objects via L{Command} objects and error identifiers. """ def expose(self, commandObject, exceptionType): """ Expose a function for processing a given AMP error. """ thunk = super(_AMPErrorExposer, self).expose( (commandObject.commandName, commandObject.errors.get(exceptionType))) def thunkplus(function): result = thunk(function) result.command = commandObject result.exception = exceptionType return result return thunkplus def errbackForName(self, instance, commandName, errorName): """ Retrieve an errback - a callable object that accepts a L{Failure} as an argument - that is exposed on the given instance, given an AMP commandName and a name in that command's error mapping. """ return super(_AMPErrorExposer, self).get(instance, (commandName, errorName)) commandMethod = _AMPExposer(""" Use this exposer to expose methods on L{AMPReceiver} subclasses which can respond to AMP commands. Use like so:: @commandMethod.expose(YourCommand) def yourMethod(self, yourCommandArgument, ...): ... """) answerMethod = _AMPExposer(""" Use this exposer to expose methods on L{AMPReceiver} subclasses which can deal with AMP command responses. Use like so:: @answerMethod.expose(YourCommand) def yourMethod(self, yourCommandResponseArgument, ...): ... """) errorMethod = _AMPErrorExposer(""" Use this exposer to expose methods on L{AMPReceiver} subclasses which can deal with AMP command error responses. Use like so:: @errorMethod.expose(YourCommand, YourCommandException) def yourMethod(self, failure): ... """) class AMPReceiver(object): """ This is a mixin for L{Item} objects which wish to implement L{IMessageReceiver} and/or L{IDeliveryConsequence} by parsing the bodies of arriving messages and answers as AMP boxes. To implement L{IMessageReceiver}, use the L{commandMethod} decorator. To implement L{IMessageRouter}, use the L{answerMethod} and L{errorMethod} decorators. For example:: class MyCommand(Command): arguments = [('hello', Integer())] response = [('goodbye', Text())] class MyResponder(Item, AMPReceiver): ... @commandMethod.expose(MyCommand) def processCommand(self, hello): return dict(goodbye=u'goodbye!') @answerMethod.expose(MyCommand) def processAnswer(self, goodbye): # process 'goodbye' here. In this example, a L{MyResponder} object might be shared and used as a target for a call to L{AMPMessenger.messageRemote} with L{MyCommand}, or used as the L{consequence} argument to that same call. """ implements(IMessageReceiver, IDeliveryConsequence) def _boxFromData(self, messageData): """ A box. @param messageData: a serialized AMP box representing either a message or an error. @type messageData: L{str} @raise MalformedMessage: if the C{messageData} parameter does not parse to exactly one AMP box. """ inputBoxes = parseString(messageData) if not len(inputBoxes) == 1: raise MalformedMessage() [inputBox] = inputBoxes return inputBox def messageReceived(self, value, sender, target): """ An AMP-formatted message was received. Dispatch to the appropriate command responder, i.e. a method on this object exposed with L{commandMethod.expose}. @see IMessageReceiver.messageReceived """ if value.type != AMP_MESSAGE_TYPE: raise UnknownMessageType() inputBox = self._boxFromData(value.data) thunk = commandMethod.responderForName(self, inputBox[COMMAND]) placeholder = _ProtocolPlaceholder(sender, target) arguments = thunk.command.parseArguments(inputBox, placeholder) try: result = thunk(**arguments) except tuple(thunk.command.errors.keys()), knownError: errorCode = thunk.command.errors[knownError.__class__] raise RevertAndRespond( Value(AMP_ANSWER_TYPE, Box(_error_code=errorCode, _error_description=str(knownError)).serialize())) else: response = thunk.command.makeResponse(result, None) return Value(AMP_ANSWER_TYPE, response.serialize()) def answerReceived(self, value, originalValue, originalSender, originalTarget): """ An answer was received. Dispatch to the appropriate answer responder, i.e. a method on this object exposed with L{answerMethod.expose}. @see IDeliveryConsequence.answerReceived """ if value.type != AMP_ANSWER_TYPE: raise UnknownMessageType() commandName = self._boxFromData(originalValue.data)[COMMAND] rawArgs = self._boxFromData(value.data) placeholder = _ProtocolPlaceholder(originalSender, originalTarget) if ERROR in rawArgs: thunk = errorMethod.errbackForName(self, commandName, rawArgs[ERROR]) thunk(Failure(thunk.exception())) else: thunk = answerMethod.responderForName(self, commandName) arguments = thunk.command.parseResponse(rawArgs, placeholder) thunk(**arguments) __all__ = ['AMPMessenger', 'AMPReceiver', 'AMP_ANSWER_TYPE', 'AMP_MESSAGE_TYPE', 'DELIVERY_ERROR', 'ERROR', 'ERROR_BAD_SENDER', 'ERROR_NO_SHARE', 'ERROR_NO_USER', 'ERROR_REMOTE_EXCEPTION', 'LocalMessageRouter', 'MessageQueue', 'SenderArgument', 'TargetArgument', 'Value', 'answerMethod', 'commandMethod', 'errorMethod'] PK9F:!!xmantissa/ixmantissa.py# -*- test-case-name: xmantissa.test -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Public interfaces used in Mantissa. """ from zope.interface import Interface, Attribute from nevow.inevow import IRenderer class IColumn(Interface): """ Represents a column that can be viewed via a scrolling table, and provides hints & metadata about the column. """ def sortAttribute(): """ @return: a sortable axiom.attribute, or None if this column cannot be sorted """ def extractValue(model, item): """ @type model: L{xmantissa.tdb.TabularDataModel} @param item: the L{axiom.item.Item} from which to extract column value @return: the underlying value for this column """ def getType(): """ returns a string describing the type of this column, or None """ def toComparableValue(value): """ Convert a value received from the client into one that can be compared like-for-like with L{sortAttribute}, when executing an axiom query. (Callers should note that this is new as of Mantissa 0.6.6, and be prepared to deal with its absence in legacy code.) """ attributeID = Attribute( """ An ASCII-encoded str object uniquely describing this column. """) class ITemplateNameResolver(Interface): """ Loads Nevow document factories from a particular theme based on simple string names. """ def getDocFactory(name, default=None): """ Retrieve a Nevow document factory for the given name. @param name: a short string that names a fragment template for development purposes. @return: a Nevow docFactory """ class IPreferenceAggregator(Interface): """ Allows convenient retrieval of individual preferences """ def getPreferenceCollections(): """ Return a list of all installed L{IPreferenceCollection}s """ def getPreferenceValue(key): """ Return the value of the preference associated with "key" """ class ISearchProvider(Interface): """ Represents an Item capable of searching for things """ def count(term): """ Return the number of items currently associated with the given (unprocessed) search string """ def search(term, keywords=None, count=None, offset=0, sortAscending=True): """ Query for items which contain the given term. @type term: C{unicode} @param keywords: C{dict} mapping C{unicode} field name to C{unicode} field contents. Search results will be limited to documents with fields of these names containing these values. @type count: C{int} or C{NoneType} @type offset: C{int}, default is 0 @param sortAscending: should the results be sorted ascendingly @type sortAscending: boolean @rtype: L{twisted.internet.defer.Deferred} @return: a Deferred which will fire with an iterable of L{search.SearchResult} instances, representing C{count} results for the unprocessed search represented by C{term}, starting at C{offset}. The bounds of offset and count will be within the value last returned from L{count} for this term. """ class ISearchAggregator(Interface): """ An Item responsible for interleaving and displaying search results obtained from available ISearchProviders """ def count(term): """ same as ISearchProvider.count, but queries all search providers """ def search(term, keywords, count, offset, sortAscending): """ same as ISearchProvider.search, but queries all search providers """ def providers(): """ returns the number of available search providers """ class IFulltextIndexer(Interface): """ A general interface to a low-level full-text indexer. """ def add(document): """ Add the given document to this index. This method may only be called in the batch process (it will synchronously invoke an indexer method which may block or cause a segfault). """ def remove(document): """ Remove the given document from this index. This method may be called from any process. """ class IFulltextIndexable(Interface): """ Something which can be indexed for later search. """ def uniqueIdentifier(): """ @return: a C{str} uniquely identifying this item. """ def textParts(): """ @return: an iterable of unicode strings to be indexed as the text of this item. """ def keywordParts(): """ @return: a C{dict} mapping C{str} to C{unicode} of additional metadata. It will be possible to search on these fields using L{ISearchAggregator.search}. """ def documentType(): """ @return: a C{str} uniquely identifying the type of this item. Like the return value of L{keywordParts}, it will be possible to search for this using the C{"documentType"} key in the C{keywords} argument to L{ISearchAggregator.search}. """ def sortKey(): """ @return: A unicode string that will be used as the key when sorting search results comprised of items of this type. """ class ISiteURLGenerator(Interface): """ Lowest-level APIs for generating URLs which refer to parts of a Mantissa site. """ def cleartextRoot(hostname=None): """ Return the HTTP URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of other considerations. @rtype: L{nevow.url.URL} """ def encryptedRoot(hostname=None): """ Return the HTTPS URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of other considerations. @rtype: L{nevow.url.URL} """ def rootURL(request): """ Return the URL for the root of this website which is appropriate to use in links generated in response to the given request. @type request: L{twisted.web.http.Request} @param request: The request which is being responded to. @rtype: L{URL} @return: The location at which the root of the resource hierarchy for this website is available. NOTE: This function may take an URL (which is the "base" URL, relative to which all links in the ultimate view context will be interpreted) instead of a Request in the future. """ class IStaticShellContent(Interface): """ Represents per-store header/footer content that's used to buttress the shell template """ def getHeader(): """ Returns stan to be added to the page header. Can return None if no header is desired. """ def getFooter(): """ Returns stan to be added to the page footer. Can return None if no footer is desired. """ class IViewer(Interface): def roleIn(userStore): """ Retrieve a L{xmantissa.sharing.Role} object for the user that this viewer represents in the provided user-store. @param userStore: a store that contains some sharing roles. @type userStore: L{axiom.store.Store} @rtype: L{xmantissa.sharing.Role} """ class IWebViewer(IViewer): """ An object that provides navigation bits for web content produced by Mantissa applications. """ def wrapModel(model): """ Converts application-provided model objects to L{IResource} providers. @param model: An L{Item} or L{SharedProxy}. """ class ISiteRootPlugin(Interface): """ Plugin Interface for functionality provided at the root of the website. This interface is queried for on the Store by website.WebSite when processing an HTTP request. Things which are installed on a Store using s.powerUp(x, ISiteRootPlugin) will be visible to individual users when installed on a user's store or visible to the general public when installed on a top-level store. """ def resourceFactory(segments): """ This is deprecated; implement L{produceResource} instead. Get an object that provides IResource. @type segments: list of str, representing decoded requested URL segments @return: None or a two-tuple of the IResource provider and the segments to pass to its locateChild. """ del resourceFactory # It's deprecated, so when we verifyObject, we # don't want to check for this. def produceResource(request, segments, webViewer): """ Get an object that provides IResource. @param request: An L{IRequest}. @type segments: list of str, representing decoded requested URL segments @param webViewer: An L{IWebViewer}. @return: None or a two-tuple of the IResource provider and the segments to pass to its locateChild. """ class IMantissaSite(Interface): # XXX this documentation is terrible, rephrase to describe something # abstract. """ This is different from ISiteRootPlugin because it is invoked in a different context. ISiteRootPlugin is a plugin powerup interface that lots of different things can provide. This is an interface that only the site needs to provide, for L{CustomizedPublicPage} to do stuff with. """ def siteProduceResource(request, segments, webViewer): """ Give me a resource based on a bunch of ISiteRootPlugin powerups. """ class ISessionlessSiteRootPlugin(Interface): """ L{ISessionlessSiteRootPlugin} powerups installed on a site store are powerups which can produce a resource to respond to a particular request, even if the browser in question has no session. This powerup interface exists mainly to allow applications to provide resources which are not subject to the redirect that L{nevow.guard} introduces. This can be important for interacting with limited user-agents that do not support cookies or redirects. However, this is a temporary workaround, as L{nevow.guard} should have this redirect requirement removed. See ticket #2494 for details. """ def sessionlessProduceResource(request, segments): """ Return a 2-tuple of C{(resource, segments)} if a resource can be found to match this request and its segments, or None. """ def resourceFactory(segments): """ This is deprecated; implement L{sessionlessProduceResource} instead. Get an object that provides IResource. @type segments: list of str, representing decoded requested URL segments @return: None or a two-tuple of the IResource provider and the segments to pass to its locateChild. """ del resourceFactory # It's deprecated, so when we verifyObject, we # don't want to check for this. class ICustomizable(Interface): """ Factory for creating IResource objects which can be customized for a specific user. """ def customizeFor(avatarName): """ Retrieve a IResource provider specialized for the given avatar. @type avatarName: C{unicode} @param avatarName: The user for whom to return a specialized resource. @rtype: C{IResource} @return: A public-page resource, possibly customized for the indicated user. """ class ICustomizablePublicPage(Interface): """ Don't use this. Delete it if you notice it still exists but upgradePublicWeb2To3 has been removed. """ class IWebTranslator(Interface): """ Provide methods for naming objects on the web, and vice versa. """ def fromWebID(webID): """ @param webID: A string that identifies an item through this translator. @return: an Item, or None if no Item is found. """ def toWebID(item): """ @param item: an item in the same store as this translator. @return: a string, shorter than 80 characters, which is an opaque identifier that may be used to look items up through this translator using fromWebID (or the legacy 'linkFrom') """ def linkTo(storeID): """ @param storeID: The Store ID of an Axiom item. @rtype: C{str} @return: An URL which refers to the item with the given Store ID. """ def linkFrom(webID): """ The inverse of L{linkTo} """ class INavigableElement(Interface): """Tab interface used by the web navigation plugin system. Plugins for this interface are retrieved when generating the navigation user-interface. Each result has C{getTabs} invoked, after which the results are merged and the result used to construct various top- and secondary-level \"tabs\" which can be used to visit different parts of the application. """ def getTabs(): """Retrieve data about this elements navigation. This returns a list of C{xmantissa.webnav.Tab}s. For example, a powerup which wanted to install navigation under the Divmod tab would return this list::: [Tab("Divmod", self.storeID, 1.0 children=[ Tab("Summary", self.storeID, 1.0), Tab("Inbox", self.inbox.storeID, 0.8) ])] """ class INavigableFragment(Interface): """ Register an adapter to this interface in order to provide web UI content within the context of the 'private' application with navigation, etc. You will still need to produce some UI by implementing INavigableElement and registering a powerup for that as well, which allows users to navigate to this object. The primary requirement of this interface is that providers of it also provide L{nevow.inevow.IRenderer}. The easiest way to achieve this is to subclass L{nevow.page.Element}. """ title = Attribute( """ The title of this fragment, which will be used in the tag of the page displaying it, or otherwise outside the content area of the fragment but associated with it. """) fragmentName = Attribute( """ The name of this fragment; a string used to look up the template from the current theme(s). This is done by implementors of L{IWebViewer.wrapModel}. This attribute may be set to None or left unset if you do not want this type of customization. However, if you do not set it, you must set a docFactory yourself, and your docFactory will not be changed to accommodate the user's preferred theme. """) docFactory = Attribute( """ Nevow-style docFactory object. Must be set if fragmentName is not. """) def head(): """ Provide some additional content to be included in the <head> section of the page when this fragment is being rendered. May return None if nothing needs to be added there. This method is optional. If not implemented, nothing will be added to the head. """ def locateChild(ctx, segments): """ INavigableFragments may optionally provide a locateChild method similar to the one found on L{nevow.inevow.IResource.locateChild}. You may implement this method if your INavigableFragment contains any resources which it may need to refer to with hyperlinks when rendered. Please note that an INavigableFragment may be rendered on any page within an application, and that hyperlinks to resources returned from this method must always be to /private/<your-webid>/..., not the current page's URL, if you are using the default L{xmantissa.webapp.PrivateApplication} URL dispatcher. (There is a slight bug in the calling code's handling of Deferreds. If you wish to delegate to normal child-resource handling, you must return rend.NotFound exactly, not a Deferred which fires it.) """ def setFragmentParent(fragmentParent): """ Sets the L{LiveFragment} (or L{LivePage}) which is the logical parent of this fragment. See L{nevow.athena._LiveMixin.setFragmentParent}'s docstring for more information. """ def customizeFor(userID): """ This method is optional. If not provided, it is the same as returning 'self'. When a logged-in user is viewing an L{INavigableFragment} provider, this method will be invoked with that user's ID, and the returned navigable fragment will be presented to the user instead. @param userID: a user@host formatted string, indicating what user is viewing this. @type userID: L{unicode} @return: a fragment customized for the provided user-ID. @rtype: L{INavigableFragment} """ class ITab(Interface): """ Abstract, non-UI representation of a tab that shows up in the UI. The only concrete representation is xmantissa.webnav.Tab """ class IMessageReceiver(Interface): """ An L{IMessageReceiver} is an object that can receive messages via inter-store messaging, L{xmantissa.interstore}. Share an item with this interface and it will be able to receive messages queued with L{xmantissa.interstore.MessageQueue.queueMessage}. """ def messageReceived(value, sender, target): """ @param value: A value to be sent as the body of the message. @type value: L{xmantissa.interstore.Value} @param sender: a L{xmantissa.sharing.Identifier}, identifying the sender of the message being received. @param target: a L{xmantissa.sharing.Identifier}, identifying the object receiving the message, i.e. self. @return: a value for the response. @rtype: L{xmantissa.interstore.Value} """ class IDeliveryConsequence(Interface): """ A provider of L{IDeliveryConsequence} can receive notifications of messages being successfully handled by a remote store via L{IMessageReceiver.messageReceived}. Providers of this interface must also be L{Item}s in Axiom stores so that they can persist between process invocations along with the queued message it is waiting for a response to. """ def answerReceived(value, originalValue, originalSender, originalTarget): """ An answer was received to a message sent via L{xmantissa.messaging.MessageQueue.queueMessage} with this L{IDeliveryConsequence} provider as its consequence. @param value: the value of the answer. @type value: L{xmantissa.interstore.Value} @param originalValue: the C{value} argument passed to the original L{IMessageReceiver.messageReceived} that this is a response to. @type originalData: L{xmantissa.interstore.Value} @param originalSender: the C{sender} argument passed to the original L{IMessageReceiver.messageReceived} that this is a response to. @param originalTarget: the C{target} argument passed to the original L{IMessageReceiver.messageReceived} that this is a response to. """ class IMessageRouter(Interface): """ An L{IMessageRouter} is an object that can route messages between different users. No guarantees are provided; all message routing is potentially unreliable. The suggested API for applications is L{xmantissa.interstore.AMPMessenger.messageRemote}. Applications with specialized serialization needs might use the medium-level L{xmantissa.messaging.MessageQueue.queueMessage} instead. It is unlikely that you will want to use this interface unless you are implementing your own routing mechanism. Any user of this interface should take care to test the failure cases, since most transports which implement this interface will, in practice, be (statistically speaking) extremely reliable and fail only in the most obscure cases. Application code should always be using something higher level, since this interface provides no mechanism for transactionality guarantees, and different providers may only know how to route to a subset of all possible destinations. For example, the implementation installed on a user store will only know how to route to that user. """ def routeMessage(sender, target, value, messageID): """ Route a message to the given target. @param sender: The description of the shared item that is the sender of the message. @type sender: L{xmantissa.sharing.Identifier} @param target: The description of the shared item that is the target of the message. @type target: L{xmantissa.sharing.Identifier} @param messageID: An identifier for the message, unique to a given sender. @type messageID: L{int} @param value: The value of the message to be delivered. @type value: L{xmantissa.interstore.Value} """ def routeAnswer(originalSender, originalTarget, value, messageID): """ Route an answer to a message previously queued to a particular user. @param originalSender: The original sender of the message; in this case, the target of the answer. @type originalSender: L{xmantissa.sharing.Identifier} @param originalTarget: The original target of the message; in this case; the target of the answer. @type originalTarget: L{xmantissa.sharing.Identifier} @param messageID: The unique identifier for the message, as passed to L{IMessageRouter.routeMessage}. @type messageID: L{int} @param value: The value of the answer to be delivered. This is not the value of the original message, but a separate value describing the result of processing the message. @type value: L{xmantissa.interstore.Value} @return: a L{Deferred} which fires with None if the message is successfully delivered, or fails with L{MessageTransportError} if the message could not be delivered. """ class IBenefactor(Interface): """ Make accounts for users and give them things to use. """ def endow(ticket, avatar): """ Make a user and return it. Give the newly created user new powerups or other functionality. This is only called when the user has confirmed the email address passed in by receiving a message and clicking on the link in the provided email. """ def deprive(ticket, avatar): """ Remove the increment of functionality or privilege that we have previously bestowed upon the indicated avatar. """ class IBenefactorFactory(Interface): """A factory which describes and creates IBenefactor providers. """ def dependencies(): """ Return an iterable of other IBenefactorFactory providers that this one depends upon, and must be installed before this one is invoked. """ def parameters(): """ Return a description of keyword parameters to be passed to instantiate. @rtype: A list of 4-tuples. The first element of each tuple is a keyword argument to L{instantiate}. The second describes the type of prompt to present for this field. The third is a one-argument callable will should be invoked with a string the user supplies and should return the value for this keyword argument. The fourth is a description of the purpose of this keyword argument. """ def instantiate(**kw): """ Create an IBenefactor provider and return it. """ class IQ2QService(Interface): q2qPortNumber = Attribute( """ The TCP port number on which to listen for Q2Q connections. """) inboundTCPPortNumber = Attribute( """ The TCP port number on which to listen for Q2Q data connections. """) publicIP = Attribute( """ Dotted-quad format string representing the IP address via which this service is exposed to the public internet. """) udpEnabled = Attribute( """ A boolean indicating whether or not PTCP connections will be allowed or attempted. """) def listenQ2Q(fromAddress, protocolsToFactories, serverDescription): """ @see: L{vertex.q2q.Q2QService.connectQ2Q} """ def connectQ2Q(fromAddress, toAddress, protocolName, protocolFactory, usePrivateCertificate=None, fakeFromDomain=None, chooser=None): """ @see: L{vertex.q2q.Q2QService.connectQ2Q} """ class IPreferenceCollection(Interface): """ I am an item that groups preferences into logical chunks. """ def getPreferences(): """ Returns a mapping of preference-name->preference-value. """ def getSections(): """ Returns a sequence of INavigableFragments or None. These fragments will be displayed alongside preferences under this collection's settings group. """ def getPreferenceAttributes(): """ Returns a sequence of L{xmantissa.liveform.Parameter} instances - one for each preference. The names of the parameters should correspond to the attribute names of the preference attributes on this item. """ def getTabs(): """ Like L{ixmantissa.INavigableElement.getTabs}, but for preference tabs """ class ITemporalEvent(Interface): """ I am an event which happens at a particular time and has a specific duration. """ startTime = Attribute(""" An extime.Time. The start-point of this event. """) endTime = Attribute(""" An extime.Time. The end-point of this event. """) class IDateBook(Interface): """ A source of L{IAppointment}s which have times associated with them. """ def eventsBetween(startTime, endTime): """ Retrieve events which overlap a particular range. @param startTime: an L{epsilon.extime.Time} that begins a range. @param endTime: an L{epsilon.extime.Time} that ends a range. @return: an iterable of L{ITemporalEvent} providers. """ class IContactType(Interface): """ A means by which communication with a L{Person} might occur. For example, a telephone number. """ allowMultipleContactItems = Attribute(""" C{bool} indicating whether more than one contact item of this type can be created of a particular L{Person}. """) def getParameters(contactInfoItem): """ Return some liveform parameters, one for each piece of information that is needed to construct a contact info item of this type. If C{contactInfoItem} is supplied, implementations may return C{None} to indicate that the given contact item is not editable. @param contactInfoItem: An existing contact info item of this type, or C{None}. If not C{None}, then the current values of the contact info type will be used to provide suitable defaults for the parameters that are returned. @type contactInfoItem: L{axiom.item.Item} subclass. @return: Some liveform parameters or C{None}. @rtype: C{NoneType} or C{list} of L{xmantissa.liveform.Parameter}. """ def createContactItem(person, **parameters): """ Create a new instance of this contact type for the given person. @type person: L{Person} @param person: The person to whom the contact item pertains. @param parameters: The form input key/value pairs as returned by the L{xmantissa.liveform.LiveForm} constructed from L{getParameters}'s parameter instances. @return: The created contact item or C{None} if one was not created for any reason. """ def getContactItems(person): """ Return an iterator of contact items created by this contact type for the given person. @type person: L{Person} @param person: The person to whom the contact item pertains. """ def uniqueIdentifier(): """ Return a C{unicode} string which, for the lifetime of a single Python process, uniquely identifies this type of contact information. """ def descriptiveIdentifier(): """ A descriptive name for this type of contact information. @rtype: C{unicode} """ def getEditFormForPerson(person): """ Return a L{LiveForm} which will allow the given person's contact items to be edited. @type person: L{xmantissa.people.Person} @rtype: L{xmantissa.liveform.LiveForm} """ def editContactItem(contact, **parameters): """ Update the given contact item to reflect the new parameters. @param **parameters: The form input key/value pairs, as produced by the L{LiveForm} returned by L{getEditFormForPerson}. """ def getContactGroup(contactItem): """ Return a L{xmantissa.people.ContactGroup} describing the group affinity of a contact item of the type created by this contact type, or C{None} if the item doesn't have an explicit group. @param contactItem: A contact item. @rtype: L{xmantissa.people.ContactGroup} or C{NoneType} """ def getReadOnlyView(contact): """ Return an L{IRenderer} which will display the given contact. """ class IPeopleFilter(Interface): """ Object which collects L{Person} items into a logical group. """ filterName = Attribute(""" The name of this filter; something which describes the type of people included in its group.""") def getPeopleQueryComparison(store): """ Return a query comparison describing the subset of people in the given store which are included in this group. @type store: L{axiom.store.Store} @rtype: L{axiom.iaxiom.IComparison} """ class IOrganizerPlugin(Interface): """ Powerup which provides additional functionality to Mantissa People. Organizer plugins add support for new kinds of person data (for example, one Organizer plugin might add support for contact information: physical addresses, email addresses, telephone numbers, etc. Another plugin might retrieve and aggregate blog posts, or provide an interface for configuring sharing permissions). """ name = Attribute('The C{unicode} display name of this plugin.') def getContactTypes(): """ Return an iterator of L{IContactType} providers supplied by this plugin. """ def getPeopleFilters(): """ Return an iterator of L{IPeopleFilter} providers supplied by this plugin. """ def personCreated(person): """ Called when a new L{Person} is created. """ def personNameChanged(person, oldName): """ Called after a L{Person} item's name has been changed. @type person: L{Person} @param person: The person whose name is being changed. @type oldName: C{unicode} @param oldName: The previous value of L{{Person.name}. """ def contactItemCreated(contact): """ Called when a new contact item is created. @param contact: The new contact item. It may be any object returned by an L{IContactType.createContactItem} implementation. """ def contactItemEdited(contact): """ Called when an existing contact item has been edited. @param contact: The contact item. """ def personalize(person): """ Return some plugin-specific state for the given person. @param person: A L{xmantissa.person.Person} instance. @return: Something renderable by Nevow. """ class IPersonFragment(Interface): """ Deprecated. Nothing in Mantissa cares about this interface. """ class IOffering(Interface): """ Describes a product, service, application, or other unit of functionality which can be added to a Mantissa server. """ name = Attribute(""" What it is called. """) description = Attribute(""" What it is. """) siteRequirements = Attribute(""" A list of 2-tuples of (interface, powerupClass) of Axiom Powerups which will be installed on the Site store when this offering is installed if the store cannot be adapted to the given interface. """) appPowerups = Attribute(""" A list of Axiom Powerups which will be installed on the App store when this offering is installed. May be None if no App store is required (in this case, none will be created). """) installablePowerups = Attribute(""" A C{list} of three-tuples each of which gives the name, description, and powerup item class for a unit of functionality which this offering provides for installation on a user store. """) loginInterfaces = Attribute(""" A list of 2-tuples of (interface, description) of interfaces implemented by avatars provided by this offering, and human readable descriptions of the service provided by logging into them. Used by the statistics reporting system to label graphs of login activity. """) themes = Attribute(""" Sequence of L{xmantissa.webtheme.XHTMLDirectoryTheme} instances, constituting themes that belong to this offering """) staticContentPath = Attribute(""" A L{FilePath<twisted.python.filepath.FilePath>} referring to the root of the static content hierarchy for this offering. This directory will be served automatically by Mantissa at C{/static/<offering name>/}. May be C{None} if there is no static content. """) version = Attribute(""" L{twisted.python.versions.Version} instance indicating the version of this offering. If included, the Version's value will be displayed to users once the offering is installed. Defaults to None. """) class IOfferingTechnician(Interface): """ Support installation, uninstallation, and inspection of offerings. """ def getInstalledOfferingNames(): """ Return a C{list} of C{unicode} strings giving the names of all installed L{IOffering}s. """ def getInstalledOfferings(): """ Return a mapping from the names of installed L{IOffering} plugins to the plugins themselves. """ def installOffering(offering): """ Install the given offering plugin using the given configuration. @type offering: L{IOffering} @param offering: The offering to install. @raise L{xmantissa.offering.OfferingAlreadyInstalled}: If an offering with the same name as C{offering} is already installed. @return: The C{InstalledOffering} item created. """ class ISignupMechanism(Interface): """ Describe an Item which can be instantiated to add a means of signing up to a Mantissa server. """ name = Attribute(""" What it is called. """) description = Attribute(""" What it does. """) itemClass = Attribute(""" An Axiom Item subclass which will be instantiated and added to the site store when this signup mechanism is selected. The class should implement L{ISessionlessSiteRootPlugin} or L{ISiteRootPlugin}. """) configuration = Attribute(""" XXX EDOC ME """) class IProtocolFactoryFactory(Interface): """ Powerup interface for Items which can create Twisted protocol factories. """ def getFactory(): """ Return a Twisted protocol factory. """ class IBoxReceiverFactory(Interface): """ Powerup interface for Items which can create L{IBoxReceiver} providers to be made accessible via the standard Mantissa AMP server. """ protocol = Attribute( """ A short string describing the commands (ie, the protocol) provided by the L{IBoxReceiver} this implementation can create. It is B{strongly} recommended that this be a versioned, URI-style identifier, after the fashion of XML namespace specifiers. For example, the first version of a Divmod, Inc.-provided chat protocol might use I{https://divmod.com/ns/funny-chat}. As no format describing AMP command sets (or other protocols built on AMP) is yet defined, there is no requirement that this URI be resolvable to an actual resource. Its purpose at this time is merely to be unique. """) def getBoxReceiver(): """ Return an L{IBoxReceiver} which will be hooked up to an AMP connection and have messages sent to it. """ class IParameter(Interface): """ Description of a single variable which will take on a value from external input and be used to perform some calculation or action. For example, an HTML form is a collection of IParameters, most likely one per input tag. When POSTed, each input supplies its text value as the external input to a corresponding IParameter provider and the resulting collection is used to respond to the POST somehow. NOTE: This interface is highly unstable and subject to grossly incompatible changes. """ # XXX - These shouldn't be attributes of IParameter, I expect. They are # both really view things. Either they goes into the template which is # used for this parameter (as an explanation to a user what the parameter # is), or some code which creates the view supplies them as parameters to # that object (in which case, it's probably more of a unique identifier in # that view context for this parameter). -exarkun name = Attribute( """ A short C{unicode} string uniquely identifying this parameter within the context of a collection of L{IParameter} providers. """) label = Attribute( """ A short C{unicode} string uniquely identifying this parameter within the context of a collection of L{IParameter} providers. """) # XXX - Another thing which belongs on the view. Who even says this will # be rendered to an HTML form? type = Attribute( """ One of C{liveform.TEXT_INPUT}, C{liveform.PASSWORD_INPUT}, C{liveform.TEXTAREA_INPUT}, C{liveform.FORM_INPUT}, C{liveform.RADIO_INPUT}, or C{liveform.CHECKBOX_INPUT} indicating the kind of input interface which will be presented for this parameter. """) # XXX - This shouldn't be an attribute of IParameter. It's intended to be # displayed to end users, it belongs in a template. description = Attribute( """ A long C{unicode} string explaining the meaning or purpose of this parameter. May be C{None} to provide the end user with an unpleasant experience. """) # XXX - At this level, a default should be a structured object, not a # unicode string. There is presently no way to serialize a structured # object into the view, though, so we use unicode here. default = Attribute( """ A C{unicode} string which will be initially presented in the view as the value for this parameter, or C{None} if no such value should be presented. """) def viewFactory(parameter, default): """ @type view: L{IParameter} provider @param view: The parameter for which to create a view. @param default: An object to return if no view can be created for the given parameter. @rtype: L{IParameterView} provider """ # XXX - This is most definitely a view thing. def compact(): """ Mutate the parameter so that when a view object is created for it, it is more visually compact than it would otherwise have been. """ def fromInputs(inputs): """ Extract the value for this parameter from the given submission dictionary and return a structured value for this parameter. """ class IParameterView(IRenderer): """ View interface for an individual LiveForm parameter. """ patternName = Attribute(""" Short string giving the name of the pattern for this parameter view. Must be one of C{'text'}, C{'password'}, C{'repeatable-form'} or C{'choice'}. """) def setDefaultTemplate(tag): """ Called by L{xmantissa.liveform.LiveForm} to specify the default template for this view. @type tag: L{nevow.stan.Tag} or C{nevow.stan.Proto} """ class IPublicPage(Interface): """ Only needed for schema compatibility. This interface should be deleted once Axiom gains the ability to remove interfaces from existing stores. """ class IOneTimePadGenerator(Interface): """ An object which can generate single-use pads for authentication purposes. """ def generateOneTimePad(userStore): """ Generate a one-time pad for the user who lives in the given store. @param userStore: A user's store. @type userStore: L{axiom.store.Store} @rtype: C{str} """ class ITerminalServerFactory(Interface): """ A factory for L{ITerminalProtocol} providers which can create objects to handle input from and produce output to a terminal interface. """ name = Attribute( "A short, user-facing C{unicode} string which identifies the " "functionality provided by this factory.") def buildTerminalProtocol(shellViewer): """ Create and return a new L{ITerminalProtocol} provider to handle interact with a user. @param shellViewer: An L{IViewer} provider """ __all__ = [ 'IColumn', 'ITemplateNameResolver', 'IPreferenceAggregator', 'ISearchProvider', 'ISearchAggregator', 'IFulltextIndexer', 'IFulltextIndexable', 'IStaticShellContent', 'ISiteRootPlugin', 'ISessionlessSiteRootPlugin', 'ICustomizable', 'ICustomizablePublicPage', 'IWebTranslator', 'INavigableElement', 'INavigableFragment', 'ITab', 'IBenefactor', 'IBenefactorFactory', 'IQ2QService', 'IPreferenceCollection', 'ITemporalEvent', 'IDateBook', 'IOrganizerPlugin', 'IPersonFragment', 'IOffering', 'ISignupMechanism', 'IProtocolFactoryFactory', 'IParameterView', 'IOneTimePadGenerator', 'ITerminalServerFactory', ] PK�����9FB#]��]�����xmantissa/liveform.py# -*- test-case-name: xmantissa.test.test_liveform -*- """ XXX HYPER TURBO SUPER UNSTABLE DO NOT USE XXX """ import warnings from zope.interface import implements from twisted.python.components import registerAdapter from twisted.internet.defer import maybeDeferred, gatherResults from epsilon.structlike import record from nevow import inevow, tags, page, athena from nevow.athena import expose from nevow.page import Element, renderer from nevow.loaders import stan from xmantissa import webtheme from xmantissa.fragmentutils import PatternDictionary, dictFillSlots from xmantissa.ixmantissa import IParameter, IParameterView _LIVEFORM_JS_CLASS = u'Mantissa.LiveForm.FormWidget' _SUBFORM_JS_CLASS = u'Mantissa.LiveForm.SubFormWidget' class InputError(athena.LivePageError): """ Base class for all errors related to rejected input values. """ jsClass = u'Mantissa.LiveForm.InputError' TEXT_INPUT = 'text' PASSWORD_INPUT = 'password' TEXTAREA_INPUT = 'textarea' FORM_INPUT = 'form' RADIO_INPUT = 'radio' CHECKBOX_INPUT = 'checkbox' class _SelectiveCoercer(object): """ Mixin defining a L{IParameter.fromInputs} implementation which extracts the input associated with this parameter based on C{self.name} and passes it to C{self.coercer}, returning the result. """ def fromInputs(self, inputs): """ Extract the inputs associated with the child forms of this parameter from the given dictionary and coerce them using C{self.coercer}. @type inputs: C{dict} mapping C{unicode} to C{list} of C{unicode} @param inputs: The contents of a form post, in the conventional structure. @rtype: L{Deferred} @return: The structured data associated with this parameter represented by the post data. """ try: values = inputs[self.name] except KeyError: raise ConfigurationError( "Missing value for input: " + self.name) return self.coerceMany(values) def coerceMany(self, values): """ Convert the given C{list} of C{unicode} inputs to structured data. @param values: The inputs associated with this parameter's name in the overall inputs mapping. """ return self.coercer(values[0]) class Parameter(record('name type coercer label description default ' 'viewFactory', label=None, description=None, default=None, viewFactory=IParameterView), _SelectiveCoercer): """ @type name: C{unicode} @ivar name: A name uniquely identifying this parameter within a particular form. @ivar type: One of C{TEXT_INPUT}, C{PASSWORD_INPUT}, C{TEXTAREA_INPUT}, C{RADIO_INPUT}, or C{CHECKBOX_INPUT} indicating the kind of input interface which will be presented for this parameter. @type description: C{unicode} or C{NoneType} @ivar description: An explanation of the meaning or purpose of this parameter which will be presented in the view, or C{None} if the user is intended to guess. @type default: C{unicode} or C{NoneType} @ivar default: A value which will be initially presented in the view as the value for this parameter, or C{None} if no such value is to be presented. @ivar viewFactory: A two-argument callable which returns an L{IParameterView} provider which will be used as the view for this parameter, if one can be provided. It will be invoked with the parameter as the first argument and a default value as the second argument. The default should be returned if no view can be provided for the given parameter. """ implements(IParameter) def __init__(self, *a, **kw): super(Parameter, self).__init__(*a, **kw) if self.type == FORM_INPUT: warnings.warn( "Create a FormParameter, not a Parameter with type FORM_INPUT", category=DeprecationWarning, stacklevel=2) def compact(self): """ Compact FORM_INPUTs by calling their C{compact} method. Don't do anything for other types of input. """ if self.type == FORM_INPUT: self.coercer.compact() def clone(self, default): """ Make a copy of this parameter, supplying a different default. @type default: C{unicode} or C{NoneType} @param default: A value which will be initially presented in the view as the value for this parameter, or C{None} if no such value is to be presented. @rtype: L{Parameter} """ return self.__class__( self.name, self.type, self.coercer, self.label, self.description, default, self.viewFactory) class FormParameter(record('name form label description default viewFactory', label=None, description=None, default=None, viewFactory=IParameterView), _SelectiveCoercer): """ A parameter which is a collection of other parameters, as composed by a L{LiveForm}. @type name: C{unicode} @ivar name: A name uniquely identifying this parameter within a particular form. @type form: L{LiveForm} @ivar form: The form which defines the grouped parameters. @type description: C{unicode} or C{NoneType} @ivar description: An explanation of the meaning or purpose of this parameter which will be presented in the view, or C{None} if the user is intended to guess. @type default: C{unicode} or C{NoneType} @ivar default: A value which will be initially presented in the view as the value for this parameter, or C{None} if no such value is to be presented. @ivar viewFactory: A two-argument callable which returns an L{IParameterView} provider which will be used as the view for this parameter, if one can be provided. It will be invoked with the parameter as the first argument and a default value as the second argument. The default should be returned if no view can be provided for the given parameter. """ implements(IParameter) type = FORM_INPUT def compact(self): """ Compact the wrapped form. """ self.form.compact() def coercer(self, value): """ Invoke the wrapped form with the given value and return its result. """ return self.form.invoke(value) class CreateObject(record('values setter')): """ Represent one object which should be created as a result of a submission to a L{ListChangeParameter}. @ivar values: The coerced data from the submission which should be used to create this object. @ivar setter: A one-argument callable which must be called with the created object once it is created. Until this is called, the L{ListChangeParameter} will be in a state where it will not handle submissions correctly. (XXX - This could be hooked up to a Deferred, and L{ListChangeParameter}'s C{coercer} method could gatherResults() on all of these, delaying the success of the submission until all created objects have been set). """ class EditObject(record('object values')): """ Represent changes to be made to an object as the result of the submission of a L{ListChangeParameter}. @ivar object: The object which is to be edited. This is one of the elements of the C{modelObjects} sequence passed to L{ListChangeParameter.__init__} or it is one of the objects subsequently added to the L{ListChangeParameter} by a call to L{CreateObject.setter}. @ivar values: The new values for this object from the submission. """ def __cmp__(self, other): return cmp((self.object, self.values), (other.object, other.values)) class ListChanges(record('create edit delete')): """ Represent the submission of a L{ListChangeParameter}. @ivar create: A list of L{CreateObject} instances, one for each new object to be created as a result of the submission. @ivar edit: A list of L{EditObject} instances, one for each existing object modified by the submission. @ivar delete: A list of model objects which should be deleted as a result of the submission. """ class ListChangeParameter(record('name parameters defaults modelObjects ' 'modelObjectDescription viewFactory', defaults=(), modelObjects=(), modelObjectDescription=u'', viewFactory=IParameterView), _SelectiveCoercer): """ Use this parameter if you want to render lists of objects as forms, and to allow the objects to be edited and deleted, as well as to have new objects created. Parameters of this type will be coerced in L{ListChanges} objects. @type name: C{unicode} @ivar name: A name uniquely identifying this parameter within a particular form. @type parameters: C{list} @ivar parameters: sequence of L{Parameter} instances, describing the contents of the repeatable form. @type defaults: C{list} @ivar defaults: A sequence of dictionaries mapping names of L{parameters} to values. @type modelObjects: C{list} @ivar modelObjects: A sequence of opaque objects, one for each item in C{defaults}. @type modelObjectDescription: C{unicode} @param modelObjectDescription: A description of the type of model object being edited, e.g. 'Email Address'. Defaults to the empty string. @type viewFactory: callable @ivar viewFactory: A two-argument callable which returns an L{IParameterView} provider which will be used as the view for this parameter, if one can be provided. It will be invoked with the parameter as the first argument and a default value as the second argument. The default should be returned if no view can be provided for the given parameter. """ _parameterIsCompact = False type = None _IDENTIFIER_KEY = u'__repeated-liveform-id__' _NO_OBJECT_MARKER = object() def __init__(self, *a, **k): super(ListChangeParameter, self).__init__(*a, **k) self.liveFormFactory = LiveForm self.repeatedLiveFormWrapper = RepeatedLiveFormWrapper self._idsToObjects = {} self._lastValues = {} self._defaultStuff = [] for (defaultObject, defaultValues) in zip(self.modelObjects, self.defaults): identifier = self._idForObject(defaultObject) self._lastValues[identifier] = defaultValues self._defaultStuff.append((defaultValues, identifier)) def compact(self): """ Remember whether we were compacted or not, so we can relay this to our view. """ self._parameterIsCompact = True def _prepareSubForm(self, liveForm): """ Utility for turning liveforms into subforms, and compacting them as necessary. @param liveForm: a liveform. @type liveForm: L{LiveForm} @return: a sub form. @rtype: L{LiveForm} """ liveForm = liveForm.asSubForm(self.name) # XXX Why did this work??? # if we are compact, tell the liveform so it can tell its parameters # also if self._parameterIsCompact: liveForm.compact() return liveForm def _cloneDefaultedParameter(self, original, default): """ Make a copy of the parameter C{original}, supplying C{default} as the default value. @type original: L{Parameter} or L{ChoiceParameter} @param original: A liveform parameter. @param default: An alternate default value for the parameter. @rtype: L{Parameter} or L{ChoiceParameter} @return: A new parameter. """ if isinstance(original, ChoiceParameter): default = [Option(o.description, o.value, o.value in default) for o in original.choices] return original.clone(default) _counter = 0 def _allocateID(self): """ Allocate an internal identifier. @rtype: C{int} """ self._counter += 1 return self._counter def _idForObject(self, defaultObject): """ Generate an opaque identifier which can be used to talk about C{defaultObject}. @rtype: C{int} """ identifier = self._allocateID() self._idsToObjects[identifier] = defaultObject return identifier def _objectFromID(self, identifier): """ Find the object associated with the identifier C{identifier}. @type identifier: C{int} """ return self._idsToObjects[identifier] def _newIdentifier(self): """ Make a new identifier for an as-yet uncreated model object. @rtype: C{int} """ id = self._allocateID() self._idsToObjects[id] = self._NO_OBJECT_MARKER self._lastValues[id] = None return id def _makeALiveForm(self, parameters, identifier, removable=True): """ Make a live form with the parameters C{parameters}, which will be used to edit the values/model object with identifier C{identifier}. @type parameters: C{list} @param parameters: list of L{Parameter} instances. @type identifier: C{int} @type removable: C{bool} @rtype: L{repeatedLiveFormWrapper} """ liveForm = self.liveFormFactory(lambda **k: None, parameters, self.name) liveForm = self._prepareSubForm(liveForm) liveForm = self.repeatedLiveFormWrapper(liveForm, identifier, removable) liveForm.docFactory = webtheme.getLoader(liveForm.fragmentName) return liveForm def _makeDefaultLiveForm(self, (defaults, identifier)): """ Make a liveform suitable for editing the set of default values C{defaults}. @type defaults: C{dict} @param defaults: Mapping of parameter names to values. @rtype: L{repeatedLiveFormWrapper} """ parameters = [self._cloneDefaultedParameter(p, defaults[p.name]) for p in self.parameters] return self._makeALiveForm(parameters, identifier) def getInitialLiveForms(self): """ Make and return as many L{LiveForm} instances as are necessary to hold our default values. @return: some subforms. @rtype: C{list} of L{LiveForm} """ liveForms = [] if self._defaultStuff: for values in self._defaultStuff: liveForms.append(self._makeDefaultLiveForm(values)) else: # or only one, for the first new thing liveForms.append( self._makeALiveForm( self.parameters, self._newIdentifier(), False)) return liveForms def asLiveForm(self): """ Make and return a form, using L{parameters}. @return: a sub form. @rtype: L{LiveForm} """ return self._makeALiveForm(self.parameters, self._newIdentifier()) def _coerceSingleRepetition(self, dataSet): """ Make a new liveform with our parameters, and get it to coerce our data for us. """ # make a liveform because there is some logic in _coerced form = LiveForm(lambda **k: None, self.parameters, self.name) return form.fromInputs(dataSet) def _extractCreations(self, dataSets): """ Find the elements of C{dataSets} which represent the creation of new objects. @param dataSets: C{list} of C{dict} mapping C{unicode} form submission keys to form submission values. @return: iterator of C{tuple}s with the first element giving the opaque identifier of an object which is to be created and the second element giving a C{dict} of all the other creation arguments. """ for dataSet in dataSets: modelObject = self._objectFromID(dataSet[self._IDENTIFIER_KEY]) if modelObject is self._NO_OBJECT_MARKER: dataCopy = dataSet.copy() identifier = dataCopy.pop(self._IDENTIFIER_KEY) yield identifier, dataCopy def _extractEdits(self, dataSets): """ Find the elements of C{dataSets} which represent changes to existing objects. @param dataSets: C{list} of C{dict} mapping C{unicode} form submission keys to form submission values. @return: iterator of C{tuple}s with the first element giving a model object which is being edited and the second element giving a C{dict} of all the other arguments. """ for dataSet in dataSets: modelObject = self._objectFromID(dataSet[self._IDENTIFIER_KEY]) if modelObject is not self._NO_OBJECT_MARKER: dataCopy = dataSet.copy() identifier = dataCopy.pop(self._IDENTIFIER_KEY) yield identifier, dataCopy def _coerceAll(self, inputs): """ XXX """ def associate(result, obj): return (obj, result) coerceDeferreds = [] for obj, dataSet in inputs: oneCoerce = self._coerceSingleRepetition(dataSet) oneCoerce.addCallback(associate, obj) coerceDeferreds.append(oneCoerce) return gatherResults(coerceDeferreds) def coercer(self, dataSets): """ Coerce all of the repetitions and sort them into creations, edits and deletions. @rtype: L{ListChanges} @return: An object describing all of the creations, modifications, and deletions represented by C{dataSets}. """ # Xxx - This does a slightly complex (hey, it's like 20 lines, how # complex could it really be?) thing to figure out which elements are # newly created, which elements were edited, and which elements no # longer exist. It might be simpler if the client kept track of this # and passed a three-tuple of lists (or whatever - some separate data # structures) to the server, so everything would be all figured out # already. This would require the client # (Mantissa.LiveForm.RepeatableForm) to be more aware of what events # the user is triggering in the browser so that it could keep state for # adds/deletes/edits separately from DOM and widget objects. This # would remove the need for RepeatedLiveFormWrapper. def makeSetter(identifier, values): def setter(defaultObject): self._idsToObjects[identifier] = defaultObject self._lastValues[identifier] = values return setter created = self._coerceAll(self._extractCreations(dataSets)) edited = self._coerceAll(self._extractEdits(dataSets)) coerceDeferred = gatherResults([created, edited]) def cbCoerced((created, edited)): receivedIdentifiers = set() createObjects = [] for (identifier, dataSet) in created: receivedIdentifiers.add(identifier) createObjects.append( CreateObject(dataSet, makeSetter(identifier, dataSet))) editObjects = [] for (identifier, dataSet) in edited: receivedIdentifiers.add(identifier) lastValues = self._lastValues[identifier] if dataSet != lastValues: modelObject = self._objectFromID(identifier) editObjects.append(EditObject(modelObject, dataSet)) self._lastValues[identifier] = dataSet deleted = [] for identifier in set(self._idsToObjects) - receivedIdentifiers: existing = self._objectFromID(identifier) if existing is not self._NO_OBJECT_MARKER: deleted.append(existing) self._idsToObjects.pop(identifier) return ListChanges(createObjects, editObjects, deleted) coerceDeferred.addCallback(cbCoerced) return coerceDeferred MULTI_TEXT_INPUT = 'multi-text' class ListParameter(record('name coercer count label description defaults ' 'viewFactory', label=None, description=None, defaults=None, viewFactory=IParameterView)): type = MULTI_TEXT_INPUT def compact(self): """ Don't do anything. """ def fromInputs(self, inputs): """ Extract the inputs associated with this parameter from the given dictionary and coerce them using C{self.coercer}. @type inputs: C{dict} mapping C{str} to C{list} of C{str} @param inputs: The contents of a form post, in the conventional structure. @rtype: L{Deferred} @return: A Deferred which will be called back with a list of the structured data associated with this parameter. """ outputs = [] for i in xrange(self.count): name = self.name + '_' + str(i) try: value = inputs[name][0] except KeyError: raise ConfigurationError( "Missing value for field %d of %s" % (i, self.name)) else: outputs.append(maybeDeferred(self.coercer, value)) return gatherResults(outputs) CHOICE_INPUT = 'choice' MULTI_CHOICE_INPUT = 'multi-choice' class Option(record('description value selected')): """ A single choice for a L{ChoiceParameter}. """ class ChoiceParameter(record('name choices label description multiple ' 'viewFactory', label=None, description="", multiple=False, viewFactory=IParameterView), _SelectiveCoercer): """ A choice parameter, represented by a <select> element in HTML. @ivar choices: A sequence of L{Option} instances (deprecated: a sequence of three-tuples giving the attributes of L{Option} instances). @ivar multiple: C{True} if multiple choice selections are allowed @ivar viewFactory: A two-argument callable which returns an L{IParameterView} provider which will be used as the view for this parameter, if one can be provided. It will be invoked with the parameter as the first argument and a default value as the second argument. The default should be returned if no view can be provided for the given parameter. """ def __init__(self, *a, **kw): ChoiceParameter.__bases__[0].__init__(self, *a, **kw) if self.choices and isinstance(self.choices[0], tuple): warnings.warn( "Pass a list of Option instances to ChoiceParameter, " "not a list of tuples.", category=DeprecationWarning, stacklevel=2) self.choices = [Option(*o) for o in self.choices] def type(self): if self.multiple: return MULTI_CHOICE_INPUT return CHOICE_INPUT type = property(type) def coercer(self, value): if self.multiple: return tuple(self.choices[int(v)].value for v in value) return self.choices[int(value)].value def compact(self): """ Don't do anything. """ def clone(self, choices): """ Make a copy of this parameter, supply different choices. @param choices: A sequence of L{Option} instances. @type choices: C{list} @rtype: L{ChoiceParameter} """ return self.__class__( self.name, choices, self.label, self.description, self.multiple, self.viewFactory) class ConfigurationError(Exception): """ User-specified configuration for a newly created Item was invalid or incomplete. """ class InvalidInput(Exception): """ Data entered did not meet the requirements of the coercer. """ def _legacySpecialCases(form, patterns, parameter): """ Create a view object for the given parameter. This function implements the remaining view construction logic which has not yet been converted to the C{viewFactory}-style expressed in L{_LiveFormMixin.form}. @type form: L{_LiveFormMixin} @param form: The form fragment which contains the given parameter. @type patterns: L{PatternDictionary} @type parameter: L{Parameter}, L{ChoiceParameter}, or L{ListParameter}. """ p = patterns[parameter.type + '-input-container'] if parameter.type == TEXTAREA_INPUT: p = dictFillSlots(p, dict(label=parameter.label, name=parameter.name, value=parameter.default or '')) elif parameter.type == MULTI_TEXT_INPUT: subInputs = list() for i in xrange(parameter.count): subInputs.append(dictFillSlots(patterns['input'], dict(name=parameter.name + '_' + str(i), type='text', value=parameter.defaults[i]))) p = dictFillSlots(p, dict(label=parameter.label or parameter.name, inputs=subInputs)) else: if parameter.default is not None: value = parameter.default else: value = '' if parameter.type == CHECKBOX_INPUT and parameter.default: inputPattern = 'checked-checkbox-input' else: inputPattern = 'input' p = dictFillSlots( p, dict(label=parameter.label or parameter.name, input=dictFillSlots(patterns[inputPattern], dict(name=parameter.name, type=parameter.type, value=value)))) p(**{'class' : 'liveform_'+parameter.name}) if parameter.description: description = patterns['description'].fillSlots( 'description', parameter.description) else: description = '' return dictFillSlots( patterns['parameter-input'], dict(input=p, description=description)) class _LiveFormMixin(record('callable parameters description', description=None)): jsClass = _LIVEFORM_JS_CLASS subFormName = None fragmentName = 'liveform' compactFragmentName = 'liveform-compact' def __init__(self, *a, **k): super(_LiveFormMixin, self).__init__(*a, **k) if self.docFactory is None: # Give subclasses a chance to assign their own docFactory. self.docFactory = webtheme.getLoader(self.fragmentName) def compact(self): """ Switch to the compact variant of the live form template. By default, this will simply create a loader for the C{self.compactFragmentName} template and compact all of this form's parameters. """ self.docFactory = webtheme.getLoader(self.compactFragmentName) for param in self.parameters: param.compact() def getInitialArguments(self): if self.subFormName: subFormName = self.subFormName.decode('utf-8') else: subFormName = None return (subFormName,) def asSubForm(self, name): """ Make a form suitable for nesting within another form (a subform) out of this top-level liveform. @param name: the name of the subform within its parent. @type name: C{unicode} @return: a subform. @rtype: L{LiveForm} """ self.subFormName = name self.jsClass = _SUBFORM_JS_CLASS return self def _getDescription(self): descr = self.description if descr is None: descr = self.callable.__name__ return descr def submitbutton(self, request, tag): """ Render an INPUT element of type SUBMIT which will post this form to the server. """ return tags.input(type='submit', name='__submit__', value=self._getDescription()) page.renderer(submitbutton) def render_submitbutton(self, ctx, data): return self.submitbutton(inevow.IRequest(ctx), ctx.tag) def render_liveFragment(self, ctx, data): return self.liveElement(inevow.IRequest(ctx), ctx.tag) def form(self, request, tag): """ Render the inputs for a form. @param tag: A tag with: - I{form} and I{description} slots - I{liveform} and I{subform} patterns, to fill the I{form} slot - An I{inputs} slot, to fill with parameter views - L{IParameterView.patternName}I{-input-container} patterns for each parameter type in C{self.parameters} """ patterns = PatternDictionary(self.docFactory) inputs = [] for parameter in self.parameters: view = parameter.viewFactory(parameter, None) if view is not None: view.setDefaultTemplate( tag.onePattern(view.patternName + '-input-container')) setFragmentParent = getattr(view, 'setFragmentParent', None) if setFragmentParent is not None: setFragmentParent(self) inputs.append(view) else: inputs.append(_legacySpecialCases(self, patterns, parameter)) if self.subFormName is None: pattern = tag.onePattern('liveform') else: pattern = tag.onePattern('subform') return dictFillSlots( tag, dict(form=pattern.fillSlots('inputs', inputs), description=self._getDescription())) page.renderer(form) def render_form(self, ctx, data): return self.form(inevow.IRequest(ctx), ctx.tag) def invoke(self, formPostEmulator): """ Invoke my callable with input from the browser. @param formPostEmulator: a dict of lists of strings in a format like a cgi-module form post. """ result = self.fromInputs(formPostEmulator) result.addCallback(lambda params: self.callable(**params)) return result expose(invoke) def __call__(self, formPostEmulator): """ B{Private} helper which passes through to L{invoke} to support legacy code passing L{LiveForm} instances to L{Parameter} instead of L{FormParameter}. Do B{not} call this. """ return self.invoke(formPostEmulator) def fromInputs(self, received): """ Convert some random strings received from a browser into structured data, using a list of parameters. @param received: a dict of lists of strings, i.e. the canonical Python form of web form post. @rtype: L{Deferred} @return: A Deferred which will be called back with a dict mapping parameter names to coerced parameter values. """ results = [] for parameter in self.parameters: name = parameter.name.encode('ascii') d = maybeDeferred(parameter.fromInputs, received) d.addCallback(lambda value, name=name: (name, value)) results.append(d) return gatherResults(results).addCallback(dict) class LiveFormFragment(_LiveFormMixin, athena.LiveFragment): """ DEPRECATED. @see LiveForm """ class LiveForm(_LiveFormMixin, athena.LiveElement): """ A live form. Create with a callable and a list of L{Parameter}s which describe the form of the arguments which the callable will expect. @ivar callable: a callable that you can call @ivar parameters: a list of L{Parameter} objects describing the arguments which should be passed to C{callable}. """ class _ParameterViewMixin: """ Base class providing common functionality for different parameter views. @type parameter: L{Parameter} """ def __init__(self, parameter): """ @type tag: L{nevow.stan.Tag} @param tag: The document template to use to render this view. """ self.parameter = parameter def __eq__(self, other): """ Define equality such other views which are instances of the same class as this view and which wrap the same L{Parameter} are considered equal to this one. """ if isinstance(other, self.__class__): return self.parameter is other.parameter return False def __ne__(self, other): """ Define inequality as the negation of equality. """ return not self.__eq__(other) def setDefaultTemplate(self, tag): """ Use the given default template. """ self.docFactory = stan(tag) def name(self, request, tag): """ Render the name of the wrapped L{Parameter} or L{ChoiceParameter} instance. """ return tag[self.parameter.name] renderer(name) def label(self, request, tag): """ Render the label of the wrapped L{Parameter} or L{ChoiceParameter} instance. """ if self.parameter.label: tag[self.parameter.label] return tag renderer(label) def description(self, request, tag): """ Render the description of the wrapped L{Parameter} instance. """ if self.parameter.description is not None: tag[self.parameter.description] return tag renderer(description) class RepeatedLiveFormWrapper(athena.LiveElement): """ A wrapper around a L{LiveForm} which has been repeated via L{ListChangeParameter.asLiveForm}. @ivar liveForm: The repeated liveform. @type liveForm: L{LiveForm} @ivar identifier: An integer identifying this repetition. @type identifier: C{int} @ivar removable: Whether this repetition can be unrepeated/removed. @type removable: C{bool} """ fragmentName = 'repeated-liveform' jsClass = u'Mantissa.LiveForm.RepeatedLiveFormWrapper' def __init__(self, liveForm, identifier, removable=True): athena.LiveElement.__init__(self) self.liveForm = liveForm self.identifier = identifier self.removable = removable def getInitialArguments(self): """ Include the name of the form we're wrapping, and our original values. """ return (self.liveForm.subFormName.decode('utf-8'), self.identifier) def realForm(self, req, tag): """ Render L{liveForm}. """ self.liveForm.setFragmentParent(self) return self.liveForm page.renderer(realForm) def removeLink(self, req, tag): """ Render C{tag} if L{removable} is C{True}, otherwise return the empty string. """ if self.removable: return tag return '' page.renderer(removeLink) class _TextLikeParameterView(_ParameterViewMixin, Element): """ View definition base class for L{Parameter} instances which are simple text inputs. """ def default(self, request, tag): """ Render the initial value of the wrapped L{Parameter} instance. """ if self.parameter.default is not None: tag[self.parameter.default] return tag renderer(default) class TextParameterView(_TextLikeParameterView): """ View definition for L{Parameter} instances with type of C{TEXT_INPUT} """ implements(IParameterView) patternName = 'text' class PasswordParameterView(_TextLikeParameterView): """ View definition for L{Parameter} instances with type of C{PASSWORD_INPUT} """ implements(IParameterView) patternName = 'password' class OptionView(Element): """ View definition for a single choice of a L{ChoiceParameter}. @type option: L{Option} """ def __init__(self, index, option, tag): self._index = index self.option = option self.docFactory = stan(tag) def __eq__(self, other): """ Define equality such other L{OptionView} instances which wrap the same L{Option} are considered equal to this one. """ if isinstance(other, OptionView): return self.option is other.option return False def __ne__(self, other): """ Define inequality as the negation of equality. """ return not self.__eq__(other) def description(self, request, tag): """ Render the description of the wrapped L{Option} instance. """ return tag[self.option.description] renderer(description) def value(self, request, tag): """ Render the value of the wrapped L{Option} instance. """ return tag[self.option.value] renderer(value) def index(self, request, tag): """ Render the index specified to C{__init__}. """ return tag[self._index] renderer(index) def selected(self, request, tag): """ Render a selected attribute on the given tag if the wrapped L{Option} instance is selected. """ if self.option.selected: tag(selected='selected') return tag renderer(selected) def _textParameterToView(parameter): """ Return a L{TextParameterView} adapter for C{TEXT_INPUT}, C{PASSWORD_INPUT}, and C{FORM_INPUT} L{Parameter} instances. """ if parameter.type == TEXT_INPUT: return TextParameterView(parameter) if parameter.type == PASSWORD_INPUT: return PasswordParameterView(parameter) if parameter.type == FORM_INPUT: return FormInputParameterView(parameter) return None registerAdapter(_textParameterToView, Parameter, IParameterView) class ChoiceParameterView(_ParameterViewMixin, Element): """ View definition for L{Parameter} instances with type of C{CHOICE_INPUT}. """ implements(IParameterView) patternName = 'choice' def multiple(self, request, tag): """ Render a I{multiple} attribute on the given tag if the wrapped L{ChoiceParameter} instance allows multiple selection. """ if self.parameter.multiple: tag(multiple='multiple') return tag renderer(multiple) def options(self, request, tag): """ Render each of the options of the wrapped L{ChoiceParameter} instance. """ option = tag.patternGenerator('option') return tag[[ OptionView(index, o, option()) for (index, o) in enumerate(self.parameter.choices)]] renderer(options) registerAdapter(ChoiceParameterView, ChoiceParameter, IParameterView) class ListChangeParameterView(_ParameterViewMixin, athena.LiveElement): """ L{IParameterView} adapter for L{ListChangeParameter}. @ivar parameter: the parameter being viewed. @type parameter: L{ListChangeParameter} """ jsClass = u'Mantissa.LiveForm.RepeatableForm' patternName = 'repeatable-form' def __init__(self, parameter): self.parameter = parameter athena.LiveElement.__init__(self) def getInitialArguments(self): """ Pass the name of our parameter to the client. """ return (self.parameter.name,) def forms(self, req, tag): """ Make and return some forms, using L{self.parameter.getInitialLiveForms}. @return: some subforms. @rtype: C{list} of L{LiveForm} """ liveForms = self.parameter.getInitialLiveForms() for liveForm in liveForms: liveForm.setFragmentParent(self) return liveForms page.renderer(forms) def repeatForm(self): """ Make and return a form, using L{self.parameter.asLiveForm}. @return: a subform. @rtype: L{LiveForm} """ liveForm = self.parameter.asLiveForm() liveForm.setFragmentParent(self) return liveForm athena.expose(repeatForm) def repeater(self, req, tag): """ Render some UI for repeating our form. """ repeater = inevow.IQ(self.docFactory).onePattern('repeater') return repeater.fillSlots( 'object-description', self.parameter.modelObjectDescription) page.renderer(repeater) registerAdapter(ListChangeParameterView, ListChangeParameter, IParameterView) class FormParameterView(_ParameterViewMixin, athena.LiveElement): """ L{IParameterView} adapter for L{FormParameter}. @ivar parameter: the parameter being viewed. @type parameter: L{FormParameter} """ implements(IParameterView) patternName = 'form' def __init__(self, parameter): _ParameterViewMixin.__init__(self, parameter) athena.LiveElement.__init__(self) def input(self, request, tag): """ Add the wrapped form, as a subform, as a child of the given tag. """ subform = self.parameter.form.asSubForm(self.parameter.name) subform.setFragmentParent(self) return tag[subform] renderer(input) registerAdapter(FormParameterView, FormParameter, IParameterView) class FormInputParameterView(_ParameterViewMixin, athena.LiveElement): """ L{IParameterView} adapter for C{FORM_INPUT} L{Parameter}s. @ivar parameter: the parameter being viewed. @type parameter: L{Parameter} """ implements(IParameterView) patternName = 'form' def __init__(self, parameter): _ParameterViewMixin.__init__(self, parameter) athena.LiveElement.__init__(self) def input(self, request, tag): """ Add the wrapped form, as a subform, as a child of the given tag. """ subform = self.parameter.coercer.asSubForm(self.parameter.name) subform.setFragmentParent(self) return tag[subform] renderer(input) PK�����9FR"�������xmantissa/myaccount.py""" This module is here to satisfy old databases that contain MyAccount items """ from axiom.item import Item from axiom.upgrade import registerUpgrader class MyAccount(Item): typeName = 'mantissa_myaccount' schemaVersion = 2 def deleteMyAccount(old): # Just get rid of the old account object. Don't even create a new one. old.deleteFromStore() return None registerUpgrader(deleteMyAccount, 'mantissa_myaccount', 1, 2) PK�����9F$j*��j*�����xmantissa/offering.py# -*- test-case-name: xmantissa.test.test_offering -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Implemenation of Offerings, Mantissa's unit of installable application functionality. """ from zope.interface import implements from twisted import plugin from twisted.python.components import registerAdapter from nevow import inevow, loaders, rend, athena from nevow.athena import expose from epsilon.structlike import record from axiom.store import Store from axiom import item, userbase, attributes, substore from axiom.dependency import installOn from xmantissa import ixmantissa, plugins class OfferingAlreadyInstalled(Exception): """ Tried to install an offering, but an offering by the same name was already installed. This may mean someone tried to install the same offering twice, or that two unrelated offerings picked the same name and therefore conflict! Oops. """ class Benefactor(object): """ I implement a method of installing and removing chunks of functionality from a user's store. """ class Offering(record( 'name description siteRequirements appPowerups installablePowerups ' 'loginInterfaces themes staticContentPath version', staticContentPath=None, version=None)): """ A set of functionality which can be added to a Mantissa server. @see L{ixmantissa.IOffering} """ implements(plugin.IPlugin, ixmantissa.IOffering) class InstalledOffering(item.Item): typeName = 'mantissa_installed_offering' schemaVersion = 1 offeringName = attributes.text(doc=""" The name of the Offering to which this corresponds. """, allowNone=False) application = attributes.reference(doc=""" A reference to the Application SubStore for this offering. """) def getOffering(self): """ @return: the L{Offering} plugin object that corresponds to this object, or L{None}. """ # XXX maybe we need to optimize this; it's called SUPER often, and this # is ghetto as hell, on the other hand, isn't that the plugin system's # fault? dang, I don't know. for o in getOfferings(): if o.name == self.offeringName: return o def getOfferings(): """ Return the IOffering plugins available on this system. """ return plugin.getPlugins(ixmantissa.IOffering, plugins) class OfferingAdapter(object): """ Implementation of L{ixmantissa.IOfferingTechnician} for L{axiom.store.Store}. @ivar _siteStore: The L{axiom.store.Store} being adapted. """ implements(ixmantissa.IOfferingTechnician) def __init__(self, siteStore): self._siteStore = siteStore def getInstalledOfferingNames(self): """ Get the I{offeringName} attribute of each L{InstalledOffering} in C{self._siteStore}. """ return list( self._siteStore.query(InstalledOffering).getColumn("offeringName")) def getInstalledOfferings(self): """ Return a mapping from the name of each L{InstalledOffering} in C{self._siteStore} to the corresponding L{IOffering} plugins. """ d = {} installed = self._siteStore.query(InstalledOffering) for installation in installed: offering = installation.getOffering() if offering is not None: d[offering.name] = offering return d def installOffering(self, offering): """ Install the given offering:: - Create and install the powerups in its I{siteRequirements} list. - Create an application L{Store} and a L{LoginAccount} referring to it. Install the I{appPowerups} on the application store. - Create an L{InstalledOffering. Perform all of these tasks in a transaction managed within the scope of this call (that means you should not call this function inside a transaction, or you should not handle any exceptions it raises inside an externally managed transaction). @type offering: L{IOffering} @param offering: The offering to install. @return: The C{InstalledOffering} item created. """ for off in self._siteStore.query( InstalledOffering, InstalledOffering.offeringName == offering.name): raise OfferingAlreadyInstalled(off) def siteSetup(): for (requiredInterface, requiredPowerup) in offering.siteRequirements: if requiredInterface is not None: nn = requiredInterface(self._siteStore, None) if nn is not None: continue if requiredPowerup is None: raise NotImplementedError( 'Interface %r required by %r but not provided by %r' % (requiredInterface, offering, self._siteStore)) self._siteStore.findOrCreate( requiredPowerup, lambda p: installOn(p, self._siteStore)) ls = self._siteStore.findOrCreate(userbase.LoginSystem) substoreItem = substore.SubStore.createNew( self._siteStore, ('app', offering.name + '.axiom')) ls.addAccount(offering.name, None, None, internal=True, avatars=substoreItem) from xmantissa.publicweb import PublicWeb PublicWeb(store=self._siteStore, application=substoreItem, prefixURL=offering.name) ss = substoreItem.open() def appSetup(): for pup in offering.appPowerups: installOn(pup(store=ss), ss) ss.transact(appSetup) # Woops, we need atomic cross-store transactions. io = InstalledOffering( store=self._siteStore, offeringName=offering.name, application=substoreItem) #Some new themes may be available now. Clear the theme cache #so they can show up. #XXX This is pretty terrible -- there #really should be a scheme by which ThemeCache instances can #be non-global. Fix this at the earliest opportunity. from xmantissa import webtheme webtheme.theThemeCache.emptyCache() return io return self._siteStore.transact(siteSetup) registerAdapter(OfferingAdapter, Store, ixmantissa.IOfferingTechnician) def isAppStore(s): """ Return whether the given store is an application store or not. @param s: A Store. """ if s.parent is None: return False substore = s.parent.getItemByID(s.idInParent) return s.parent.query(InstalledOffering, InstalledOffering.application == substore ).count() > 0 def getInstalledOfferingNames(s): """ Return a list of the names of the Offerings which are installed on the given store. @param s: Site Store on which offering installations are tracked. """ return ixmantissa.IOfferingTechnician(s).getInstalledOfferingNames() def getInstalledOfferings(s): """ Return a mapping from the names of installed IOffering plugins to the plugins themselves. @param s: Site Store on which offering installations are tracked. """ return ixmantissa.IOfferingTechnician(s).getInstalledOfferings() def installOffering(s, offering, configuration): """ Create an app store for an L{Offering}, possibly installing some powerups on it, after checking that the site store has the requisite powerups installed on it. Also create an L{InstalledOffering} item referring to the app store and return it. """ return ixmantissa.IOfferingTechnician(s).installOffering(offering) class OfferingConfiguration(item.Item): """ Provide administrative configuration tools for the L{IOffering}s available in this Mantissa server. """ typeName = 'mantissa_offering_configuration_powerup' schemaVersion = 1 installedOfferingCount = attributes.integer(default=0) installedOn = attributes.reference() powerupInterfaces = (ixmantissa.INavigableElement,) def installOffering(self, offering, configuration): """ Create an app store for an L{Offering} and install its dependencies. Also create an L{InstalledOffering} in the site store, and return it. """ s = self.store.parent self.installedOfferingCount += 1 return installOffering(s, offering, configuration) def getTabs(self): # XXX profanity from xmantissa import webnav return [webnav.Tab('Admin', self.storeID, 0.3, [webnav.Tab('Offerings', self.storeID, 1.0)], authoritative=True)] class UninstalledOfferingFragment(athena.LiveFragment): """ Fragment representing a single Offering which has not been installed on the system. It has a single remote method which will install it. """ jsClass = u'Mantissa.Offering.UninstalledOffering' def __init__(self, original, offeringConfig, offeringPlugin, **kw): super(UninstalledOfferingFragment, self).__init__(original, **kw) self.offeringConfig = offeringConfig self.offeringPlugin = offeringPlugin def install(self, configuration): self.offeringConfig.installOffering(self.offeringPlugin, configuration) expose(install) class OfferingConfigurationFragment(athena.LiveFragment): fragmentName = 'offering-configuration' live = 'athena' def __init__(self, *a, **kw): super(OfferingConfigurationFragment, self).__init__(*a, **kw) self.installedOfferings = getInstalledOfferingNames(self.original.store.parent) self.offeringPlugins = dict((p.name, p) for p in plugin.getPlugins(ixmantissa.IOffering, plugins)) def head(self): return None def render_offerings(self, ctx, data): iq = inevow.IQ(ctx.tag) uninstalled = iq.patternGenerator('uninstalled') installed = iq.patternGenerator('installed') def offerings(): for p in self.offeringPlugins.itervalues(): data = {'name': p.name, 'description': p.description} if p.name not in self.installedOfferings: f = UninstalledOfferingFragment(data, self.original, p, docFactory=loaders.stan(uninstalled())) f.page = self.page else: f = rend.Fragment(data, docFactory=loaders.stan(installed())) yield f return ctx.tag[offerings()] registerAdapter(OfferingConfigurationFragment, OfferingConfiguration, ixmantissa.INavigableFragment) PK�����9FKN�N����xmantissa/people.py# -*- test-case-name: xmantissa.test.test_people -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Person item and related functionality. """ from warnings import warn try: from PIL import Image except ImportError: Image = None # Python < 2.5 compatibility try: from email.utils import getaddresses except ImportError: from email.Utils import getaddresses from zope.interface import implements from twisted.python import components from twisted.python.filepath import FilePath from twisted.python.reflect import qual from nevow import rend, athena, inevow, static, tags, url from nevow.athena import expose, LiveElement from nevow.loaders import stan from nevow.page import Element, renderer from nevow.taglibrary import tabbedPane from formless import nameToLabel from epsilon import extime from epsilon.structlike import record from epsilon.descriptor import requiredAttribute from axiom import item, attributes from axiom.tags import Tag from axiom.dependency import dependsOn from axiom.attributes import boolean from axiom.upgrade import ( registerUpgrader, registerAttributeCopyingUpgrader, registerDeletionUpgrader) from axiom.userbase import LoginAccount, LoginMethod from xmantissa.ixmantissa import IPeopleFilter from xmantissa import ixmantissa, webnav, webtheme, liveform, signup from xmantissa.ixmantissa import IOrganizerPlugin, IContactType from xmantissa.webapp import PrivateApplication from xmantissa.tdbview import TabularDataView, ColumnViewBase from xmantissa.scrolltable import ScrollingElement, UnsortableColumn from xmantissa.fragmentutils import dictFillSlots from xmantissa.webtheme import ThemedDocumentFactory def makeThumbnail(inputFile, outputFile, thumbnailSize, outputFormat='jpeg'): """ Make a thumbnail of the image stored at C{inputPath}, preserving its aspect ratio, and write the result to C{outputPath}. @param inputFile: The image file (or path to the file) to thumbnail. @type inputFile: C{file} or C{str} @param outputFile: The file (or path to the file) to write the thumbnail to. @type outputFile: C{file} or C{str} @param thumbnailSize: The maximum length (in pixels) of the longest side of the thumbnail image. @type thumbnailSize: C{int} @param outputFormat: The C{format} argument to pass to L{Image.save}. Defaults to I{jpeg}. @type format: C{str} """ if Image is None: # throw the ImportError here import PIL image = Image.open(inputFile) # Resize needed? if thumbnailSize < max(image.size): # Convert bilevel and paletted images to grayscale and RGB respectively; # otherwise PIL silently switches to Image.NEAREST sampling. if image.mode == '1': image = image.convert('L') elif image.mode == 'P': image = image.convert('RGB') image.thumbnail((thumbnailSize, thumbnailSize), Image.ANTIALIAS) image.save(outputFile, outputFormat) def _normalizeWhitespace(text): """ Remove leading and trailing whitespace and collapse adjacent spaces into a single space. @type text: C{unicode} @rtype: C{unicode} """ return u' '.join(text.split()) def _objectToName(o): """ Derive a possibly-useful string type name from C{o}'s class. @rtype: C{str} """ return nameToLabel(o.__class__.__name__).lstrip() def _descriptiveIdentifier(contactType): """ Get a descriptive identifier for C{contactType}, taking into account the fact that it might not have implemented the C{descriptiveIdentifier} method. @type contactType: L{IContactType} provider. @rtype: C{unicode} """ descriptiveIdentifierMethod = getattr( contactType, 'descriptiveIdentifier', None) if descriptiveIdentifierMethod is not None: return descriptiveIdentifierMethod() warn( "IContactType now has the 'descriptiveIdentifier'" " method, %s did not implement it" % (contactType.__class__,), category=PendingDeprecationWarning) return _objectToName(contactType).decode('ascii') def _organizerPluginName(plugin): """ Get a name for C{plugin}, taking into account the fact that it might not have defined L{IOrganizerPlugin.name}. @type plugin: L{IOrganizerPlugin} provider. @rtype: C{unicode} """ name = getattr(plugin, 'name', None) if name is not None: return name warn( "IOrganizerPlugin now has the 'name' attribute" " and %s does not define it" % (plugin.__class__,), category=PendingDeprecationWarning) return _objectToName(plugin).decode('ascii') class ContactGroup(record('groupName')): """ An object describing a group of L{IContactItem} implementors. @see IContactType.getContactGroup @ivar groupName: The name of the group. @type groupNode: C{unicode} """ class BaseContactType(object): """ Base class for L{IContactType} implementations which provides useful default behavior. """ allowMultipleContactItems = True def uniqueIdentifier(self): """ Uniquely identify this contact type. """ return qual(self.__class__).decode('ascii') def getParameters(self, contact): """ Return a list of L{liveform.Parameter} objects to be used to create L{liveform.LiveForm}s suitable for creating or editing contact information of this type. Override this in a subclass. @param contact: A contact item, values from which should be used as defaults in the parameters. C{None} if the parameters are for creating a new contact item. """ raise NotImplementedError("%s did not implement getParameters" % (self,)) def coerce(self, **kw): """ Callback for input validation. @param **kw: Mapping of submitted parameter names to values. @rtype: C{dict} @return: Mapping of coerced parameter names to values. """ return kw def getEditFormForPerson(self, person): """ Return C{None}. """ return None def getContactGroup(self, contactItem): """ Return C{None}. """ return None class SimpleReadOnlyView(Element): """ Simple read-only contact item view, suitable for returning from an implementation of L{IContactType.getReadOnlyView}. @ivar attribute: The contact item attribute which should be rendered. @type attribute: Axiom attribute (e.g. L{attributes.text}) @ivar contactItem: The contact item. @type contactItem: L{item.Item} """ docFactory = ThemedDocumentFactory( 'person-contact-read-only-view', 'store') def __init__(self, attribute, contactItem): Element.__init__(self) self.attribute = attribute self.contactItem = contactItem self.store = contactItem.store def attributeName(self, req, tag): """ Render the name of L{contactItem}'s class, e.g. "Email Address". """ return nameToLabel(self.contactItem.__class__.__name__) renderer(attributeName) def attributeValue(self, req, tag): """ Render the value of L{attribute} on L{contactItem}. """ return self.attribute.__get__(self.contactItem) renderer(attributeValue) class _PersonVIPStatus: """ Contact item type used by L{VIPPersonContactType}. @param person: The person whose VIP status we're interested in. @type person: L{Person} """ def __init__(self, person): self.person = person class VIPPersonContactType(BaseContactType): """ A contact type for controlling whether L{Person.vip} is set. """ implements(IContactType) allowMultipleContactItems = False def getParameters(self, contactItem): """ Return a list containing a single parameter suitable for changing the VIP status of a person. @type contactItem: L{_PersonVIPStatus} @rtype: C{list} of L{liveform.Parameter} """ isVIP = False # default if contactItem is not None: isVIP = contactItem.person.vip return [liveform.Parameter( 'vip', liveform.CHECKBOX_INPUT, bool, 'VIP', default=isVIP)] def getContactItems(self, person): """ Return a list containing a L{_PersonVIPStatus} instance for C{person}. @type person: L{Person} @rtype: C{list} of L{_PersonVIPStatus} """ return [_PersonVIPStatus(person)] def createContactItem(self, person, vip): """ Set the VIP status of C{person} to C{vip}. @type person: L{Person} @type vip: C{bool} @rtype: L{_PersonVIPStatus} """ person.vip = vip return _PersonVIPStatus(person) def editContactItem(self, contactItem, vip): """ Change the VIP status of C{contactItem}'s person to C{vip}. @type contactItem: L{_PersonVIPStatus} @type vip: C{bool} @rtype: C{NoneType} """ contactItem.person.vip = vip def getReadOnlyView(self, contactItem): """ Return a fragment which will render as the empty string. L{PersonSummaryView} handles the rendering of VIP status in the read-only L{Person} view. @type contactItem: L{_PersonVIPStatus} @rtype: L{Element} """ return Element(docFactory=stan(tags.invisible())) class EmailContactType(BaseContactType): """ Contact type plugin which allows a person to have an email address. @ivar store: The L{Store} the contact items will be created in. """ implements(IContactType) def __init__(self, store): self.store = store def getParameters(self, emailAddress): """ Return a C{list} of one L{LiveForm} parameter for editing an L{EmailAddress}. @type emailAddress: L{EmailAddress} or C{NoneType} @param emailAddress: If not C{None}, an existing contact item from which to get the email address default value. @rtype: C{list} @return: The parameters necessary for specifying an email address. """ if emailAddress is not None: address = emailAddress.address else: address = u'' return [ liveform.Parameter('email', liveform.TEXT_INPUT, _normalizeWhitespace, 'Email Address', default=address)] def descriptiveIdentifier(self): """ Return 'Email Address' """ return u'Email Address' def _existing(self, email): """ Return the existing L{EmailAddress} item with the given address, or C{None} if there isn't one. """ return self.store.findUnique( EmailAddress, EmailAddress.address == email, default=None) def createContactItem(self, person, email): """ Create a new L{EmailAddress} associated with the given person based on the given email address. @type person: L{Person} @param person: The person with whom to associate the new L{EmailAddress}. @type email: C{unicode} @param email: The value to use for the I{address} attribute of the newly created L{EmailAddress}. If C{''}, no L{EmailAddress} will be created. @return: C{None} """ if email: address = self._existing(email) if address is not None: raise ValueError('There is already a person with that email ' 'address (%s): ' % (address.person.name,)) return EmailAddress(store=self.store, address=email, person=person) def getContactItems(self, person): """ Return all L{EmailAddress} instances associated with the given person. @type person: L{Person} """ return person.store.query( EmailAddress, EmailAddress.person == person) def editContactItem(self, contact, email): """ Change the email address of the given L{EmailAddress} to that specified by C{email}. @type email: C{unicode} @param email: The new value to use for the I{address} attribute of the L{EmailAddress}. @return: C{None} """ address = self._existing(email) if address is not None and address is not contact: raise ValueError('There is already a person with that email ' 'address (%s): ' % (address.person.name,)) contact.address = email def getReadOnlyView(self, contact): """ Return a L{SimpleReadOnlyView} for the given L{EmailAddress}. """ return SimpleReadOnlyView(EmailAddress.address, contact) class PeopleBenefactor(item.Item): implements(ixmantissa.IBenefactor) endowed = attributes.integer(default=0) powerupNames = ["xmantissa.people.AddPerson"] class Person(item.Item): """ Person Per"son (p[~e]r"s'n; 277), n. 1. A character or part, as in a play; a specific kind or manifestation of individual character, whether in real life, or in literary or dramatic representation; an assumed character. [Archaic] [1913 Webster] This is Mantissa's simulation of a person, which has attached contact information. It is highly pluggable, mostly via the L{Organizer} object. Do not create this item directly, as functionality of L{IOrganizerPlugin} powerups will be broken if you do. Instead, use L{Organizer.createPerson}. """ typeName = 'mantissa_person' schemaVersion = 3 organizer = attributes.reference( doc=""" The L{Organizer} to which this Person belongs. """) name = attributes.text( doc=""" This name of this person. """, caseSensitive=False) created = attributes.timestamp(defaultFactory=extime.Time) vip = boolean( doc=""" Flag indicating this L{Person} is very important. """, default=False, allowNone=False) def getDisplayName(self): return self.name def getEmailAddresses(self): """ Return an iterator of all email addresses associated with this person. @return: an iterator of unicode strings in RFC2822 address format. """ return self.store.query( EmailAddress, EmailAddress.person == self).getColumn('address') def getEmailAddress(self): """ Return the default email address associated with this person. Note: this is effectively random right now if a person has more than one address. It's just the first address returned. This should be fixed in a future version. @return: a unicode string in RFC2822 address format. """ for a in self.getEmailAddresses(): return a def getMugshot(self): """ Return the L{Mugshot} associated with this L{Person}, or an unstored L{Mugshot} pointing at a placeholder mugshot image. """ mugshot = self.store.findUnique( Mugshot, Mugshot.person == self, default=None) if mugshot is not None: return mugshot return Mugshot.placeholderForPerson(self) def registerExtract(self, extract, timestamp=None): """ @param extract: some Item that implements L{inevow.IRenderer} """ if timestamp is None: timestamp = extime.Time() return ExtractWrapper(store=self.store, extract=extract, timestamp=timestamp, person=self) def getExtractWrappers(self, n): return self.store.query(ExtractWrapper, ExtractWrapper.person == self, sort=ExtractWrapper.timestamp.desc, limit=n) item.declareLegacyItem( Person.typeName, 1, dict(organizer=attributes.reference(), name=attributes.text(caseSensitive=True), created=attributes.timestamp())) registerAttributeCopyingUpgrader(Person, 1, 2) item.declareLegacyItem( Person.typeName, 2, dict(organizer=attributes.reference(), name=attributes.text(caseSensitive=True), created=attributes.timestamp(), vip=attributes.boolean(default=False, allowNone=False))) registerAttributeCopyingUpgrader(Person, 2, 3) class ExtractWrapper(item.Item): extract = attributes.reference(whenDeleted=attributes.reference.CASCADE) timestamp = attributes.timestamp(indexed=True) person = attributes.reference(reftype=Person, whenDeleted=attributes.reference.CASCADE) def _stringifyKeys(d): """ Return a copy of C{d} with C{str} keys. @type d: C{dict} with C{unicode} keys. @rtype: C{dict} with C{str} keys. """ return dict((k.encode('ascii'), v) for (k, v) in d.iteritems()) class AllPeopleFilter(object): """ L{IPeopleFilter} which includes all L{Person} items from the given store in its query. """ implements(IPeopleFilter) filterName = 'All' def getPeopleQueryComparison(self, store): """ @see IPeopleFilter.getPeopleQueryComparison """ return None class VIPPeopleFilter(object): """ L{IPeopleFilter} which includes all VIP L{Person} items from the given store in its query. """ implements(IPeopleFilter) filterName = 'VIP' def getPeopleQueryComparison(self, store): """ @see IPeopleFilter.getPeopleQueryComparison """ return Person.vip == True class TaggedPeopleFilter(record('filterName')): """ L{IPeopleFilter} which includes in its query all L{Person} items to which a specific tag has been applied. """ implements(IPeopleFilter) def getPeopleQueryComparison(self, store): """ @see IPeopleFilter.getPeopleQueryComparison """ return attributes.AND( Tag.object == Person.storeID, Tag.name == self.filterName) class BaseOrganizerPlugin(object): """ Base class for L{IOrganizerPlugin} implementations, which provides null implementations of the interface's callback/notification methods. """ name = requiredAttribute('name') def personCreated(self, person): """ Do nothing. @see IOrganizerPlugin.personCreated """ def personNameChanged(self, person, name): """ Do nothing. @see IOrganizerPlugin.personNameChanged """ def contactItemCreated(self, item): """ Do nothing. @see IOrganizerPlugin.contactItemCreated """ def contactItemEdited(self, item): """ Do nothing. @see IOrganizerPlugin.contactItemEdited """ class ContactInfoOrganizerPlugin(BaseOrganizerPlugin): """ Trivial in-memory L{IOrganizerPlugin}. """ implements(IOrganizerPlugin) name = u'Contact' def getContactTypes(self): """ No contact types. @see IOrganizerPlugin.getContactTypes """ return () def getPeopleFilters(self): """ No people filters. @see IOrganizerPlugin.getPeopleFilters """ return () def personalize(self, person): """ Return a L{ReadOnlyContactInfoView} for C{person}. @see IOrganizerPlugin.personalize """ return ReadOnlyContactInfoView(person) class Organizer(item.Item): """ Oversee the creation, location, destruction, and modification of people in a particular set (eg, the set of people you know). """ implements(ixmantissa.INavigableElement) typeName = 'mantissa_people' schemaVersion = 3 _webTranslator = dependsOn(PrivateApplication) storeOwnerPerson = attributes.reference( doc="A L{Person} representing the owner of the store this organizer lives in", reftype=Person, whenDeleted=attributes.reference.DISALLOW) powerupInterfaces = (ixmantissa.INavigableElement,) def __init__(self, *a, **k): super(Organizer, self).__init__(*a, **k) if 'storeOwnerPerson' not in k: self.storeOwnerPerson = self._makeStoreOwnerPerson() def _makeStoreOwnerPerson(self): """ Make a L{Person} representing the owner of the store that this L{Organizer} is installed in. @rtype: L{Person} """ if self.store is None: return None userInfo = self.store.findFirst(signup.UserInfo) name = u'' if userInfo is not None: name = userInfo.realName account = self.store.findUnique(LoginAccount, LoginAccount.avatars == self.store, None) ownerPerson = self.createPerson(name) if account is not None: for method in (self.store.query( LoginMethod, attributes.AND(LoginMethod.account == account, LoginMethod.internal == False))): self.createContactItem( EmailContactType(self.store), ownerPerson, dict( email=method.localpart + u'@' + method.domain)) return ownerPerson def getOrganizerPlugins(self): """ Return an iterator of the installed L{IOrganizerPlugin} powerups. """ return (list(self.store.powerupsFor(IOrganizerPlugin)) + [ContactInfoOrganizerPlugin()]) def _gatherPluginMethods(self, methodName): """ Walk through each L{IOrganizerPlugin} powerup, yielding the bound method if the powerup implements C{methodName}. Upon encountering a plugin which fails to implement it, issue a L{PendingDeprecationWarning}. @param methodName: The name of a L{IOrganizerPlugin} method. @type methodName: C{str} @return: Iterable of methods. """ for plugin in self.getOrganizerPlugins(): implementation = getattr(plugin, methodName, None) if implementation is not None: yield implementation else: warn( ('IOrganizerPlugin now has the %r method, %s' ' did not implement it') % ( methodName, plugin.__class__), category=PendingDeprecationWarning) def _checkContactType(self, contactType): """ Possibly emit some warnings about C{contactType}'s implementation of L{IContactType}. @type contactType: L{IContactType} provider """ if getattr(contactType, 'getEditFormForPerson', None) is None: warn( "IContactType now has the 'getEditFormForPerson'" " method, but %s did not implement it." % ( contactType.__class__,), category=PendingDeprecationWarning) if getattr(contactType, 'getEditorialForm', None) is not None: warn( "The IContactType %s defines the 'getEditorialForm'" " method, which is deprecated. 'getEditFormForPerson'" " does something vaguely similar." % (contactType.__class__,), category=DeprecationWarning) def getContactTypes(self): """ Return an iterator of L{IContactType} providers available to this organizer's store. """ yield VIPPersonContactType() yield EmailContactType(self.store) yield PostalContactType() yield PhoneNumberContactType() yield NotesContactType() for getContactTypes in self._gatherPluginMethods('getContactTypes'): for contactType in getContactTypes(): self._checkContactType(contactType) yield contactType def getPeopleFilters(self): """ Return an iterator of L{IPeopleFilter} providers available to this organizer's store. """ yield AllPeopleFilter() yield VIPPeopleFilter() for getPeopleFilters in self._gatherPluginMethods('getPeopleFilters'): for peopleFilter in getPeopleFilters(): yield peopleFilter for tag in sorted(self.getPeopleTags()): yield TaggedPeopleFilter(tag) def getPeopleTags(self): """ Return a sequence of tags which have been applied to L{Person} items. @rtype: C{set} """ query = self.store.query( Tag, Tag.object == Person.storeID) return set(query.getColumn('name').distinct()) def groupReadOnlyViews(self, person): """ Collect all contact items from the available contact types for the given person, organize them by contact group, and turn them into read-only views. @type person: L{Person} @param person: The person whose contact items we're interested in. @return: A mapping of of L{ContactGroup} names to the read-only views of their member contact items, with C{None} being the key for groupless contact items. @rtype: C{dict} of C{str} """ # this is a slightly awkward, specific API, but at the time of # writing, read-only views are the thing that the only caller cares # about. we need the contact type to get a read-only view for a # contact item. there is no way to get from a contact item to a # contact type, so this method can't be "groupContactItems" (which # seems to make more sense), unless it returned some weird data # structure which managed to associate contact items and contact # types. grouped = {} for contactType in self.getContactTypes(): for contactItem in contactType.getContactItems(person): contactGroup = contactType.getContactGroup(contactItem) if contactGroup is not None: contactGroup = contactGroup.groupName if contactGroup not in grouped: grouped[contactGroup] = [] grouped[contactGroup].append( contactType.getReadOnlyView(contactItem)) return grouped def getContactCreationParameters(self): """ Yield a L{Parameter} for each L{IContactType} known. Each yielded object can be used with a L{LiveForm} to create a new instance of a particular L{IContactType}. """ for contactType in self.getContactTypes(): if contactType.allowMultipleContactItems: descriptiveIdentifier = _descriptiveIdentifier(contactType) yield liveform.ListChangeParameter( contactType.uniqueIdentifier(), contactType.getParameters(None), defaults=[], modelObjects=[], modelObjectDescription=descriptiveIdentifier) else: yield liveform.FormParameter( contactType.uniqueIdentifier(), liveform.LiveForm( lambda **k: k, contactType.getParameters(None))) def _parametersToDefaults(self, parameters): """ Extract the defaults from C{parameters}, constructing a dictionary mapping parameter names to default values, suitable for passing to L{ListChangeParameter}. @type parameters: C{list} of L{liveform.Parameter} or L{liveform.ChoiceParameter}. @rtype: C{dict} """ defaults = {} for p in parameters: if isinstance(p, liveform.ChoiceParameter): selected = [] for choice in p.choices: if choice.selected: selected.append(choice.value) defaults[p.name] = selected else: defaults[p.name] = p.default return defaults def toContactEditorialParameter(self, contactType, person): """ Convert the given contact type into a L{liveform.LiveForm} parameter. @type contactType: L{IContactType} provider. @type person: L{Person} @rtype: L{liveform.Parameter} or similar. """ contactItems = list(contactType.getContactItems(person)) if contactType.allowMultipleContactItems: defaults = [] modelObjects = [] for contactItem in contactItems: defaultedParameters = contactType.getParameters(contactItem) if defaultedParameters is None: continue defaults.append(self._parametersToDefaults( defaultedParameters)) modelObjects.append(contactItem) descriptiveIdentifier = _descriptiveIdentifier(contactType) return liveform.ListChangeParameter( contactType.uniqueIdentifier(), contactType.getParameters(None), defaults=defaults, modelObjects=modelObjects, modelObjectDescription=descriptiveIdentifier) (contactItem,) = contactItems return liveform.FormParameter( contactType.uniqueIdentifier(), liveform.LiveForm( lambda **k: k, contactType.getParameters(contactItem))) def getContactEditorialParameters(self, person): """ Yield L{LiveForm} parameters to edit each contact item of each contact type for the given person. @type person: L{Person} @return: An iterable of two-tuples. The first element of each tuple is an L{IContactType} provider. The third element of each tuple is the L{LiveForm} parameter object for that contact item. """ for contactType in self.getContactTypes(): yield ( contactType, self.toContactEditorialParameter(contactType, person)) _NO_VIP = object() def createPerson(self, nickname, vip=_NO_VIP): """ Create a new L{Person} with the given name in this organizer. @type nickname: C{unicode} @param nickname: The value for the new person's C{name} attribute. @type vip: C{bool} @param vip: Value to set the created person's C{vip} attribute to (deprecated). @rtype: L{Person} """ for person in (self.store.query( Person, attributes.AND( Person.name == nickname, Person.organizer == self))): raise ValueError("Person with name %r exists already." % (nickname,)) person = Person( store=self.store, created=extime.Time(), organizer=self, name=nickname) if vip is not self._NO_VIP: warn( "Usage of Organizer.createPerson's 'vip' parameter" " is deprecated", category=DeprecationWarning) person.vip = vip self._callOnOrganizerPlugins('personCreated', person) return person def createContactItem(self, contactType, person, contactInfo): """ Create a new contact item for the given person with the given contact type. Broadcast a creation to all L{IOrganizerPlugin} powerups. @type contactType: L{IContactType} @param contactType: The contact type which will be used to create the contact item. @type person: L{Person} @param person: The person with whom the contact item will be associated. @type contactInfo: C{dict} @param contactInfo: The contact information to use to create the contact item. @return: The contact item, as created by the given contact type. """ contactItem = contactType.createContactItem( person, **_stringifyKeys(contactInfo)) if contactItem is not None: self._callOnOrganizerPlugins('contactItemCreated', contactItem) return contactItem def editContactItem(self, contactType, contactItem, contactInfo): """ Edit the given contact item with the given contact type. Broadcast the edit to all L{IOrganizerPlugin} powerups. @type contactType: L{IContactType} @param contactType: The contact type which will be used to edit the contact item. @param contactItem: The contact item to edit. @type contactInfo: C{dict} @param contactInfo: The contact information to use to edit the contact item. @return: C{None} """ contactType.editContactItem( contactItem, **_stringifyKeys(contactInfo)) self._callOnOrganizerPlugins('contactItemEdited', contactItem) def _callOnOrganizerPlugins(self, methodName, *args): """ Call a method on all L{IOrganizerPlugin} powerups on C{self.store}, or emit a deprecation warning for each one which does not implement that method. """ for observer in self.getOrganizerPlugins(): method = getattr(observer, methodName, None) if method is not None: method(*args) else: warn( "IOrganizerPlugin now has the %s method, %s " "did not implement it" % (methodName, observer.__class__,), category=PendingDeprecationWarning) def editPerson(self, person, nickname, edits): """ Change the name and contact information associated with the given L{Person}. @type person: L{Person} @param person: The person which will be modified. @type nickname: C{unicode} @param nickname: The new value for L{Person.name} @type edits: C{list} @param edits: list of tuples of L{IContactType} providers and corresponding L{ListChanges} objects or dictionaries of parameter values. """ for existing in self.store.query(Person, Person.name == nickname): if existing is person: continue raise ValueError( "A person with the name %r exists already." % (nickname,)) oldname = person.name person.name = nickname self._callOnOrganizerPlugins('personNameChanged', person, oldname) for contactType, submission in edits: if contactType.allowMultipleContactItems: for edit in submission.edit: self.editContactItem( contactType, edit.object, edit.values) for create in submission.create: create.setter( self.createContactItem( contactType, person, create.values)) for delete in submission.delete: delete.deleteFromStore() else: (contactItem,) = contactType.getContactItems(person) self.editContactItem( contactType, contactItem, submission) def deletePerson(self, person): """ Delete the given person from the store. """ person.deleteFromStore() def personByName(self, name): """ Retrieve the L{Person} item for the given Q2Q address, creating it first if necessary. @type name: C{unicode} """ return self.store.findOrCreate(Person, organizer=self, name=name) def personByEmailAddress(self, address): """ Retrieve the L{Person} item for the given email address (or return None if no such person exists) @type name: C{unicode} """ email = self.store.findUnique(EmailAddress, EmailAddress.address == address, default=None) if email is not None: return email.person def linkToPerson(self, person): """ @param person: L{Person} instance @return: string url at which C{person} will be rendered """ return self._webTranslator.linkTo(person.storeID) def urlForViewState(self, person, viewState): """ Return a url for L{OrganizerFragment} which will display C{person} in state C{viewState}. @type person: L{Person} @type viewState: L{ORGANIZER_VIEW_STATES} constant. @rtype: L{url.URL} """ # ideally there would be a more general mechanism for encoding state # like this in a url, rather than ad-hoc query arguments for each # fragment which needs to do it. organizerURL = self._webTranslator.linkTo(self.storeID) return url.URL( netloc='', scheme='', pathsegs=organizerURL.split('/')[1:], querysegs=(('initial-person', person.name), ('initial-state', viewState))) # INavigableElement def getTabs(self): """ Implement L{INavigableElement.getTabs} to return a single tab, 'People', that points to this item. """ return [webnav.Tab('People', self.storeID, 0.5, authoritative=True)] def organizer1to2(old): o = old.upgradeVersion(old.typeName, 1, 2) o._webTranslator = old.store.findOrCreate(PrivateApplication) return o registerUpgrader(organizer1to2, Organizer.typeName, 1, 2) item.declareLegacyItem(Organizer.typeName, 2, dict(_webTranslator=attributes.reference())) registerAttributeCopyingUpgrader(Organizer, 2, 3) class VIPColumn(UnsortableColumn): def getType(self): return 'boolean' class MugshotURLColumn(record('organizer attributeID')): """ L{ixmantissa.IColumn} provider which extracts the URL of a L{Person}'s mugshot. @type organizer: L{Organizer} """ implements(ixmantissa.IColumn) def extractValue(self, model, item): """ Figure out the URL of C{item}'s mugshot. @type item: L{Person} @rtype: C{unicode} """ return self.organizer.linkToPerson(item) + u'/mugshot/smaller' def sortAttribute(self): """ Return C{None}. Unsortable. """ return None def getType(self): """ Return C{text} """ return 'text' def toComparableValue(self, value): """ This shouldn't be called. @raise L{NotImplementedError}: Always. """ raise NotImplementedError( '%r does not implement toComparableValue: it is unsortable' % (self.__class__,)) class PersonScrollingFragment(ScrollingElement): """ Scrolling element which displays L{Person} objects and allows actions to be taken on them. @type organizer: L{Organizer} """ jsClass = u'Mantissa.People.PersonScroller' def __init__(self, organizer, baseConstraint, defaultSortColumn, webTranslator): self._originalBaseConstraint = baseConstraint ScrollingElement.__init__( self, organizer.store, Person, baseConstraint, [MugshotURLColumn(organizer, 'mugshotURL'), VIPColumn(Person.vip, 'vip'), Person.name], defaultSortColumn=defaultSortColumn, webTranslator=webTranslator) self.organizer = organizer self.filters = dict((filter.filterName, filter) for filter in organizer.getPeopleFilters()) def getInitialArguments(self): """ Include L{organizer}'s C{storeOwnerPerson}'s name. """ return (ScrollingElement.getInitialArguments(self) + [self.organizer.storeOwnerPerson.name]) def filterByFilter(self, filterName): """ Swap L{baseConstraint} with the result of calling L{IPeopleFilter.getPeopleQueryComparison} on the named filter. @type filterName: C{unicode} """ filter = self.filters[filterName] self.baseConstraint = filter.getPeopleQueryComparison(self.store) expose(filterByFilter) class PersonSummaryView(Element): """ Fragment which renders a business card-like summary of a L{Person}: their mugshot, vip status, and name. @type person: L{Person} @ivar person: The person to summarize. """ docFactory = ThemedDocumentFactory('person-summary', 'store') def __init__(self, person): self.person = person self.organizer = person.organizer self.store = person.store def mugshotURL(self, req, tag): """ Render the URL of L{person}'s mugshot, or the URL of a placeholder mugshot if they don't have one set. """ return self.organizer.linkToPerson(self.person) + '/mugshot/smaller' renderer(mugshotURL) def personName(self, req, tag): """ Render the display name of L{person}. """ return self.person.getDisplayName() renderer(personName) def vipStatus(self, req, tag): """ Return C{tag} if L{person} is a VIP, otherwise return the empty string. """ if self.person.vip: return tag return '' renderer(vipStatus) class ReadOnlyContactInfoView(Element): """ Fragment which renders a read-only version of a person's contact information. @ivar person: A person. @type person: L{Person} """ docFactory = ThemedDocumentFactory( 'person-read-only-contact-info', 'store') def __init__(self, person): self.person = person self.organizer = person.organizer self.store = person.store Element.__init__(self) def personSummary(self, request, tag): """ Render a L{PersonSummaryView} for L{person}. """ return PersonSummaryView(self.person) renderer(personSummary) def contactInfo(self, request, tag): """ Render the result of calling L{IContactType.getReadOnlyView} on the corresponding L{IContactType} for each piece of contact info associated with L{person}. Arrange the result by group, using C{tag}'s I{contact-group} pattern. Groupless contact items will have their views yielded directly. The I{contact-group} pattern appears once for each distinct L{ContactGroup}, with the following slots filled: I{name} - The group's C{groupName}. I{views} - A sequence of read-only views belonging to the group. """ groupPattern = inevow.IQ(tag).patternGenerator('contact-group') groupedViews = self.organizer.groupReadOnlyViews(self.person) for (groupName, views) in groupedViews.iteritems(): if groupName is None: yield views else: yield groupPattern().fillSlots( 'name', groupName).fillSlots( 'views', views) renderer(contactInfo) class ORGANIZER_VIEW_STATES: """ Some constants describing possible initial states of L{OrganizerFragment}. @ivar EDIT: The state which involves editing a person. @ivar ALL_STATES: A sequence of all valid initial states. """ EDIT = u'edit' ALL_STATES = (EDIT,) class _ElementWrapper(LiveElement): """ L{LiveElement} which wraps & renders an L{Element}. @type wrapped: L{Element} """ docFactory = stan( tags.div(render=tags.directive('liveElement'))[ tags.directive('element')]) def __init__(self, wrapped): LiveElement.__init__(self) self.wrapped = wrapped def element(self, request, tag): """ Render L{wrapped}. """ return self.wrapped renderer(element) class PersonPluginView(LiveElement): """ Element which renders UI for selecting between the views of available person plugins. A tab will be rendered for each L{IOrganizerPlugin} in L{plugins}, with the corresponding personalization being rendered when a tab is selected. @ivar plugins: Sequence of L{IOrganizerPlugin} providers. @type plugins: C{list} @ivar person: The person we're interested in. @type person: L{Person} """ docFactory = ThemedDocumentFactory('person-plugins', 'store') jsClass = u'Mantissa.People.PersonPluginView' def __init__(self, plugins, person): LiveElement.__init__(self) self.plugins = plugins self.person = person self.store = person.store def pluginTabbedPane(self, request, tag): """ Render a L{tabbedPane.TabbedPaneFragment} with an entry for each item in L{plugins}. """ iq = inevow.IQ(tag) tabNames = [ _organizerPluginName(p).encode('ascii') # gunk for p in self.plugins] child = tabbedPane.TabbedPaneFragment( zip(tabNames, ([self.getPluginWidget(tabNames[0])] + [iq.onePattern('pane-body') for _ in tabNames[1:]]))) child.jsClass = u'Mantissa.People.PluginTabbedPane' child.setFragmentParent(self) return child renderer(pluginTabbedPane) def _toLiveElement(self, element): """ Wrap the given element in a L{LiveElement} if it is not already one. @rtype: L{LiveElement} """ if isinstance(element, LiveElement): return element return _ElementWrapper(element) def getPluginWidget(self, pluginName): """ Return the named plugin's view. @type pluginName: C{unicode} @param pluginName: The name of the plugin. @rtype: L{LiveElement} """ # this will always pick the first plugin with pluginName if there is # more than one. don't do that. for plugin in self.plugins: if _organizerPluginName(plugin) == pluginName: view = self._toLiveElement( plugin.personalize(self.person)) view.setFragmentParent(self) return view expose(getPluginWidget) class OrganizerFragment(LiveElement): """ Address book view. The initial state of this fragment can be extracted from the query parameters in its url, if present. The two parameters it looks for are: I{initial-person} (the name of the L{Person} to select initially in the scrolltable) and I{initial-state} (a L{ORGANIZER_VIEW_STATES} constant describing what to do with the person). Both query arguments must be present if either is. @type organizer: L{Organizer} @ivar organizer: The organizer for which this is a view. @ivar initialPerson: The person to load initially. Defaults to C{None}. @type initialPerson: L{Person} or C{NoneType} @ivar initialState: The initial state of the organizer view. Defaults to C{None}. @type initialState: L{ORGANIZER_VIEW_STATES} or C{NoneType} """ docFactory = ThemedDocumentFactory('people-organizer', 'store') fragmentName = None live = 'athena' title = 'People' jsClass = u'Mantissa.People.Organizer' def __init__(self, organizer, initialPerson=None, initialState=None): LiveElement.__init__(self) self.organizer = organizer self.initialPerson = initialPerson self.initialState = initialState self.store = organizer.store self.wt = organizer._webTranslator def head(self): """ Do nothing. """ def beforeRender(self, ctx): """ Implement this hook to initialize the L{initialPerson} and L{initialState} slots with information from the request url's query args. """ # see the comment in Organizer.urlForViewState which suggests an # alternate implementation of this kind of functionality. request = inevow.IRequest(ctx) if not set(['initial-person', 'initial-state']).issubset( # <= set(request.args)): return initialPersonName = request.args['initial-person'][0].decode('utf-8') initialPerson = self.store.findFirst( Person, Person.name == initialPersonName) if initialPerson is None: return initialState = request.args['initial-state'][0].decode('utf-8') if initialState not in ORGANIZER_VIEW_STATES.ALL_STATES: return self.initialPerson = initialPerson self.initialState = initialState def getInitialArguments(self): """ Include L{organizer}'s C{storeOwnerPerson}'s name, and the name of L{initialPerson} and the value of L{initialState}, if they are set. """ initialArguments = (self.organizer.storeOwnerPerson.name,) if self.initialPerson is not None: initialArguments += (self.initialPerson.name, self.initialState) return initialArguments def getAddPerson(self): """ Return an L{AddPersonFragment} which is a child of this fragment and which will add a person to C{self.organizer}. """ fragment = AddPersonFragment(self.organizer) fragment.setFragmentParent(self) return fragment expose(getAddPerson) def getImportPeople(self): """ Return an L{ImportPeopleWidget} which is a child of this fragment and which will add people to C{self.organizer}. """ fragment = ImportPeopleWidget(self.organizer) fragment.setFragmentParent(self) return fragment expose(getImportPeople) def getEditPerson(self, name): """ Get an L{EditPersonView} for editing the person named C{name}. @param name: A person name. @type name: C{unicode} @rtype: L{EditPersonView} """ view = EditPersonView(self.organizer.personByName(name)) view.setFragmentParent(self) return view expose(getEditPerson) def deletePerson(self, name): """ Delete the person named C{name} @param name: A person name. @type name: C{unicode} """ self.organizer.deletePerson(self.organizer.personByName(name)) expose(deletePerson) def peopleTable(self, request, tag): """ Return a L{PersonScrollingFragment} which will display the L{Person} items in the wrapped organizer. """ f = PersonScrollingFragment( self.organizer, None, Person.name, self.wt) f.setFragmentParent(self) f.docFactory = webtheme.getLoader(f.fragmentName) return f renderer(peopleTable) def peopleFilters(self, request, tag): """ Return an instance of C{tag}'s I{filter} pattern for each filter we get from L{Organizer.getPeopleFilters}, filling the I{name} slot with the filter's name. The first filter will be rendered using the I{selected-filter} pattern. """ filters = iter(self.organizer.getPeopleFilters()) # at some point we might actually want to look at what filter is # yielded first, and filter the person list accordingly. we're just # going to assume it's the "All" filter, and leave the person list # untouched for now. yield tag.onePattern('selected-filter').fillSlots( 'name', filters.next().filterName) pattern = tag.patternGenerator('filter') for filter in filters: yield pattern.fillSlots('name', filter.filterName) renderer(peopleFilters) def getPersonPluginWidget(self, name): """ Return the L{PersonPluginView} for the named person. @type name: C{unicode} @param name: A value which corresponds to the I{name} attribute of an extant L{Person}. @rtype: L{PersonPluginView} """ fragment = PersonPluginView( self.organizer.getOrganizerPlugins(), self.organizer.personByName(name)) fragment.setFragmentParent(self) return fragment expose(getPersonPluginWidget) components.registerAdapter(OrganizerFragment, Organizer, ixmantissa.INavigableFragment) class EditPersonView(LiveElement): """ Render a view for editing the contact information for a L{Person}. @ivar person: L{Person} which can be edited. @ivar contactTypes: A mapping from parameter names to the L{IContactTypes} whose items the parameters are editing. """ docFactory = ThemedDocumentFactory('edit-person', 'store') fragmentName = 'edit-person' jsClass = u'Mantissa.People.EditPerson' def __init__(self, person): athena.LiveElement.__init__(self) self.person = person self.store = person.store self.organizer = person.organizer self.contactTypes = {} def editContactItems(self, nickname, **edits): """ Update the information on the contact items associated with the wrapped L{Person}. @type nickname: C{unicode} @param nickname: New value to use for the I{name} attribute of the L{Person}. @param **edits: mapping from contact type identifiers to ListChanges instances. """ submissions = [] for paramName, submission in edits.iteritems(): contactType = self.contactTypes[paramName] submissions.append((contactType, submission)) self.person.store.transact( self.organizer.editPerson, self.person, nickname, submissions) def makeEditorialLiveForms(self): """ Make some L{liveform.LiveForm} instances for editing the contact information of the wrapped L{Person}. """ parameters = [ liveform.Parameter( 'nickname', liveform.TEXT_INPUT, _normalizeWhitespace, 'Name', default=self.person.name)] separateForms = [] for contactType in self.organizer.getContactTypes(): if getattr(contactType, 'getEditFormForPerson', None): editForm = contactType.getEditFormForPerson(self.person) if editForm is not None: editForm.setFragmentParent(self) separateForms.append(editForm) continue param = self.organizer.toContactEditorialParameter( contactType, self.person) parameters.append(param) self.contactTypes[param.name] = contactType form = liveform.LiveForm( self.editContactItems, parameters, u'Save') form.compact() form.jsClass = u'Mantissa.People.EditPersonForm' form.setFragmentParent(self) return [form] + separateForms def mugshotFormURL(self, request, tag): """ Render a URL for L{MugshotUploadForm}. """ return self.organizer.linkToPerson(self.person) + '/mugshotUploadForm' renderer(mugshotFormURL) def editorialContactForms(self, request, tag): """ Put the result of L{makeEditorialLiveForms} in C{tag}. """ return tag[self.makeEditorialLiveForms()] renderer(editorialContactForms) class RealName(item.Item): """ This is a legacy item left over from a previous schema. Do not create it. """ typeName = 'mantissa_organizer_addressbook_realname' schemaVersion = 2 empty = attributes.reference() item.declareLegacyItem( RealName.typeName, 1, dict(person=attributes.reference( doc=""" allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Person """), first=attributes.text(), last=attributes.text(indexed=True))) registerDeletionUpgrader(RealName, 1, 2) class EmailAddress(item.Item): """ An email address contact info item associated with a L{Person}. Do not create this item directly, as functionality of L{IOrganizerPlugin} powerups will be broken if you do. Instead, use L{Organizer.createContactItem} with L{EmailContactType}. """ typeName = 'mantissa_organizer_addressbook_emailaddress' schemaVersion = 3 address = attributes.text(allowNone=False) person = attributes.reference( allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Person) label = attributes.text( """ This is a label for the role of the email address, usually something like "home", "work", "school". """, allowNone=False, default=u'') def emailAddress1to2(old): return old.upgradeVersion('mantissa_organizer_addressbook_emailaddress', 1, 2, address=old.address, person=old.person) registerUpgrader(emailAddress1to2, 'mantissa_organizer_addressbook_emailaddress', 1, 2) item.declareLegacyItem(EmailAddress.typeName, 2, dict( address = attributes.text(allowNone=False), person = attributes.reference(allowNone=False))) registerAttributeCopyingUpgrader(EmailAddress, 2, 3) class PhoneNumber(item.Item): """ A contact information item representing a L{Person}'s phone number. Do not create this item directly, as functionality of L{IOrganizerPlugin} powerups will be broken if you do. Instead, use L{Organizer.createContactItem} with L{PhoneNumberContactType}. """ typeName = 'mantissa_organizer_addressbook_phonenumber' schemaVersion = 3 number = attributes.text(allowNone=False) person = attributes.reference(allowNone=False) label = attributes.text( """ This is a label for the role of the phone number. """, allowNone=False, default=u'',) class LABELS: """ Constants to use for the value of the L{label} attribute, describing the type of the telephone number. @ivar HOME: This is a home phone number. @type HOME: C{unicode} @ivar WORK: This is a work phone number. @type WORK: C{unicode} @ivar MOBILE: This is a mobile phone number. @type MOBILE: C{unicode} @ivar HOME_FAX: This is the 80's and someone has a fax machine in their house. @type HOME_FAX: C{unicode} @ivar WORK_FAX: This is the 80's and someone has a fax machine in their office. @type WORK_FAX: C{unicode} @ivar PAGER: This is the 80's and L{person} is a drug dealer. @type PAGER: C{unicode} @ivar ALL_LABELS: A sequence of all of the label constants. @type ALL_LABELS: C{list} """ HOME = u'Home' WORK = u'Work' MOBILE = u'Mobile' HOME_FAX = u'Home Fax' WORK_FAX = u'Work Fax' PAGER = u'Pager' ALL_LABELS = [HOME, WORK, MOBILE, HOME_FAX, WORK_FAX, PAGER] def phoneNumber1to2(old): return old.upgradeVersion('mantissa_organizer_addressbook_phonenumber', 1, 2, number=old.number, person=old.person) item.declareLegacyItem(PhoneNumber.typeName, 2, dict( number = attributes.text(allowNone=False), person = attributes.reference(allowNone=False))) registerUpgrader(phoneNumber1to2, 'mantissa_organizer_addressbook_phonenumber', 1, 2) registerAttributeCopyingUpgrader(PhoneNumber, 2, 3) class PhoneNumberContactType(BaseContactType): """ Contact type plugin which allows telephone numbers to be associated with people. """ implements(IContactType) def getParameters(self, phoneNumber): """ Return a C{list} of two liveform parameters, one for editing C{phoneNumber}'s I{number} attribute, and one for editing its I{label} attribute. @type phoneNumber: L{PhoneNumber} or C{NoneType} @param phoneNumber: If not C{None}, an existing contact item from which to get the parameter's default values. @rtype: C{list} """ defaultNumber = u'' defaultLabel = PhoneNumber.LABELS.HOME if phoneNumber is not None: defaultNumber = phoneNumber.number defaultLabel = phoneNumber.label labelChoiceParameter = liveform.ChoiceParameter( 'label', [liveform.Option(label, label, label == defaultLabel) for label in PhoneNumber.LABELS.ALL_LABELS], 'Number Type') return [ labelChoiceParameter, liveform.Parameter( 'number', liveform.TEXT_INPUT, unicode, 'Phone Number', default=defaultNumber)] def descriptiveIdentifier(self): """ Return 'Phone Number' """ return u'Phone Number' def createContactItem(self, person, label, number): """ Create a L{PhoneNumber} item for C{number}, associated with C{person}. @type person: L{Person} @param label: The value to use for the I{label} attribute of the new L{PhoneNumber} item. @type label: C{unicode} @param number: The value to use for the I{number} attribute of the new L{PhoneNumber} item. If C{''}, no item will be created. @type number: C{unicode} @rtype: L{PhoneNumber} or C{NoneType} """ if number: return PhoneNumber( store=person.store, person=person, label=label, number=number) def editContactItem(self, contact, label, number): """ Change the I{number} attribute of C{contact} to C{number}, and the I{label} attribute to C{label}. @type contact: L{PhoneNumber} @type label: C{unicode} @type number: C{unicode} @return: C{None} """ contact.label = label contact.number = number def getContactItems(self, person): """ Return an iterable of L{PhoneNumber} items that are associated with C{person}. @type person: L{Person} """ return person.store.query( PhoneNumber, PhoneNumber.person == person) def getReadOnlyView(self, contact): """ Return a L{ReadOnlyPhoneNumberView} for the given L{PhoneNumber}. """ return ReadOnlyPhoneNumberView(contact) class ReadOnlyPhoneNumberView(Element): """ Read-only view for L{PhoneNumber}. @type phoneNumber: L{PhoneNumber} """ docFactory = ThemedDocumentFactory( 'person-contact-read-only-phone-number-view', 'store') def __init__(self, phoneNumber): self.phoneNumber = phoneNumber self.store = phoneNumber.store def number(self, request, tag): """ Add the value of L{phoneNumber}'s I{number} attribute to C{tag}. """ return tag[self.phoneNumber.number] renderer(number) def label(self, request, tag): """ Add the value of L{phoneNumber}'s I{label} attribute to C{tag}. """ return tag[self.phoneNumber.label] renderer(label) class PostalAddress(item.Item): typeName = 'mantissa_organizer_addressbook_postaladdress' address = attributes.text(allowNone=False) person = attributes.reference( allowNone=False, whenDeleted=attributes.reference.CASCADE, reftype=Person) class PostalContactType(BaseContactType): """ Contact type plugin which allows a person to have a snail-mail address. """ implements(IContactType) def getParameters(self, postalAddress): """ Return a C{list} of one L{LiveForm} parameter for editing a L{PostalAddress}. @type postalAddress: L{PostalAddress} or C{NoneType} @param postalAddress: If not C{None}, an existing contact item from which to get the postal address default value. @rtype: C{list} @return: The parameters necessary for specifying a postal address. """ address = u'' if postalAddress is not None: address = postalAddress.address return [ liveform.Parameter('address', liveform.TEXT_INPUT, unicode, 'Postal Address', default=address)] def descriptiveIdentifier(self): """ Return 'Postal Address' """ return u'Postal Address' def createContactItem(self, person, address): """ Create a new L{PostalAddress} associated with the given person based on the given postal address. @type person: L{Person} @param person: The person with whom to associate the new L{EmailAddress}. @type address: C{unicode} @param address: The value to use for the I{address} attribute of the newly created L{PostalAddress}. If C{''}, no L{PostalAddress} will be created. @rtype: L{PostalAddress} or C{NoneType} """ if address: return PostalAddress( store=person.store, person=person, address=address) def editContactItem(self, contact, address): """ Change the postal address of the given L{PostalAddress} to that specified by C{address}. @type contact: L{PostalAddress} @param contact: The existing contact item to modify. @type address: C{unicode} @param address: The new value to use for the I{address} attribute of the L{PostalAddress}. @return: C{None} """ contact.address = address def getContactItems(self, person): """ Return a C{list} of the L{PostalAddress} items associated with the given person. @type person: L{Person} """ return person.store.query(PostalAddress, PostalAddress.person == person) def getReadOnlyView(self, contact): """ Return a L{SimpleReadOnlyView} for the given L{PostalAddress}. """ return SimpleReadOnlyView(PostalAddress.address, contact) class Notes(item.Item): typeName = 'mantissa_organizer_addressbook_notes' notes = attributes.text(allowNone=False) person = attributes.reference(allowNone=False) class NotesContactType(BaseContactType): """ Contact type plugin which allows a person to be annotated with a free-form textual note. """ implements(IContactType) allowMultipleContactItems = False def getParameters(self, notes): """ Return a C{list} of one L{LiveForm} parameter for editing a L{Notes}. @type notes: L{Notes} or C{NoneType} @param notes: If not C{None}, an existing contact item from which to get the parameter's default value. @rtype: C{list} """ defaultNotes = u'' if notes is not None: defaultNotes = notes.notes return [ liveform.Parameter('notes', liveform.TEXTAREA_INPUT, unicode, 'Notes', default=defaultNotes)] def descriptiveIdentifier(self): """ Return 'Notes' """ return u'Notes' def createContactItem(self, person, notes): """ Create a new L{Notes} associated with the given person based on the given string. @type person: L{Person} @param person: The person with whom to associate the new L{Notes}. @type notes: C{unicode} @param notes: The value to use for the I{notes} attribute of the newly created L{Notes}. If C{''}, no L{Notes} will be created. @rtype: L{Notes} or C{NoneType} """ if notes: return Notes( store=person.store, person=person, notes=notes) def editContactItem(self, contact, notes): """ Set the I{notes} attribute of C{contact} to the value of the C{notes} parameter. @type contact: L{Notes} @param contact: The existing contact item to modify. @type notes: C{unicode} @param notes: The new value to use for the I{notes} attribute of the L{Notes}. @return: C{None} """ contact.notes = notes def getContactItems(self, person): """ Return a C{list} of the L{Notes} items associated with the given person. If none exist, create one, wrap it in a list and return it. @type person: L{Person} """ notes = list(person.store.query(Notes, Notes.person == person)) if not notes: return [Notes(store=person.store, person=person, notes=u'')] return notes def getReadOnlyView(self, contact): """ Return a L{SimpleReadOnlyView} for the given L{Notes}. """ return SimpleReadOnlyView(Notes.notes, contact) class AddPerson(item.Item): implements(ixmantissa.INavigableElement) typeName = 'mantissa_add_person' schemaVersion = 2 powerupInterfaces = (ixmantissa.INavigableElement,) organizer = dependsOn(Organizer) def getTabs(self): return [] def addPerson1to2(old): ap = old.upgradeVersion(old.typeName, 1, 2) ap.organizer = old.store.findOrCreate(Organizer) return ap registerUpgrader(addPerson1to2, AddPerson.typeName, 1, 2) class AddPersonFragment(athena.LiveFragment): """ View class for L{AddPerson}, presenting a user interface for creating a new L{Person}. @ivar organizer: The L{Organizer} instance which will be used to add the person. """ docFactory = ThemedDocumentFactory('add-person', 'store') jsClass = u'Mantissa.People.AddPerson' def __init__(self, organizer): athena.LiveFragment.__init__(self) self.organizer = organizer self.store = organizer.store def head(self): """ Supply no content to the head area of the page. """ return None _baseParameters = [ liveform.Parameter('nickname', liveform.TEXT_INPUT, _normalizeWhitespace, 'Name')] def render_addPersonForm(self, ctx, data): """ Create and return a L{liveform.LiveForm} for creating a new L{Person}. """ addPersonForm = liveform.LiveForm( self.addPerson, self._baseParameters, description='Add Person') addPersonForm.compact() addPersonForm.jsClass = u'Mantissa.People.AddPersonForm' addPersonForm.setFragmentParent(self) return addPersonForm def addPerson(self, nickname): """ Create a new L{Person} with the given C{nickname}. @type nickname: C{unicode} @param nickname: The value for the I{name} attribute of the created L{Person}. @raise L{liveform.InputError}: When some aspect of person creation raises a L{ValueError}. """ try: self.organizer.createPerson(nickname) except ValueError, e: raise liveform.InputError(unicode(e)) expose(addPerson) class ImportPeopleWidget(athena.LiveElement): """ Widget that implements importing people to an L{Organizer}. @ivar organizer: the L{Organizer} to use """ docFactory = ThemedDocumentFactory('import-people', 'store') jsClass = u'Mantissa.People.ImportPeopleWidget' def __init__(self, organizer): athena.LiveElement.__init__(self) self.organizer = organizer self.store = organizer.store def _parseAddresses(addresses): """ Extract name/address pairs from an RFC 2822 style address list. For addresses without a display name, the name defaults to the local-part for the purpose of importing. @type addresses: unicode @return: a list of C{(name, email)} tuples """ def coerce((name, email)): if len(email): if not len(name): name = email.split(u'@', 1)[0] # lame return (name, email) coerced = map(coerce, getaddresses([addresses])) return [r for r in coerced if r is not None] _parseAddresses = staticmethod(_parseAddresses) def importPeopleForm(self, request, tag): """ Create and return a L{liveform.LiveForm} for adding new L{Person}s. """ form = liveform.LiveForm( self.importAddresses, [liveform.Parameter('addresses', liveform.TEXTAREA_INPUT, self._parseAddresses, 'Email Addresses')], description='Import People') form.jsClass = u'Mantissa.People.ImportPeopleForm' form.compact() form.setFragmentParent(self) return form importPeopleForm = renderer(importPeopleForm) def importAddresses(self, addresses): """ Create new L{Person}s for the given names and email addresses. Names and emails that already exist are ignored. @param addresses: a sequence of C{(name, email)} tuples (as returned from L{_parseAddresses}) @return: the names of people actually imported """ results = [] for (name, address) in addresses: def txn(): # Skip names and addresses that already exist. if self.organizer.personByEmailAddress(address) is not None: return # XXX: Needs a better existence check. if self.store.query(Person, Person.name == name).count(): return try: person = self.organizer.createPerson(name) self.organizer.createContactItem( EmailContactType(self.store), person, dict(email=address)) except ValueError, e: # XXX: Granularity required; see #711 and #2435 raise liveform.ConfigurationError(u'%r' % (e,)) return person results.append(self.store.transact(txn)) return [p.name for p in results if p is not None] class PersonExtractFragment(TabularDataView): def render_navigation(self, ctx, data): return inevow.IQ( webtheme.getLoader('person-extracts')).onePattern('navigation') class ExtractWrapperColumnView(ColumnViewBase): def stanFromValue(self, idx, item, value): return inevow.IRenderer(item.extract) class MugshotUploadForm(rend.Page): """ Resource which presents some UI for associating a new mugshot with L{person}. @ivar person: The person whose mugshot is going to be changed. @type person: L{Person} @ivar cbGotImage: Function to call after a successful upload. It will be passed the C{unicode} content-type of the uploaded image and a file containing the uploaded image. """ docFactory = ThemedDocumentFactory('mugshot-upload-form', 'store') def __init__(self, person, cbGotMugshot): rend.Page.__init__(self) self.person = person self.organizer = person.organizer self.store = person.store self.cbGotMugshot = cbGotMugshot def renderHTTP(self, ctx): """ Extract the data from the I{uploaddata} field of the request and pass it to our callback. """ req = inevow.IRequest(ctx) if req.method == 'POST': udata = req.fields['uploaddata'] self.cbGotMugshot(udata.type.decode('ascii'), udata.file) return rend.Page.renderHTTP(self, ctx) def render_smallerMugshotURL(self, ctx, data): """ Render the URL of a smaller version of L{person}'s mugshot. """ return self.organizer.linkToPerson(self.person) + '/mugshot/smaller' class Mugshot(item.Item): """ An image that is associated with a person """ schemaVersion = 3 type = attributes.text(doc=""" Content-type of image data """, allowNone=False) body = attributes.path(doc=""" Path to image data """, allowNone=False) # at the moment we require an upgrader to change the size of either of the # mugshot thumbnails. we might save ourselves some effort by generating # scaled versions on demand, and caching them. smallerBody = attributes.path(doc=""" Path to smaller version of image data """, allowNone=False) person = attributes.reference(doc=""" L{Person} this mugshot is of """, allowNone=False) size = 120 smallerSize = 60 def fromFile(cls, person, inputFile, format): """ Create a L{Mugshot} item for C{person} out of the image data in C{inputFile}, or update C{person}'s existing L{Mugshot} item to reflect the new images. @param inputFile: An image of a person. @type inputFile: C{file} @param person: The person this mugshot is to be associated with. @type person: L{Person} @param format: The format of the data in C{inputFile}. @type format: C{unicode} (e.g. I{jpeg}) @rtype: L{Mugshot} """ body = cls.makeThumbnail(inputFile, person, format, smaller=False) inputFile.seek(0) smallerBody = cls.makeThumbnail( inputFile, person, format, smaller=True) ctype = u'image/' + format self = person.store.findUnique( cls, cls.person == person, default=None) if self is None: self = cls(store=person.store, person=person, type=ctype, body=body, smallerBody=smallerBody) else: self.body = body self.smallerBody = smallerBody self.type = ctype return self fromFile = classmethod(fromFile) def makeThumbnail(cls, inputFile, person, format, smaller): """ Make a thumbnail of a mugshot image and store it on disk. @param inputFile: The image to thumbnail. @type inputFile: C{file} @param person: The person this mugshot thumbnail is associated with. @type person: L{Person} @param format: The format of the data in C{inputFile}. @type format: C{str} (e.g. I{jpeg}) @param smaller: Thumbnails are available in two sizes. if C{smaller} is C{True}, then the thumbnail will be in the smaller of the two sizes. @type smaller: C{bool} @return: path to the thumbnail. @rtype: L{twisted.python.filepath.FilePath} """ dirsegs = ['mugshots', str(person.storeID)] if smaller: dirsegs.insert(1, 'smaller') size = cls.smallerSize else: size = cls.size atomicOutputFile = person.store.newFile(*dirsegs) makeThumbnail(inputFile, atomicOutputFile, size, format) atomicOutputFile.close() return atomicOutputFile.finalpath makeThumbnail = classmethod(makeThumbnail) def placeholderForPerson(cls, person): """ Make an unstored, placeholder L{Mugshot} instance for the given person. @param person: A person without a L{Mugshot}. @type person: L{Person} @rtype: L{Mugshot} """ imageDir = FilePath(__file__).parent().child( 'static').child('images') return cls( type=u'image/png', body=imageDir.child('mugshot-placeholder.png'), smallerBody=imageDir.child( 'mugshot-placeholder-smaller.png'), person=person) placeholderForPerson = classmethod(placeholderForPerson) def mugshot1to2(old): """ Upgrader for L{Mugshot} from version 1 to version 2, which sets the C{smallerBody} attribute to the path of a smaller mugshot image. """ smallerBody = Mugshot.makeThumbnail(old.body.open(), old.person, old.type.split('/')[1], smaller=True) return old.upgradeVersion(Mugshot.typeName, 1, 2, person=old.person, type=old.type, body=old.body, smallerBody=smallerBody) registerUpgrader(mugshot1to2, Mugshot.typeName, 1, 2) item.declareLegacyItem( Mugshot.typeName, 2, dict(person=attributes.reference(), type=attributes.text(), body=attributes.path(), smallerBody=attributes.path())) def mugshot2to3(old): """ Upgrader for L{Mugshot} from version 2 to version 3, which re-thumbnails the mugshot to take into account the new value of L{Mugshot.smallerSize}. """ new = old.upgradeVersion(Mugshot.typeName, 2, 3, person=old.person, type=old.type, body=old.body, smallerBody=old.smallerBody) new.smallerBody = new.makeThumbnail( new.body.open(), new.person, new.type[len('image/'):], smaller=True) return new registerUpgrader(mugshot2to3, Mugshot.typeName, 2, 3) class MugshotResource(rend.Page): """ Web accessible resource that serves Mugshot images. Serves a smaller mugshot if the final path segment is "smaller" """ smaller = False def __init__(self, mugshot): """ @param mugshot: L{Mugshot} """ self.mugshot = mugshot rend.Page.__init__(self) def locateChild(self, ctx, segments): if segments == ('smaller',): self.smaller = True return (self, ()) return rend.NotFound def renderHTTP(self, ctx): if self.smaller: path = self.mugshot.smallerBody else: path = self.mugshot.body return static.File(path.path, str(self.mugshot.type)) def getPersonURL(person): """ Return the address the view for this Person is available at. """ return person.organizer.linkToPerson(person) class PersonDetailFragment(athena.LiveFragment, rend.ChildLookupMixin): """ Renderer for detailed information about a L{Person}. """ fragmentName = 'person-detail' live = 'athena' def __init__(self, person): athena.LiveFragment.__init__(self, person) self.person = person def head(self): return None def _gotMugshotFile(self, ctype, infile): (majortype, minortype) = ctype.split('/') if majortype == 'image': Mugshot.fromFile(self.person, infile, minortype) def child_mugshotUploadForm(self, ctx): """ Return a L{MugshotUploadForm}, which will render some UI for associating a new mugshot with this person. """ return MugshotUploadForm(self.person, self._gotMugshotFile) def child_mugshot(self, ctx): """ Return a L{MugshotResource} displaying this L{Person}'s mugshot image. """ return MugshotResource(self.person.getMugshot()) components.registerAdapter(PersonDetailFragment, Person, ixmantissa.INavigableFragment) class PersonFragment(rend.Fragment): def __init__(self, person, contactMethod=None): rend.Fragment.__init__(self, person, webtheme.getLoader('person-fragment')) self.person = person self.contactMethod = contactMethod def render_person(self, ctx, data): detailURL = self.person.organizer.linkToPerson(self.person) mugshot = self.person.store.findUnique(Mugshot, Mugshot.person == self.person, default=None) if mugshot is None: mugshotURL = '/Mantissa/images/mugshot-placeholder-smaller.png' else: mugshotURL = detailURL + '/mugshot/smaller' name = self.person.getDisplayName() return dictFillSlots(ctx.tag, {'name': name, 'detail-url': detailURL, 'contact-method': self.contactMethod or name, 'mugshot-url': mugshotURL}) PK�����9F)c�������xmantissa/prefs.py# -*- test-case-name: xmantissa.test.historic -*- import pytz from zope.interface import implements from twisted.python.components import registerAdapter from nevow import athena, tags, loaders, inevow from nevow.taglibrary import tabbedPane from nevow.page import renderer from axiom.item import Item from axiom import attributes, upgrade from xmantissa.webtheme import getLoader from xmantissa.liveform import ChoiceParameter, LiveForm from xmantissa import webnav, ixmantissa from xmantissa.webgestalt import AuthenticationApplication class PreferenceCollectionMixin: """ Convenience mixin for L{xmantissa.ixmantissa.IPreferenceCollection} implementors. Provides only the C{getPreferences} method. """ def getPreferences(self): # this is basically a hack so that PreferenceAggregator can # continue work in a similar way d = {} for param in self.getPreferenceParameters() or (): d[param.name] = getattr(self, param.name) return d class DefaultPreferenceCollection(Item, PreferenceCollectionMixin): """ Badly named L{xmantissa.ixmantissa.IPreferenceCollection} which encapsulates basic preferences that are useful to Mantissa in various places, and probably to Mantissa-based applications as well. """ implements(ixmantissa.IPreferenceCollection) typeName = 'mantissa_default_preference_collection' schemaVersion = 2 installedOn = attributes.reference() itemsPerPage = attributes.integer(default=10) timezone = attributes.text(default=u'US/Eastern') powerupInterfaces = (ixmantissa.IPreferenceCollection,) def getPreferenceParameters(self): return (ChoiceParameter( 'timezone', list((c, unicode(c, 'ascii'), c == self.timezone) for c in pytz.common_timezones), 'Timezone'), ChoiceParameter( 'itemsPerPage', list((c, c, c == self.itemsPerPage) for c in (10, 20, 30)), 'Items Per Page')) def getSections(self): authapp = self.store.findUnique(AuthenticationApplication, default=None) if authapp is None: return None return (authapp,) def getTabs(self): return (webnav.Tab('General', self.storeID, 1.0, authoritative=True),) upgrade.registerAttributeCopyingUpgrader(DefaultPreferenceCollection, 1, 2) class PreferenceCollectionLiveForm(LiveForm): """ L{xmantissa.liveform.LiveForm} subclass which switches the docfactory, the jsClass, and overrides the submit button renderer. """ jsClass = u'Mantissa.Preferences.PrefCollectionLiveForm' def __init__(self, *a, **k): super(PreferenceCollectionLiveForm, self).__init__(*a, **k) self.docFactory = getLoader('liveform-compact') def submitbutton(self, request, tag): return tags.input(type='submit', name='__submit__', value='Save') renderer(submitbutton) class PreferenceCollectionFragment(athena.LiveElement): """ L{inevow.IRenderer} adapter for L{xmantissa.ixmantissa.IPreferenceCollection}. """ docFactory = loaders.stan(tags.directive('fragments')) liveFormClass = PreferenceCollectionLiveForm def __init__(self, collection): super(PreferenceCollectionFragment, self).__init__() self.collection = collection def fragments(self, req, tag): """ Render our preference collection, any child preference collections we discover by looking at self.tab.children, and any fragments returned by its C{getSections} method. Subtabs and C{getSections} fragments are rendered as fieldsets inside the parent preference collection's tab. """ f = self._collectionToLiveform() if f is not None: yield tags.fieldset[tags.legend[self.tab.name], f] for t in self.tab.children: f = inevow.IRenderer( self.collection.store.getItemByID(t.storeID)) f.tab = t if hasattr(f, 'setFragmentParent'): f.setFragmentParent(self) yield f for f in self.collection.getSections() or (): f = ixmantissa.INavigableFragment(f) f.setFragmentParent(self) f.docFactory = getLoader(f.fragmentName) yield tags.fieldset[tags.legend[f.title], f] renderer(fragments) def _collectionToLiveform(self): params = self.collection.getPreferenceParameters() if not params: return None f = self.liveFormClass( lambda **k: self._savePrefs(params, k), params, description=self.tab.name) f.setFragmentParent(self) return f def _savePrefs(self, params, values): for (k, v) in values.iteritems(): setattr(self.collection, k, v) return self._collectionToLiveform() # IRenderer(IPreferenceCollection) # -> PreferenceCollectionFragment, unless an adapter is registered for a # specific IPreferenceCollection implementor registerAdapter(PreferenceCollectionFragment, ixmantissa.IPreferenceCollection, inevow.IRenderer) class PreferenceAggregator(Item): """ L{xmantissa.ixmantissa.IPreferenceAggregator} implementor, which provides access to the values of preferences by name """ implements(ixmantissa.IPreferenceAggregator) schemaVersion = 1 typeName = 'preference_aggregator' _collections = attributes.inmemory() installedOn = attributes.reference() powerupInterfaces = (ixmantissa.IPreferenceAggregator,) def activate(self): self._collections = None # IPreferenceAggregator def getPreferenceCollections(self): if self._collections is None: self._collections = list(self.store.powerupsFor(ixmantissa.IPreferenceCollection)) return self._collections def getPreferenceValue(self, key): for collection in self.getPreferenceCollections(): for (_key, value) in collection.getPreferences().iteritems(): if _key == key: return value class PreferenceEditor(athena.LiveElement): """ L{xmantissa.ixmantissa.INavigableFragment} adapter for L{xmantissa.prefs.PreferenceAggregator}. Responsible for rendering all installed L{xmantissa.ixmantissa.IPreferenceCollection}s """ implements(ixmantissa.INavigableFragment) title = 'Settings' fragmentName = 'preference-editor' def __init__(self, aggregator): self.aggregator = aggregator super(PreferenceEditor, self).__init__() def tabbedPane(self, req, tag): """ Render a tabbed pane tab for each top-level L{xmantissa.ixmantissa.IPreferenceCollection} tab """ navigation = webnav.getTabs(self.aggregator.getPreferenceCollections()) pages = list() for tab in navigation: f = inevow.IRenderer( self.aggregator.store.getItemByID(tab.storeID)) f.tab = tab if hasattr(f, 'setFragmentParent'): f.setFragmentParent(self) pages.append((tab.name, f)) f = tabbedPane.TabbedPaneFragment(pages, name='preference-editor') f.setFragmentParent(self) return f renderer(tabbedPane) def head(self): return tabbedPane.tabbedPaneGlue.inlineCSS registerAdapter(PreferenceEditor, PreferenceAggregator, ixmantissa.INavigableFragment) PK�����9Fa0�������xmantissa/product.pyfrom itertools import chain from twisted.python.reflect import namedAny, qual from twisted.python.components import registerAdapter from axiom.item import Item from axiom.attributes import textlist, integer, boolean from axiom.dependency import installOn, uninstallFrom, installedRequirements from nevow import athena, tags from nevow.page import renderer from xmantissa.suspension import unsuspendTabProviders from xmantissa.ixmantissa import INavigableElement, INavigableFragment from xmantissa import liveform from xmantissa.offering import getInstalledOfferings from xmantissa.webnav import Tab from zope.interface import implements class Product(Item): """ I represent a collection of powerups to install on a user store. When a user is to be endowed with the functionality described here, an Installation is created in its store based on me. """ types = textlist() def installProductOn(self, userstore): """ Creates an Installation in this user store for our collection of powerups, and then install those powerups on the user's store. """ def install(): i = Installation(store=userstore) i.types = self.types i.install() userstore.transact(install) def removeProductFrom(self, userstore): """ Uninstall all the powerups this product references and remove the Installation item from the user's store. Doesn't remove the actual powerups currently, but /should/ reactivate them if this product is reinstalled. """ def uninstall(): #this is probably highly insufficient, but i don't know the #requirements i = userstore.findFirst(Installation, Installation.types == self.types) i.uninstall() i.deleteFromStore() userstore.transact(uninstall) def installOrResume(self, userstore): """ Install this product on a user store. If this product has been installed on the user store already and the installation is suspended, it will be resumed. If it exists and is not suspended, an error will be raised. """ for i in userstore.query(Installation, Installation.types == self.types): if i.suspended: unsuspendTabProviders(i) return else: raise RuntimeError("installOrResume called for an" " installation that isn't suspended") else: self.installProductOn(userstore) class Installation(Item): """ I represent a collection of functionality installed on a user store. I reference a collection of powerups, probably designated by a Product. """ types = textlist() _items = textlist() suspended = boolean(default=False) def items(self): """ Loads the items this Installation refers to. """ for id in self._items: yield self.store.getItemByID(int(id)) items = property(items) def allPowerups(self): return set(chain(self.items, *[installedRequirements(self.store, i) for i in self.items])) allPowerups = property(allPowerups) def install(self): """ Called when installed on the user store. Installs my powerups. """ items = [] for typeName in self.types: it = self.store.findOrCreate(namedAny(typeName)) installOn(it, self.store) items.append(str(it.storeID).decode('ascii')) self._items = items def uninstall(self): """ Called when uninstalled from the user store. Uninstalls all my powerups. """ for item in self.items: uninstallFrom(item, self.store) self._items = [] class ProductConfiguration(Item): implements(INavigableElement) attribute = integer(doc="It is an attribute") powerupInterfaces = (INavigableElement,) def getTabs(self): return [Tab('Admin', self.storeID, 0.5, [Tab('Products', self.storeID, 0.6)], authoritative=False)] def createProduct(self, powerups): """ Create a new L{Product} instance which confers the given powerups. @type powerups: C{list} of powerup item types @rtype: L{Product} @return: The new product instance. """ types = [qual(powerup).decode('ascii') for powerup in powerups] for p in self.store.parent.query(Product): for t in types: if t in p.types: raise ValueError("%s is already included in a Product" % (t,)) return Product(store=self.store.parent, types=types) class ProductFragment(athena.LiveElement): fragmentName = 'product-configuration' live = 'athena' def __init__(self, configger): athena.LiveElement.__init__(self) self.original = configger def head(self): #XXX put this in its own CSS file? return tags.style(type='text/css')[''' input[name=linktext], input[name=subject], textarea[name=blurb] { width: 40em } '''] def getInstallablePowerups(self): for installedOffering in getInstalledOfferings(self.original.store.parent).itervalues(): for p in installedOffering.installablePowerups: yield p def coerceProduct(self, **kw): """ Create a product and return a status string which should be part of a template. @param **kw: Fully qualified Python names for powerup types to associate with the created product. """ self.original.createProduct(filter(None, kw.values())) return u'Created.' def makePowerupCoercer(self, powerup): def powerupCoercer(selectedPowerup): if selectedPowerup: return powerup else: return None return powerupCoercer def makePowerupSelector(self, desc): return liveform.Parameter('selectedPowerup', liveform.CHECKBOX_INPUT, bool, desc) def powerupConfigurationParameter(self, (name, desc, p)): return liveform.Parameter( name, liveform.FORM_INPUT, liveform.LiveForm(self.makePowerupCoercer(p), [self.makePowerupSelector(desc)], name)) def productConfigurationForm(self, request, tag): productList = liveform.LiveForm(self.coerceProduct, [self.powerupConfigurationParameter(pi) for pi in self.getInstallablePowerups()], u"Installable Powerups") productList.setFragmentParent(self) return productList renderer(productConfigurationForm) def configuredProducts(self, request, tag): for prod in self.original.store.parent.query(Product): yield repr(prod.types) renderer(configuredProducts) registerAdapter(ProductFragment, ProductConfiguration, INavigableFragment) PK�����9FAK���������xmantissa/publicresource.py """ This module is only in place for supporting legacy names. It will be removed soon. """ from xmantissa.publicweb import * PK�����9FvO�������xmantissa/publicweb.py# -*- test-case-name: xmantissa.test.test_publicweb -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ This module contains code for the publicly-visible areas of a Mantissa server's web interface. """ from warnings import warn from zope.interface import implements from twisted.internet import defer from nevow.inevow import IRequest, IResource from nevow import rend, tags, inevow from nevow.url import URL from axiom.iaxiom import IPowerupIndirector from axiom import item, attributes, upgrade, userbase from axiom.dependency import dependsOn, requiresFromSite from xmantissa import ixmantissa, website, offering from xmantissa._webutil import (MantissaViewHelper, SiteRootMixin, WebViewerHelper) from xmantissa.webtheme import (ThemedDocumentFactory, getInstalledThemes, SiteTemplateResolver) from xmantissa.ixmantissa import ( IStaticShellContent, ISiteRootPlugin, IMantissaSite, IWebViewer, INavigableFragment) from xmantissa.webnav import startMenu, settingsLink, applicationNavigation from xmantissa.websharing import UserIndexPage, SharingIndex, getDefaultShareID from xmantissa.sharing import getEveryoneRole, NoSuchShare def getLoader(*a, **kw): """ Deprecated. Don't use this. """ warn("xmantissa.publicweb.getLoader is deprecated, use " "PrivateApplication.getDocFactory or SiteTemplateResolver." "getDocFactory.", category=DeprecationWarning, stacklevel=2) from xmantissa.webtheme import getLoader return getLoader(*a, **kw) def renderShortUsername(ctx, username): """ Render a potentially shortened version of the user's login identifier, depending on how the user is viewing it. For example, if bob@example.com is viewing http://example.com/private/, then render 'bob'. If bob instead signed up with only his email address (bob@hotmail.com), and is viewing a page at example.com, then render the full address, 'bob@hotmail.com'. @param ctx: a L{WovenContext} which has remembered IRequest. @param username: a string of the form localpart@domain. @return: a L{Tag}, the given context's tag, with the appropriate username appended to it. """ if username is None: return '' req = inevow.IRequest(ctx) localpart, domain = username.split('@') host = req.getHeader('Host').split(':')[0] if host == domain or host.endswith("." + domain): username = localpart return ctx.tag[username] class PublicWeb(item.Item, website.PrefixURLMixin): """ Fixture for site-wide public-facing content. I implement ISiteRootPlugin and use PrefixURLMixin; see the documentation for each of those for a detailed explanation of my usage. I wrap a L{websharing.SharingIndex} around an app store. I am installed in an app store when it is created. """ implements(ISiteRootPlugin, ixmantissa.ISessionlessSiteRootPlugin) typeName = 'mantissa_public_web' schemaVersion = 3 prefixURL = attributes.text( doc=""" The prefix of the URL where objects represented by this fixture will appear. For the front page this is u'', for other pages it is their respective URLs. """, allowNone=False) application = attributes.reference( doc=""" A L{SubStore} for an application store. """, allowNone=False) installedOn = attributes.reference( doc=""" """) sessioned = attributes.boolean( doc=""" Will this resource be provided to clients with a session? Defaults to False. """, default=False) sessionless = attributes.boolean( doc=""" Will this resource be provided without a session to clients without a session? Defaults to False. """, default=False) def createResource(self): """ When invoked by L{PrefixURLMixin}, return a L{websharing.SharingIndex} for my application. """ pp = ixmantissa.IPublicPage(self.application, None) if pp is not None: warn( "Use the sharing system to provide public pages, not IPublicPage", category=DeprecationWarning, stacklevel=2) return pp.getResource() return SharingIndex(self.application.open()) def upgradePublicWeb1To2(oldWeb): newWeb = oldWeb.upgradeVersion( 'mantissa_public_web', 1, 2, prefixURL=oldWeb.prefixURL, application=oldWeb.application, installedOn=oldWeb.installedOn) newWeb.installedOn.powerUp(newWeb, ixmantissa.ICustomizablePublicPage) return newWeb upgrade.registerUpgrader(upgradePublicWeb1To2, 'mantissa_public_web', 1, 2) def upgradePublicWeb2To3(oldWeb): newWeb = oldWeb.upgradeVersion( 'mantissa_public_web', 2, 3, prefixURL=oldWeb.prefixURL, application=oldWeb.application, installedOn=oldWeb.installedOn, # There was only one PublicWeb before, and it definitely # wanted to be sessioned. sessioned=True) newWeb.installedOn.powerDown(newWeb, ixmantissa.ICustomizablePublicPage) other = newWeb.installedOn newWeb.installedOn = None newWeb.installOn(other) return newWeb upgrade.registerUpgrader(upgradePublicWeb2To3, 'mantissa_public_web', 2, 3) class _AnonymousWebViewer(WebViewerHelper): """ An implementation of L{IWebViewer} for anonymous users. @ivar _siteStore: A site store that contains an L{AnonymousSite} and L{SiteConfiguration}. @ivar _getDocFactory: the L{SiteTemplateResolver.getDocFactory} method that will resolve themes for my site store. """ implements(IWebViewer) def __init__(self, siteStore): """ Create an L{_AnonymousWebViewer} for browsing a given site store. """ WebViewerHelper.__init__( self, SiteTemplateResolver(siteStore).getDocFactory, lambda : getInstalledThemes(siteStore)) self._siteStore = siteStore # IWebViewer def roleIn(self, userStore): """ Return only the 'everyone' role in the given user- or app-store, since the user represented by this object is anonymous. """ return getEveryoneRole(userStore) # Complete WebViewerHelper implementation def _wrapNavFrag(self, frag, useAthena): """ Wrap the given L{INavigableFragment} in the appropriate type of L{_PublicPageMixin}. """ if useAthena: return PublicAthenaLivePage(self._siteStore, frag) else: return PublicPage(None, self._siteStore, frag, None, None) class _CustomizingResource(object): """ _CustomizingResource is a wrapping resource used to implement CustomizedPublicPage. There is an implementation assumption here, which is that the top _CustomizingResource is always at "/", and locateChild will always be invoked at least once. If this doesn't hold, this render method might be invoked on the top level _CustomizingResource, which would cause it to be rendered without customization. If you're going to use this class directly for some reason, please keep this in mind. """ implements(inevow.IResource) def __repr__(self): return '<Customizing Resource %r: %r>' % (self.forWho, self.currentResource) def __init__(self, topResource, forWho): """ Create a _CustomizingResource. @param topResource: an L{inevow.IResource} provider, who may also provide L{ixmantissa.ICustomizable} if it wishes to be customized. @param forWho: the external ID of the currently logged-in user. @type forWho: unicode """ self.currentResource = topResource self.forWho = forWho def locateChild(self, ctx, segments): """ Return a Deferred which will fire with the customized version of the resource being located. """ D = defer.maybeDeferred( self.currentResource.locateChild, ctx, segments) def finishLocating((nextRes, nextPath)): custom = ixmantissa.ICustomizable(nextRes, None) if custom is not None: return (custom.customizeFor(self.forWho), nextPath) self.currentResource = nextRes if nextRes is None: return (nextRes, nextPath) return (_CustomizingResource(nextRes, self.forWho), nextPath) return D.addCallback(finishLocating) def renderHTTP(self, ctx): """ Render the resource I was provided at construction time. """ if self.currentResource is None: return rend.FourOhFour() return self.currentResource # nevow will automatically adapt to # IResource and call rendering methods. class CustomizedPublicPage(item.Item): """ L{CustomizedPublicPage} is what allows logged-in users to see dynamic resources present in the site store. Although static resources (under http://your-domain.example.com/static) are available to everyone, any user who should be able to see content such as http://yoursite/users/some-user/ when they are logged in must have this installed. """ implements(ISiteRootPlugin) typeName = 'mantissa_public_customized' schemaVersion = 2 installedOn = attributes.reference( doc=""" The Avatar for which this item will attempt to retrieve a customized page. """) powerupInterfaces = [(ISiteRootPlugin, -257)] publicSiteRoot = requiresFromSite(IMantissaSite, lambda ignored: None) def produceResource(self, request, segments, webViewer): """ Produce a resource that traverses site-wide content, passing down the given webViewer. This delegates to the site store's L{IMantissaSite} adapter, to avoid a conflict with the L{ISiteRootPlugin} interface. This method will typically be given an L{_AuthenticatedWebViewer}, which can build an appropriate resource for an authenticated shell page, whereas the site store's L{IWebViewer} adapter would show an anonymous page. The result of this method will be a L{_CustomizingResource}, to provide support for resources which may provide L{ICustomizable}. Note that Mantissa itself no longer implements L{ICustomizable} anywhere, though. All application code should phase out inspecting the string passed to ICustomizable in favor of getting more structured information from the L{IWebViewer}. However, it has not been deprecated yet because the interface which allows application code to easily access the L{IWebViewer} from view code has not yet been developed; it is forthcoming. See ticket #2707 for progress on this. """ mantissaSite = self.publicSiteRoot if mantissaSite is not None: for resource, domain in userbase.getAccountNames(self.store): username = '%s@%s' % (resource, domain) break else: username = None bottomResource, newSegments = mantissaSite.siteProduceResource( request, segments, webViewer) return (_CustomizingResource(bottomResource, username), newSegments) return None def customizedPublicPage1To2(oldPage): newPage = oldPage.upgradeVersion( 'mantissa_public_customized', 1, 2, installedOn=oldPage.installedOn) newPage.installedOn.powerDown(newPage, ISiteRootPlugin) newPage.installedOn.powerUp(newPage, ISiteRootPlugin, -257) return newPage upgrade.registerUpgrader(customizedPublicPage1To2, 'mantissa_public_customized', 1, 2) class _PublicPageMixin(MantissaViewHelper): """ Mixin for use by C{Page} or C{LivePage} subclasses that are visible to unauthenticated clients. @ivar store: The site store. """ fragment = None username = None def _getViewerPrivateApplication(self): """ Get the L{PrivateApplication} object for the logged-in user who is viewing this resource, as indicated by its C{username} attribute. This is highly problematic because it precludes the possibility of separating the stores of the viewer and the viewee into separate processes, and it is only here until we can get rid of it. The reason it remains is that some application code still imports things which subclass L{PublicAthenaLivePage} and L{PublicPage} and uses them with usernames specified. See ticket #2702 for progress on this goal. However, Mantissa itself will no longer set this class's username attribute to anything other than None, because authenticated users' pages will be generated using L{xmantissa.webapp._AuthenticatedWebViewer}. This method is used only to render content in the shell template, and those classes have a direct reference to the requisite object. @rtype: L{PrivateApplication} """ ls = self.store.findUnique(userbase.LoginSystem) substore = ls.accountByAddress(*self.username.split('@')).avatars.open() from xmantissa.webapp import PrivateApplication return substore.findUnique(PrivateApplication) def render_authenticateLinks(self, ctx, data): """ For unauthenticated users, add login and signup links to the given tag. For authenticated users, remove the given tag from the output. When necessary, the I{signup-link} pattern will be loaded from the tag. Each copy of it will have I{prompt} and I{url} slots filled. The list of copies will be added as children of the tag. """ if self.username is not None: return '' # there is a circular import here which should probably be avoidable, # since we don't actually need signup links on the signup page. on the # other hand, maybe we want to eventually put those there for # consistency. for now, this import is easiest, and although it's a # "friend" API, which I dislike, it doesn't seem to cause any real # problems... -glyph from xmantissa.signup import _getPublicSignupInfo IQ = inevow.IQ(ctx.tag) signupPattern = IQ.patternGenerator('signup-link') signups = [] for (prompt, url) in _getPublicSignupInfo(self.store): signups.append(signupPattern.fillSlots( 'prompt', prompt).fillSlots( 'url', url)) return ctx.tag[signups] def render_startmenu(self, ctx, data): """ For authenticated users, add the start-menu style navigation to the given tag. For unauthenticated users, remove the given tag from the output. @see L{xmantissa.webnav.startMenu} """ if self.username is None: return '' translator = self._getViewerPrivateApplication() pageComponents = translator.getPageComponents() return startMenu(translator, pageComponents.navigation, ctx.tag) def render_settingsLink(self, ctx, data): """ For authenticated users, add the URL of the settings page to the given tag. For unauthenticated users, remove the given tag from the output. """ if self.username is None: return '' translator = self._getViewerPrivateApplication() return settingsLink( translator, translator.getPageComponents().settings, ctx.tag) def render_applicationNavigation(self, ctx, data): """ For authenticated users, add primary application navigation to the given tag. For unauthenticated users, remove the given tag from the output. @see L{xmantissa.webnav.applicationNavigation} """ if self.username is None: return '' translator = self._getViewerPrivateApplication() return applicationNavigation( ctx, translator, translator.getPageComponents().navigation) def render_search(self, ctx, data): """ Render some UI for performing searches, if we know about a search aggregator. """ if self.username is None: return '' translator = self._getViewerPrivateApplication() searchAggregator = translator.getPageComponents().searchAggregator if searchAggregator is None or not searchAggregator.providers(): return '' return ctx.tag.fillSlots( 'form-action', translator.linkTo(searchAggregator.storeID)) def render_username(self, ctx, data): return renderShortUsername(ctx, self.username) def render_logout(self, ctx, data): if self.username is None: return '' return ctx.tag def render_title(self, ctx, data): """ Return the current context tag containing C{self.fragment}'s C{title} attribute, or "Divmod". """ return ctx.tag[getattr(self.fragment, 'title', 'Divmod')] def render_rootURL(self, ctx, data): """ Add the WebSite's root URL as a child of the given tag. """ return ctx.tag[ ixmantissa.ISiteURLGenerator(self.store).rootURL(IRequest(ctx))] def render_header(self, ctx, data): """ Render any required static content in the header, from the C{staticContent} attribute of this page. """ if self.staticContent is None: return ctx.tag header = self.staticContent.getHeader() if header is not None: return ctx.tag[header] else: return ctx.tag def render_footer(self, ctx, data): """ Render any required static content in the footer, from the C{staticContent} attribute of this page. """ if self.staticContent is None: return ctx.tag header = self.staticContent.getFooter() if header is not None: return ctx.tag[header] else: return ctx.tag def render_urchin(self, ctx, data): """ Render the code for recording Google Analytics statistics, if so configured. """ key = website.APIKey.getKeyForAPI(self.store, website.APIKey.URCHIN) if key is None: return '' return ctx.tag.fillSlots('urchin-key', key.apiKey) def render_content(self, ctx, data): """ This renderer, which is used for the visual bulk of the page, provides self.fragment renderer. """ return ctx.tag[self.fragment] def getHeadContent(self, req): """ Retrieve a list of header content from all installed themes on the site store. """ site = ixmantissa.ISiteURLGenerator(self.store) for t in getInstalledThemes(self.store): yield t.head(req, site) def render_head(self, ctx, data): """ This renderer calculates content for the <head> tag by concatenating the values from L{getHeadContent} and the overridden L{head} method. """ req = inevow.IRequest(ctx) more = getattr(self.fragment, 'head', None) if more is not None: fragmentHead = more() else: fragmentHead = None return ctx.tag[filter(None, list(self.getHeadContent(req)) + [fragmentHead])] class PublicPage(_PublicPageMixin, rend.Page): """ PublicPage is a utility superclass for implementing static pages which have theme support and authentication trimmings. """ docFactory = ThemedDocumentFactory('shell', 'templateResolver') def __init__(self, original, store, fragment, staticContent, forUser, templateResolver=None): """ Create a public page. @param original: any object @param store: a site store containing a L{WebSite}. @type store: L{axiom.store.Store}. @param fragment: a L{rend.Fragment} to display in the content area of the page. @param staticContent: some stan, to include in the header of the page. @param forUser: a string, the external ID of a user to customize for. @param templateResolver: a template resolver instance that will return the appropriate doc factory. """ if templateResolver is None: templateResolver = ixmantissa.ITemplateNameResolver(store) super(PublicPage, self).__init__(original) self.store = store self.fragment = fragment self.staticContent = staticContent self.templateResolver = templateResolver if forUser is not None: assert isinstance(forUser, unicode), forUser self.username = forUser class _OfferingsFragment(rend.Fragment): """ This fragment provides the list of installed offerings as a data generator. This is used to display the list of app stores on the default front page. @ivar templateResolver: An L{ITemplateNameResolver} which will be used to load the document factory. """ implements(INavigableFragment) docFactory = ThemedDocumentFactory('front-page', 'templateResolver') def __init__(self, original, templateResolver=None): """ Create an _OfferingsFragment with an item from a site store. @param original: a L{FrontPage} item. @param templateResolver: An L{ITemplateNameResolver} from which to load the document factory. If not specified, the Store of C{original} will be adapted to L{ITemplateNameResolver} and used for this purpose. It is recommended that you pass a value for this parameter. """ if templateResolver is None: templateResolver = ixmantissa.ITemplateNameResolver(original.store) self.templateResolver = templateResolver super(_OfferingsFragment, self).__init__(original) def data_offerings(self, ctx, data): """ Generate a list of installed offerings. @return: a generator of dictionaries mapping 'name' to the name of an offering installed on the store. """ for io in self.original.store.query(offering.InstalledOffering): pp = ixmantissa.IPublicPage(io.application, None) if pp is not None and getattr(pp, 'index', True): warn("Use the sharing system to provide public pages," " not IPublicPage", category=DeprecationWarning, stacklevel=2) yield {'name': io.offeringName} else: s = io.application.open() try: pp = getEveryoneRole(s).getShare(getDefaultShareID(s)) yield {'name': io.offeringName} except NoSuchShare: continue class _PublicFrontPage(object): """ This is the implementation of the default Mantissa front page. It renders a list of offering names, displays the user's name, and lists signup mechanisms. It also provides various top-level URLs. """ implements(IResource) def __init__(self, frontPageItem, webViewer): """ Create a _PublicFrontPage. @param frontPageItem: a L{FrontPage} item, which we use primarily to get at a Store. @param webViewer: an L{IWebViewer} that represents the user viewing this front page. """ self.frontPageItem = frontPageItem self.webViewer = webViewer def locateChild(self, ctx, segments): """ Look up children in the normal manner, but then customize them for the authenticated user if they support the L{ICustomizable} interface. If the user is attempting to access a private URL, redirect them. """ result = self._getAppStoreResource(ctx, segments[0]) if result is not None: child, segments = result, segments[1:] return child, segments if segments[0] == '': result = self.child_(ctx) if result is not None: child, segments = result, segments[1:] return child, segments # If the user is trying to access /private/*, then his session has # expired or he is otherwise not logged in. Redirect him to /login, # preserving the URL segments, rather than giving him an obscure 404. if segments[0] == 'private': u = URL.fromContext(ctx).click('/').child('login') for seg in segments: u = u.child(seg) return u, () return rend.NotFound def _getAppStoreResource(self, ctx, name): """ Customize child lookup such that all installed offerings on the site store that this page is viewing are given an opportunity to display their own page. """ offer = self.frontPageItem.store.findFirst( offering.InstalledOffering, offering.InstalledOffering.offeringName == unicode(name, 'ascii')) if offer is not None: pp = ixmantissa.IPublicPage(offer.application, None) if pp is not None: warn("Use the sharing system to provide public pages," " not IPublicPage", category=DeprecationWarning, stacklevel=2) return pp.getResource() return SharingIndex(offer.application.open(), self.webViewer) return None def child_(self, ctx): """ If the root resource is requested, return the primary application's front page, if a primary application has been chosen. Otherwise return 'self', since this page can render a simple index. """ if self.frontPageItem.defaultApplication is None: return self.webViewer.wrapModel( _OfferingsFragment(self.frontPageItem)) else: return SharingIndex(self.frontPageItem.defaultApplication.open(), self.webViewer).locateChild(ctx, [''])[0] class LoginPage(PublicPage): """ This is the page which presents a 'login' dialog to the user, at "/login". This does not perform the actual login, nevow.guard does that, at the URL /__login__; this resource merely provides the entry field and redirection logic. """ # Try to get SSL if possible. See xmantissa.web.SecuringWrapper and # xmantissa.web.SiteConfiguration.getFactory. This should really be # indicated in some other way. See #2525 -exarkun needsSecure = True def __init__(self, store, segments=(), arguments=None, templateResolver=None): """ Create a login page. @param store: a site store containing a L{WebSite}. @type store: L{axiom.store.Store}. @param segments: a list of strings. For example, if you hit /login/private/stuff, you want to log in to /private/stuff, and the resulting LoginPage will have the segments of ['private', 'stuff'] @param arguments: A dictionary mapping query argument names to lists of values for those arguments (see IRequest.args). @param templateResolver: a template resolver instance that will return the appropriate doc factory. """ if templateResolver is None: templateResolver = ixmantissa.ITemplateNameResolver(store) PublicPage.__init__(self, None, store, templateResolver.getDocFactory('login'), IStaticShellContent(store, None), None, templateResolver) self.segments = segments if arguments is None: arguments = {} self.arguments = arguments def beforeRender(self, ctx): """ Before rendering this page, identify the correct URL for the login to post to, and the error message to display (if any), and fill the 'login action' and 'error' slots in the template accordingly. """ generator = ixmantissa.ISiteURLGenerator(self.store) url = generator.rootURL(IRequest(ctx)) url = url.child('__login__') for seg in self.segments: url = url.child(seg) for queryKey, queryValues in self.arguments.iteritems(): for queryValue in queryValues: url = url.add(queryKey, queryValue) req = inevow.IRequest(ctx) err = req.args.get('login-failure', ('',))[0] if 0 < len(err): error = inevow.IQ( self.fragment).onePattern( 'error').fillSlots('error', err) else: error = '' ctx.fillSlots("login-action", url) ctx.fillSlots("error", error) def locateChild(self, ctx, segments): """ Return a clone of this page that remembers its segments, so that URLs like /login/private/stuff will redirect the user to /private/stuff after login has completed. """ arguments = IRequest(ctx).args return self.__class__( self.store, segments, arguments), () def fromRequest(cls, store, request): """ Return a L{LoginPage} which will present the user with a login prompt. @type store: L{Store} @param store: A I{site} store. @type request: L{nevow.inevow.IRequest} @param request: The HTTP request which encountered a need for authentication. This will be effectively re-issued after login succeeds. @return: A L{LoginPage} and the remaining segments to be processed. """ location = URL.fromRequest(request) segments = location.pathList(unquote=True, copy=False) segments.append(request.postpath[0]) return cls(store, segments, request.args) fromRequest = classmethod(fromRequest) class FrontPage(item.Item, website.PrefixURLMixin): """ I am a factory for the dynamic resource L{_PublicFrontPage}. """ implements(ISiteRootPlugin) typeName = 'mantissa_front_page' schemaVersion = 2 sessioned = True publicViews = attributes.integer( doc=""" The number of times this object has been viewed anonymously. This includes renderings of the front page only. """, default=0) privateViews = attributes.integer( doc=""" The number of times this object has been viewed non-anonymously. This includes renderings of the front page only. """, default=0) prefixURL = attributes.text( doc=""" See L{website.PrefixURLMixin}. """, default=u'', allowNone=False) defaultApplication = attributes.reference( doc=""" An application L{SubStore} whose default shared item should be displayed on the root web resource. If None, the default index of applications will be displayed. """, allowNone=True) def createResourceWith(self, crud): """ Create a L{_PublicFrontPage} resource wrapping this object. """ return _PublicFrontPage(self, crud) item.declareLegacyItem( FrontPage.typeName, 1, dict(publicViews = attributes.integer(), privateViews = attributes.integer(), prefixURL = attributes.text(allowNone=False))) upgrade.registerAttributeCopyingUpgrader(FrontPage, 1, 2) class PublicAthenaLivePage(_PublicPageMixin, website.MantissaLivePage): """ PublicAthenaLivePage is a publicly viewable Athena-enabled page which slots a single fragment into the center of the page. """ docFactory = ThemedDocumentFactory('shell', 'templateResolver') unsupportedBrowserLoader = ThemedDocumentFactory( 'athena-unsupported', 'templateResolver') fragment = None def __init__(self, store, fragment, staticContent=None, forUser=None, templateResolver=None, *a, **kw): """ Create a PublicAthenaLivePage. @param store: a site store containing an L{AnonymousSite} and L{SiteConfiguration}. @type store: L{axiom.store.Store}. @param fragment: The L{INavigableFragment} provider which will be displayed on this page. @param templateResolver: a template resolver instance that will return the appropriate doc factory. This page draws its HTML from the 'shell' template in the active theme. If loaded in a browser that does not support Athena, the page provided by the 'athena-unsupported' template will be displayed instead. """ self.store = store if templateResolver is None: templateResolver = ixmantissa.ITemplateNameResolver(self.store) self.templateResolver = templateResolver super(PublicAthenaLivePage, self).__init__(ixmantissa.ISiteURLGenerator(self.store), *a, **kw) if fragment is not None: self.fragment = fragment # everybody who calls this has a different idea of what 'fragment' # means - let's just be super-lenient for now if getattr(fragment, 'setFragmentParent', False): fragment.setFragmentParent(self) else: fragment.page = self self.staticContent = staticContent self.username = forUser def render_head(self, ctx, data): """ Put liveglue content into the header of this page to activate it, but otherwise delegate to my parent's renderer for <head>. """ ctx.tag[tags.invisible(render=tags.directive('liveglue'))] return _PublicPageMixin.render_head(self, ctx, data) class PublicNavAthenaLivePage(PublicAthenaLivePage): """ DEPRECATED! Use PublicAthenaLivePage. A L{PublicAthenaLivePage} which optionally includes a menubar and navigation if the viewer is authenticated. """ def __init__(self, *a, **kw): PublicAthenaLivePage.__init__(self, *a, **kw) warn( "Use PublicAthenaLivePage instead of PublicNavAthenaLivePage", category=DeprecationWarning, stacklevel=2) class AnonymousSite(item.Item, SiteRootMixin): """ Root IResource implementation for unauthenticated users. This resource allows users to login, reset their passwords, or access content provided by any site root plugins. """ powerupInterfaces = (IResource, IMantissaSite, IWebViewer) implements(*powerupInterfaces + (IPowerupIndirector,)) schemaVersion = 2 loginSystem = dependsOn(userbase.LoginSystem) def rootChild_resetPassword(self, req, webViewer): """ Return a page which will allow the user to re-set their password. """ from xmantissa.signup import PasswordResetResource return PasswordResetResource(self.store) def rootChild_login(self, req, webViewer): """ Return a login page. """ return LoginPage(self.store) def rootChild_users(self, req, webViewer): """ Return a child resource to provide access to items shared by users. @return: a resource whose children will be private pages of individual users. @rtype L{xmantissa.websharing.UserIndexPage} """ return UserIndexPage(self.loginSystem, webViewer) def _getUsername(self): """ Inform L{VirtualHostWrapper} that it's being accessed anonymously. """ return None # IPowerupIndirector def indirect(self, interface): """ Indirect the implementation of L{IWebViewer} to L{_AnonymousWebViewer}. """ if interface == IWebViewer: return _AnonymousWebViewer(self.store) return super(AnonymousSite, self).indirect(interface) AnonymousSite1 = item.declareLegacyItem( 'xmantissa_publicweb_anonymoussite', 1, dict( loginSystem=attributes.reference(), )) def _installV2Powerups(anonymousSite): """ Install the given L{AnonymousSite} for the powerup interfaces it was given in version 2. """ anonymousSite.store.powerUp(anonymousSite, IWebViewer) anonymousSite.store.powerUp(anonymousSite, IMantissaSite) upgrade.registerAttributeCopyingUpgrader(AnonymousSite, 1, 2, _installV2Powerups) PK�����9FGh�������xmantissa/scrolltable.py# -*- test-case-name: xmantissa.test.test_scrolltable -*- """ Scrollable tabular data-display area. This module provides an API for displaying data from a Twisted server in a Nevow Athena front-end. """ import inspect import warnings from zope.interface import implements from twisted.python.components import registerAdapter from twisted.python.reflect import qual from epsilon.extime import Time from nevow.athena import LiveElement, expose from axiom.attributes import timestamp, SQLAttribute, AND from xmantissa.ixmantissa import IWebTranslator, IColumn from xmantissa.error import Unsortable TYPE_FRAGMENT = 'fragment' TYPE_WIDGET = 'widget' class AttributeColumn(object): """ Implement a mapping between Axiom attributes and the scrolltable-based L{IColumn}. """ implements(IColumn) def __init__(self, attribute, attributeID=None): """ Create an L{AttributeColumn} from an Axiom attribute. @param attribute: an axiom L{SQLAttribute} subclass. @param attributeID: an optional client-side identifier for this attribute. Normally this will be this attribute's name; it isn't visible to the user on the client, it's simply the client-side internal identifier. """ self.attribute = attribute if attributeID is None: attributeID = attribute.attrname self.attributeID = attributeID def extractValue(self, model, item): """ Extract a simple value for this column from a given item, suitable for serialization via Athena's client-communication layer. @param model: The scrollable view object requesting the value. Unfortunately due to the long history of this code, this has no clear interface, and might be a L{ScrollingElement}, L{ScrollingFragment}, or L{xmantissa.tdb.TabularDataModel}, depending on which type this L{AttributeColumn} was passed to. @param item: An instance of the class that this L{AttributeColumn}'s L{attribute} was taken from, to retrieve the value from. @return: a value of an attribute of C{item}, of a type dependent upon this L{AttributeColumn}'s L{attribute}. """ return self.attribute.__get__(item) def sortAttribute(self): """ @return: an L{axiom.attributes.Comparable} that can be used to adjust an axiom query to sort the table by this column, or None, if this column cannot be sorted by. """ return self.attribute def getType(self): """ @return: a string to identify the browser-side type of this column to the JavaScript code in Mantissa.ScrollTable.ScrollTable.__init__. @rtype: L{str} """ sortattr = self.sortAttribute() if sortattr is not None: return sortattr.__class__.__name__ def toComparableValue(self, value): """ Convert C{value} into something that can be compared like-for-like with L{sortAttribute}. """ return value registerAdapter(AttributeColumn, SQLAttribute, IColumn) # these objects aren't for view junk - they allow the model # to inform the javascript controller about which columns are # sortable, as well as supporting non-attribute columns class TimestampAttributeColumn(AttributeColumn): """ Timestamps are a special case; we need to get the posix timestamp so we can send the attribute value to javascript. we don't register an adapter for attributes.timestamp because the TDB model uses IColumn.extractValue() to determine the value of the query pivot, and so it needs an extime.Time instance, not a float. """ def extractValue(self, model, item): val = AttributeColumn.extractValue(self, model, item) if val is None: raise AttributeError("%r was None" % (self.attribute,)) return val.asPOSIXTimestamp() def getType(self): return 'timestamp' def toComparableValue(self, value): """ Override L{AttributeColumn}'s implementation to return a L{Time} instance. """ return Time.fromPOSIXTimestamp(value) class UnsortableColumnWrapper(object): """ Wraps an L{AttributeColumn} and makes it unsortable @ivar column: L{AttributeColumn} """ implements(IColumn) def __init__(self, column): self.column = column self.attribute = column.attribute self.attributeID = column.attributeID def extractValue(self, model, item): """ Delegate to the wrapped column's value extraction method. """ return self.column.extractValue(model, item) def sortAttribute(self): """ Prevent sorting on this column by ignoring the wrapped column's sort attribute and always returning C{None}. """ return None def getType(self): """ Return the wrapped column's type. """ return self.column.getType() class UnsortableColumn(AttributeColumn): """ An axiom attribute column which does not allow server-side sorting for policy or performance reasons. """ def __init__(self, *a, **kw): warnings.warn( category=DeprecationWarning, message=( "Use UnsortableColumnWrapper(AttributeColumn(*a, **kw)) instead " "of UnsortableColumn(*a, **kw)."), stacklevel=2) AttributeColumn.__init__(self, *a, **kw) def sortAttribute(pelf): """ UnsortableColumns are not sortable, so this will always return L{None}. See L{AttributeColumn.sortAttribute}. """ return None def getType(self): """ Clobber the inherited implementation to work around the fact that sortAttribute returns None so that a useful value is still returned. """ return self.attribute.__class__.__name__ class _ScrollableBase(object): """ _ScrollableBase is an internal base class holding logic for dealing with lists of L{xmantissa.ixmantissa.IColumn}s, sorting, and link generation. This logic is shared by two quite different implementations of client-server communication about rows: L{InequalityModel}, which uses techniques specific to the performance characteristics of Axiom queries, and L{IndexingModel}, which uses simple indexing logic suitable for sequences. """ currentSortColumn = None def __init__(self, webTranslator, columns, defaultSortColumn, defaultSortAscending): self.webTranslator = webTranslator self.columns = {} self.columnNames = [] for col in columns: # see comment in TimestampAttributeColumn if isinstance(col, timestamp): col = TimestampAttributeColumn(col) else: col = IColumn(col) if defaultSortColumn is None: defaultSortColumn = col.sortAttribute() if (defaultSortColumn is not None and col.sortAttribute() is defaultSortColumn): self.currentSortColumn = col attributeID = unicode(col.attributeID, 'ascii') self.columns[attributeID] = col self.columnNames.append(attributeID) self.isAscending = defaultSortAscending if self.currentSortColumn is None: self._cannotDetermineSort(defaultSortColumn) def _cannotDetermineSort(self, defaultSortColumn): """ This is an internal method designed to be overridden *only* by the classes in this module. It will be called if: * No explicit sort column was specified, and none of the columns specified is sortable, or, * An explicit sort column was specified, but it is not present in the list of columns specified. In other words, this method is called when there is no consistent sort that can be performed based on the caller's input to the constructor. In the default case of the inequality-based model, nothing can be done about this, and an exception is raised. In the old index-based scrolltable, certain implicit sorts will appear to work, so those continue to work for those tables. However, Users are advised to avoid the index-based scrolltable in general, and this subtly broken implicit behavior specifically. @param defaultSortColumn: something adaptable to L{IColumn}, or None, which subclasses may use to accept a sort that is not related to any extant columns in this table. @raise: L{Unsortable}, always. Some subclasses can deal with this case better. """ raise Unsortable('you must provide a sortable column') def resort(self, columnName): """ Re-sort the table. @param columnName: the name of the column to sort by. This is a string because it is passed from the browser. """ csc = self.currentSortColumn newSortColumn = self.columns[columnName] if newSortColumn is None: raise Unsortable('column %r has no sort attribute' % (columnName,)) if csc is newSortColumn: self.isAscending = not self.isAscending else: self.currentSortColumn = newSortColumn self.isAscending = True return self.isAscending expose(resort) def linkToItem(self, item): """ Return a URL that the row for C{item} should link to, by asking the L{xmantissa.ixmantissa.IWebTranslator} in C{self.store} @return: C{unicode} URL """ return unicode(self.webTranslator.toWebID(item), 'ascii') def itemFromLink(self, link): """ Inverse of L{linkToItem}. @rtype: L{axiom.item.Item} """ return self.webTranslator.fromWebID(link) def _webTranslator(store, fallback): """ Discover a web translator based on an Axiom store and a specified default. Prefer the specified default. This is an implementation detail of various initializers in this module which require an L{IWebTranslator} provider. Some of those initializers did not previously require a webTranslator, so this function will issue a L{UserWarning} if no L{IWebTranslator} powerup exists for the given store and no fallback is provided. @param store: an L{axiom.store.Store} @param fallback: a provider of L{IWebTranslator}, or None @return: 'fallback', if it is provided, or the L{IWebTranslator} powerup on 'store'. """ if fallback is None: fallback = IWebTranslator(store, None) if fallback is None: warnings.warn( "No IWebTranslator plugin when creating Scrolltable - broken " "configuration, now deprecated! Try passing webTranslator " "keyword argument.", category=DeprecationWarning, stacklevel=4) return fallback class InequalityModel(_ScrollableBase): """ This is a utility base class for things which want to communicate about large sets of Axiom items with a remote client. The first such implementation is L{ScrollingElement}, which communicates with a Nevow Athena JavaScript client. @ivar webTranslator: A L{IWebTranslator} provider for resolving and creating web links for items. @ivar columns: A mapping of attribute identifiers to L{IColumn} providers. @ivar columnNames: A list of attribute identifiers. @ivar isAscending: A boolean indicating the current order of the sort. @ivar currentSortColumn: An L{IColumn} representing the current sort key. """ def __init__(self, store, itemType, baseConstraint, columns, defaultSortColumn, defaultSortAscending, webTranslator=None): """ Create a new InequalityModel. @param store: the store to perform queries against when the client asks for data. @type store: L{axiom.store.Store} @param itemType: the type of item that will be returned by this L{InequalityModel}. @type itemType: a subclass of L{axiom.item.Item}. @param baseConstraint: an L{IQuery} provider that specifies the set of rows within this model. @param columns: a list of L{IColumn} providers listing the columns to be sent to the client. @param defaultSortColumn: an element of the C{columns} argument to sort by, by default. @param defaultSortAscending: is the sort ascending? XXX: this is not implemented and may not even be a good idea to implement. @param webTranslator: an L{IWebTranslator} provider used to generate IDs for the client from the C{itemType}'s storeID. """ super(InequalityModel, self).__init__( _webTranslator(store, webTranslator), columns, defaultSortColumn, defaultSortAscending) self.store = store self.itemType = itemType self.baseConstraint = baseConstraint def inequalityQuery(self, constraint, count, isAscending): """ Perform a query to obtain some rows from the table represented by this model, at the behest of a networked client. @param constraint: an additional constraint to apply to the query. @type constraint: L{axiom.iaxiom.IComparison}. @param count: the maximum number of rows to return. @type count: C{int} @param isAscending: a boolean describing whether the query should be yielding ascending or descending results. @type isAscending: C{bool} @return: an query which will yield some results from this model. @rtype: L{axiom.iaxiom.IQuery} """ if self.baseConstraint is not None: if constraint is not None: constraint = AND(self.baseConstraint, constraint) else: constraint = self.baseConstraint # build the sort currentSortAttribute = self.currentSortColumn.sortAttribute() if isAscending: sort = (currentSortAttribute.ascending, self.itemType.storeID.ascending) else: sort = (currentSortAttribute.descending, self.itemType.storeID.descending) return self.store.query(self.itemType, constraint, sort=sort, limit=count).distinct() def rowsAfterValue(self, value, count): """ Retrieve some rows at or after a given sort-column value. @param value: Starting value in the index for the current sort column at which to start returning results. Rows with a column value for the current sort column which is greater than or equal to this value will be returned. @type value: Some type compatible with the current sort column, or None, to specify the beginning of the data. @param count: The maximum number of rows to return. @type count: C{int} @return: A list of row data, ordered by the current sort column, beginning at C{value} and containing at most C{count} elements. """ if value is None: query = self.inequalityQuery(None, count, True) else: pyvalue = self._toComparableValue(value) currentSortAttribute = self.currentSortColumn.sortAttribute() query = self.inequalityQuery(currentSortAttribute >= pyvalue, count, True) return self.constructRows(query) expose(rowsAfterValue) def rowsAfterItem(self, item, count): """ Retrieve some rows after a given item, not including the given item. @param item: then L{Item} to request something after. @type item: this L{InequalityModel}'s L{itemType} attribute. @param count: The maximum number of rows to return. @type count: L{int} @return: A list of row data, ordered by the current sort column, beginning immediately after C{item}. """ currentSortAttribute = self.currentSortColumn.sortAttribute() value = currentSortAttribute.__get__(item, type(item)) firstQuery = self.inequalityQuery( AND(currentSortAttribute == value, self.itemType.storeID > item.storeID), count, True) results = self.constructRows(firstQuery) count -= len(results) if count: secondQuery = self.inequalityQuery( currentSortAttribute > value, count, True) results.extend(self.constructRows(secondQuery)) return results def rowsAfterRow(self, rowObject, count): """ Wrapper around L{rowsAfterItem} which accepts the web ID for a item instead of the item itself. @param rowObject: a dictionary mapping strings to column values, sent from the client. One of those column values must be C{__id__} to uniquely identify a row. @param count: an integer, the number of rows to return. """ webID = rowObject['__id__'] return self.rowsAfterItem( self.webTranslator.fromWebID(webID), count) expose(rowsAfterRow) def rowsBeforeRow(self, rowObject, count): """ Wrapper around L{rowsBeforeItem} which accepts the web ID for a item instead of the item itself. @param rowObject: a dictionary mapping strings to column values, sent from the client. One of those column values must be C{__id__} to uniquely identify a row. @param count: an integer, the number of rows to return. """ webID = rowObject['__id__'] return self.rowsBeforeItem( self.webTranslator.fromWebID(webID), count) expose(rowsBeforeRow) def _toComparableValue(self, value): """ Trivial wrapper which takes into account the possibility that our sort column might not have defined the C{toComparableValue} method. This can probably serve as a good generic template for some infrastructure to deal with arbitrarily-potentially-missing methods from certain versions of interfaces, but we didn't take it any further than it needed to go for this system's fairly meagre requirements. *Please* feel free to refactor upwards as necessary. """ if hasattr(self.currentSortColumn, 'toComparableValue'): return self.currentSortColumn.toComparableValue(value) # Retrieve the location of the class's definition so that we can alert # the user as to where they need to insert their implementation. classDef = self.currentSortColumn.__class__ filename = inspect.getsourcefile(classDef) lineno = inspect.findsource(classDef)[1] warnings.warn_explicit( "IColumn implementor " + qual(self.currentSortColumn.__class__) + " " "does not implement method toComparableValue. This is required since " "Mantissa 0.6.6.", DeprecationWarning, filename, lineno) return value def rowsBeforeValue(self, value, count): """ Retrieve display data for rows with sort-column values less than the given value. @type value: Some type compatible with the current sort column. @param value: Starting value in the index for the current sort column at which to start returning results. Rows with a column value for the current sort column which is less than this value will be returned. @type count: C{int} @param count: The number of rows to return. @return: A list of row data, ordered by the current sort column, ending at C{value} and containing at most C{count} elements. """ if value is None: query = self.inequalityQuery(None, count, False) else: pyvalue = self._toComparableValue(value) currentSortAttribute = self.currentSortColumn.sortAttribute() query = self.inequalityQuery( currentSortAttribute < pyvalue, count, False) return self.constructRows(query)[::-1] expose(rowsBeforeValue) def rowsBeforeItem(self, item, count): """ The inverse of rowsAfterItem. @param item: then L{Item} to request rows before. @type item: this L{InequalityModel}'s L{itemType} attribute. @param count: The maximum number of rows to return. @type count: L{int} @return: A list of row data, ordered by the current sort column, beginning immediately after C{item}. """ currentSortAttribute = self.currentSortColumn.sortAttribute() value = currentSortAttribute.__get__(item, type(item)) firstQuery = self.inequalityQuery( AND(currentSortAttribute == value, self.itemType.storeID < item.storeID), count, False) results = self.constructRows(firstQuery) count -= len(results) if count: secondQuery = self.inequalityQuery(currentSortAttribute < value, count, False) results.extend(self.constructRows(secondQuery)) return results[::-1] class IndexingModel(_ScrollableBase): """ Mixin for "model" implementations of an in-browser scrollable list of elements. @ivar webTranslator: A L{IWebTranslator} provider for resolving and creating web links for items. @ivar columns: A mapping of attribute identifiers to L{IColumn} providers. @ivar columnNames: A list of attribute identifiers. @ivar isAscending: A boolean indicating the current order of the sort. @ivar currentSortColumn: An L{IColumn} representing the current sort key. """ def requestRowRange(self, rangeBegin, rangeEnd): """ Retrieve display data for the given range of rows. @type rangeBegin: C{int} @param rangeBegin: The index of the first row to retrieve. @type rangeEnd: C{int} @param rangeEnd: The index of the last row to retrieve. @return: A C{list} of C{dict}s giving row data. """ return self.constructRows(self.performQuery(rangeBegin, rangeEnd)) expose(requestRowRange) # The rest takes care of responding to requests from the client. def getTableMetadata(self): """ Retrieve a description of the various properties of this scrolltable. @return: A sequence containing 5 elements. They are, in order, a list of the names of the columns present, a mapping of column names to two-tuples of their type and a boolean indicating their sortability, the total number of rows in the scrolltable, the name of the default sort column, and a boolean indicating whether or not the current sort order is ascending. """ coltypes = {} for (colname, column) in self.columns.iteritems(): sortable = column.sortAttribute() is not None coltype = column.getType() if coltype is not None: coltype = unicode(coltype, 'ascii') coltypes[colname] = (coltype, sortable) if self.currentSortColumn: csc = unicode(self.currentSortColumn.sortAttribute().attrname, 'ascii') else: csc = None return [self.columnNames, coltypes, self.requestCurrentSize(), csc, self.isAscending] expose(getTableMetadata) def requestCurrentSize(self): return self.performCount() expose(requestCurrentSize) def performAction(self, name, rowID): method = getattr(self, 'action_' + name) item = self.itemFromLink(rowID) return method(item) expose(performAction) # Override these two in a subclass def performCount(self): """ Override this in a subclass. @rtype: C{int} @return: The total number of elements in this scrollable. """ raise NotImplementedError() def performQuery(self, rangeBegin, rangeEnd): """ Override this in a subclass. @rtype: C{list} @return: Elements from C{rangeBegin} to C{rangeEnd} of the underlying data set, as ordered by the value of C{currentSortColumn} sort column in the order indicated by C{isAscending}. """ raise NotImplementedError() Scrollable = IndexingModel # The old (deprecated) name of this class. class ScrollableView(object): """ Mixin for structuring model data in the way expected by Mantissa.ScrollTable.ScrollingWidget. Subclasses must also mix in L{_ScrollableBase} to provide required attributes and methods. """ jsClass = u'Mantissa.ScrollTable.ScrollingWidget' fragmentName = 'scroller' def constructRows(self, items): """ Build row objects that are serializable using Athena for sending to the client. @param items: an iterable of objects compatible with my columns' C{extractValue} methods. @return: a list of dictionaries, where each dictionary has a string key for each column name in my list of columns. """ rows = [] for item in items: row = dict((colname, col.extractValue(self, item)) for (colname, col) in self.columns.iteritems()) link = self.linkToItem(item) if link is not None: row[u'__id__'] = link rows.append(row) return rows class ItemQueryScrollingFragment(IndexingModel, ScrollableView, LiveElement): """ An L{ItemQueryScrollingFragment} is an Athena L{LiveElement} that can display an Axiom query using an inefficient, but precise, method for counting rows and getting data at given offsets when requested. New code which wants to display a scrollable list of data should probably use L{ScrollingElement} instead. """ def __init__(self, store, itemType, baseConstraint, columns, defaultSortColumn=None, defaultSortAscending=True, webTranslator=None, *a, **kw): self.store = store self.itemType = itemType self.baseConstraint = baseConstraint IndexingModel.__init__( self, _webTranslator(store, webTranslator), columns, defaultSortColumn, defaultSortAscending) LiveElement.__init__(self, *a, **kw) def _cannotDetermineSort(self, defaultSortColumn): """ In this old, index-based way of doing things, we can do even more implicit horrible stuff to determine the sort column, or even give up completely and accept an implicit sort. NB: this is still terrible behavior, but lots of old code relied on it, and since this class is legacy anyway it won't be deprecated or removed. We can also accept a sort column in this table that is not actually displayed or sent to the client at all. """ if defaultSortColumn is not None: self.currentSortColumn = IColumn(defaultSortColumn) def getInitialArguments(self): return [self.getTableMetadata()] def performCount(self): return self.store.query(self.itemType, self.baseConstraint).count() def performQuery(self, rangeBegin, rangeEnd): if self.isAscending: sort = self.currentSortColumn.sortAttribute().ascending else: sort = self.currentSortColumn.sortAttribute().descending return list(self.store.query(self.itemType, self.baseConstraint, offset=rangeBegin, limit=rangeEnd - rangeBegin, sort=sort)) ScrollingFragment = ItemQueryScrollingFragment class SequenceScrollingFragment(IndexingModel, ScrollableView, LiveElement): """ Scrolltable implementation backed by any Python L{axiom.item.Item} sequence. """ def __init__(self, store, elements, columns, defaultSortColumn=None, defaultSortAscending=True, webTranslator=None, *a, **kw): IndexingModel.__init__( self, _webTranslator(store, webTranslator), columns, defaultSortColumn, defaultSortAscending) LiveElement.__init__(self, *a, **kw) self.store = store self.elements = elements def _cannotDetermineSort(self, defaultSortColumn): """ Since this model back-ends to a fixed-index sequence anyway, we won't be sorting by anything and the default sort column will always be None. """ def getInitialArguments(self): return [self.getTableMetadata()] def performCount(self): return len(self.elements) def performQuery(self, rangeBegin, rangeEnd): step = 1 if not self.isAscending: # The ranges are from the end, not the beginning. rangeBegin = max(0, len(self.elements) - rangeBegin - 1) # Python is so very very confusing: # s[1:0:-1] == [] # s[1:None:-1] == [s[0]] # s[1:-1:-1] == some crazy thing you don't even want to know rangeEnd = max(-1, len(self.elements) - rangeEnd - 1) if rangeEnd == -1: rangeEnd = None step = -1 result = self.elements[rangeBegin:rangeEnd:step] return result class StoreIDSequenceScrollingFragment(SequenceScrollingFragment): """ Scrolltable implementation like L{SequenceScrollingFragment} but which is backed by a sequence of Item storeID values rather than Items themselves. """ def performQuery(self, rangeBegin, rangeEnd): return map( self.store.getItemByID, super( StoreIDSequenceScrollingFragment, self).performQuery(rangeBegin, rangeEnd)) class SearchResultScrollingFragment(SequenceScrollingFragment): """ Scrolltable implementation like L{SequenceScrollingFragment} but which is backed by a sequence of L{_PyLuceneHitWrapper} instances. XXX _PyLuceneHitWrapper should probably implement IFulltextIndexable instead of a subtly different interface. """ def performQuery(self, rangeBegin, rangeEnd): results = SequenceScrollingFragment.performQuery( self, rangeBegin, rangeEnd) return [ self.store.getItemByID(int(hit.uniqueIdentifier)) for hit in results] class ScrollingElement(InequalityModel, ScrollableView, LiveElement): """ Element for scrolling lists of items, which uses L{InequalityModel}. """ jsClass = u'Mantissa.ScrollTable.ScrollTable' fragmentName = 'inequality-scroller' def __init__(self, store, itemType, baseConstraint, columns, defaultSortColumn=None, defaultSortAscending=True, webTranslator=None, *a, **kw): InequalityModel.__init__( self, store, itemType, baseConstraint, columns, defaultSortColumn, defaultSortAscending, webTranslator) LiveElement.__init__(self, *a, **kw) def _getColumnList(self): """ Get a list of serializable objects that describe the interesting columns on our item type. Columns which report having no type will be treated as having the type I{text}. @rtype: C{list} of C{dict} """ columnList = [] for columnName in self.columnNames: column = self.columns[columnName] type = column.getType() if type is None: type = 'text' columnList.append( {u'name': columnName, u'type': type.decode('ascii')}) return columnList def getInitialArguments(self): """ Return the constructor arguments required for the JavaScript client class, Mantissa.ScrollTable.ScrollTable. @return: a 3-tuple of:: - The unicode attribute ID of my current sort column - A list of dictionaries with 'name' and 'type' keys which are strings describing the name and type of all the columns in this table. - A bool indicating whether the sort direction is initially ascending. """ ic = IColumn(self.currentSortColumn) return [ic.attributeID.decode('ascii'), self._getColumnList(), self.isAscending] PK�����9FOfW�������xmantissa/search.py# -*- test-case-name: xmantissa.test.test_search -*- from __future__ import division from zope.interface import implements from twisted.internet import defer from twisted.python import log, components from nevow import inevow, athena, tags from axiom import attributes, item from axiom.upgrade import registerDeletionUpgrader from xmantissa import ixmantissa class SearchResult(item.Item): """ A temporary, in-database object associated with a particular search (ie, one time that one guy typed in that one search phrase) and a single item which was found in that search. These live in the database to make it easy to display and sort them, but they are deleted when they get kind of oldish. These are no longer used. The upgrader to version 2 unconditionally deletes them. """ schemaVersion = 2 indexedItem = attributes.reference() identifier = attributes.integer() registerDeletionUpgrader(SearchResult, 1, 2) class SearchAggregator(item.Item): implements(ixmantissa.ISearchAggregator, ixmantissa.INavigableElement) powerupInterfaces = (ixmantissa.ISearchAggregator, ixmantissa.INavigableElement) schemaVersion = 1 typeName = 'mantissa_search_aggregator' installedOn = attributes.reference() searches = attributes.integer(default=0) # INavigableElement def getTabs(self): return [] # ISearchAggregator def providers(self): return list(self.store.powerupsFor(ixmantissa.ISearchProvider)) def count(self, term): def countedHits(results): total = 0 for (success, result) in results: if success: total += result else: log.err(result) return total return defer.DeferredList([ provider.count(term) for provider in self.providers()], consumeErrors=True).addCallback(countedHits) def search(self, *a, **k): self.searches += 1 d = defer.DeferredList([ provider.search(*a, **k) for provider in self.providers() ], consumeErrors=True) def searchCompleted(results): allSearchResults = [] for (success, result) in results: if success: allSearchResults.append(result) else: log.err(result) return allSearchResults d.addCallback(searchCompleted) return d def parseSearchTerm(term): """ Turn a string search query into a two-tuple of a search term and a dictionary of search keywords. """ terms = [] keywords = {} for word in term.split(): if word.count(':') == 1: k, v = word.split(u':') if k and v: keywords[k] = v elif k or v: terms.append(k or v) else: terms.append(word) term = u' '.join(terms) if keywords: return term, keywords return term, None class AggregateSearchResults(athena.LiveFragment): fragmentName = 'search' def __init__(self, aggregator): super(AggregateSearchResults, self).__init__() self.aggregator = aggregator def head(self): return None def render_search(self, ctx, data): req = inevow.IRequest(ctx) term = req.args.get('term', [None])[0] charset = req.args.get('_charset_')[0] if term is None: return '' try: term = term.decode(charset) except LookupError: log.err('Unable to decode search query encoded as %s.' % charset) return tags.div[ "Your browser sent your search query in an encoding that we do not understand.", tags.br, "Please set your browser's character encoding to 'UTF-8' (under the View menu in Firefox)."] term, keywords = parseSearchTerm(term) d = self.aggregator.search(term, keywords) def gotSearchResultFragments(fragments): for f in fragments: f.setFragmentParent(self) return fragments d.addCallback(gotSearchResultFragments) return d components.registerAdapter(AggregateSearchResults, SearchAggregator, ixmantissa.INavigableFragment) PK�����9FSv�������xmantissa/settings.pyfrom axiom.item import Item from axiom import attributes from axiom.upgrade import registerDeletionUpgrader class Settings(Item): typeName = 'mantissa_settings' schemaVersion = 2 installedOn = attributes.reference() def settings1to2(old): new = old.upgradeVersion('mantissa_settings', 1, 2, installedOn=None) new.deleteFromStore() registerDeletionUpgrader(Settings, 1, 2) PK�����9FVMx��x�����xmantissa/sharing.py# -*- test-case-name: xmantissa.test.test_sharing -*- """ This module provides various abstractions for sharing public data in Axiom. """ import os import warnings from zope.interface import implementedBy, directlyProvides, Interface from twisted.python.reflect import qual, namedAny from twisted.protocols.amp import Argument, Box, parseString from epsilon.structlike import record from axiom import userbase from axiom.item import Item from axiom.attributes import reference, text, AND from axiom.upgrade import registerUpgrader ALL_IMPLEMENTED_DB = u'*' ALL_IMPLEMENTED = object() class NoSuchShare(Exception): """ User requested an object that doesn't exist, was not allowed. """ class ConflictingNames(Exception): """ The same name was defined in two separate interfaces. """ class RoleRelationship(Item): """ RoleRelationship is a bridge record linking member roles with group roles that they are members of. """ schemaVersion = 1 typeName = 'sharing_relationship' member = reference( doc=""" This is a reference to a L{Role} which is a member of my 'group' attribute. """) group = reference( doc=""" This is a reference to a L{Role} which represents a group that my 'member' attribute is a member of. """) def _entuple(r): """ Convert a L{record} to a tuple. """ return tuple(getattr(r, n) for n in r.__names__) class Identifier(record('shareID localpart domain')): """ A fully-qualified identifier for an entity that can participate in a message either as a sender or a receiver. """ @classmethod def fromSharedItem(cls, sharedItem): """ Return an instance of C{cls} derived from the given L{Item} that has been shared. Note that this API does not provide any guarantees of which result it will choose. If there are are multiple possible return values, it will select and return only one. Items may be shared under multiple L{shareID}s. A user may have multiple valid account names. It is sometimes impossible to tell from context which one is appropriate, so if your application has another way to select a specific shareID you should use that instead. @param sharedItem: an L{Item} that should be shared. @return: an L{Identifier} describing the C{sharedItem} parameter. @raise L{NoSuchShare}: if the given item is not shared or its store does not contain any L{LoginMethod} items which would identify a user. """ localpart = None for (localpart, domain) in userbase.getAccountNames(sharedItem.store): break if localpart is None: raise NoSuchShare() for share in sharedItem.store.query(Share, Share.sharedItem == sharedItem): break else: raise NoSuchShare() return cls( shareID=share.shareID, localpart=localpart, domain=domain) def __cmp__(self, other): """ Compare this L{Identifier} to another object. """ # Note - might be useful to have this usable by arbitrary L{record} # objects. It can't be the default, but perhaps a mixin? if not isinstance(other, Identifier): return NotImplemented return cmp(_entuple(self), _entuple(other)) class IdentifierArgument(Argument): """ An AMP argument which can serialize and deserialize an L{Identifier}. """ def toString(self, obj): """ Convert the given L{Identifier} to a string. """ return Box(shareID=obj.shareID.encode('utf-8'), localpart=obj.localpart.encode('utf-8'), domain=obj.domain.encode('utf-8')).serialize() def fromString(self, inString): """ Convert the given string to an L{Identifier}. """ box = parseString(inString)[0] return Identifier(shareID=box['shareID'].decode('utf-8'), localpart=box['localpart'].decode('utf-8'), domain=box['domain'].decode('utf-8')) class Role(Item): """ A Role is an identifier for a group or individual which has certain permissions. Items shared within the sharing system are always shared with a particular role. """ schemaVersion = 1 typeName = 'sharing_role' externalID = text( doc=""" This is the external identifier which the role is known by. This field is used to associate users with their primary role. If a user logs in as bob@divmod.com, the sharing system will associate his primary role with the pre-existing role with the externalID of 'bob@divmod.com', or 'Everybody' if no such role exists. For group roles, the externalID is not currently used except as a display identifier. Group roles should never have an '@' character in them, however, to avoid confusion with user roles. """, allowNone=False) # XXX TODO: In addition to the externalID, we really need to have something # that identifies what protocol the user for the role is expected to log in # as, and a way to identify the way that their role was associated with # their login. For example, it might be acceptable for some security # applications (e.g. spam prevention) to simply use an HTTP cookie. For # others (accounting database manipulation) it should be possible to # require more secure methods of authentication, like a signed client # certificate. description = text( doc=""" This is a free-form descriptive string for use by users to explain the purpose of the role. Since the externalID is used by security logic and must correspond to a login identifier, this can be used to hold a user's real name. """) def becomeMemberOf(self, groupRole): """ Instruct this (user or group) Role to become a member of a group role. @param groupRole: The role that this group should become a member of. """ self.store.findOrCreate(RoleRelationship, group=groupRole, member=self) def allRoles(self, memo=None): """ Identify all the roles that this role is authorized to act as. @param memo: used only for recursion. Do not pass this. @return: an iterator of all roles that this role is a member of, including itself. """ if memo is None: memo = set() elif self in memo: # this is bad, but we have successfully detected and prevented the # only really bad symptom, an infinite loop. return memo.add(self) yield self for groupRole in self.store.query(Role, AND(RoleRelationship.member == self, RoleRelationship.group == Role.storeID)): for roleRole in groupRole.allRoles(memo): yield roleRole def shareItem(self, sharedItem, shareID=None, interfaces=ALL_IMPLEMENTED): """ Share an item with this role. This provides a way to expose items to users for later retrieval with L{Role.getShare}. @param sharedItem: an item to be shared. @param shareID: a unicode string. If provided, specify the ID under which the shared item will be shared. @param interfaces: a list of Interface objects which specify the methods and attributes accessible to C{toRole} on C{sharedItem}. @return: a L{Share} which records the ability of the given role to access the given item. """ if shareID is None: shareID = genShareID(sharedItem.store) return Share(store=self.store, shareID=shareID, sharedItem=sharedItem, sharedTo=self, sharedInterfaces=interfaces) def getShare(self, shareID): """ Retrieve a proxy object for a given shareID, previously shared with this role or one of its group roles via L{Role.shareItem}. @return: a L{SharedProxy}. This is a wrapper around the shared item which only exposes those interfaces explicitly allowed for the given role. @raise: L{NoSuchShare} if there is no item shared to the given role for the given shareID. """ shares = list( self.store.query(Share, AND(Share.shareID == shareID, Share.sharedTo.oneOf(self.allRoles())))) interfaces = [] for share in shares: interfaces += share.sharedInterfaces if shares: return SharedProxy(shares[0].sharedItem, interfaces, shareID) raise NoSuchShare() def asAccessibleTo(self, query): """ @param query: An Axiom query describing the Items to retrieve, which this role can access. @type query: an L{iaxiom.IQuery} provider. @return: an iterable which yields the shared proxies that are available to the given role, from the given query. """ # XXX TODO #2371: this method really *should* be returning an L{IQuery} # provider as well, but that is kind of tricky to do. Currently, doing # queries leaks authority, because the resulting objects have stores # and "real" items as part of their interface; having this be a "real" # query provider would obviate the need to escape the L{SharedProxy} # security constraints in order to do any querying. allRoles = list(self.allRoles()) count = 0 unlimited = query.cloneQuery(limit=None) for result in unlimited: allShares = list(query.store.query( Share, AND(Share.sharedItem == result, Share.sharedTo.oneOf(allRoles)))) interfaces = [] for share in allShares: interfaces += share.sharedInterfaces if allShares: count += 1 yield SharedProxy(result, interfaces, allShares[0].shareID) if count == query.limit: return class _really(object): """ A dynamic proxy for dealing with 'private' attributes on L{SharedProxy}, which overrides C{__getattribute__} itself. This is pretty syntax to avoid ugly references to __dict__ and super and object.__getattribute__() in dynamic proxy implementations. """ def __init__(self, orig): """ Create a _really object with a dynamic proxy. @param orig: an object that overrides __getattribute__, probably L{SharedProxy}. """ self.orig = orig def __setattr__(self, name, value): """ Set an attribute on my original, unless my original has not yet been set, in which case set it on me. """ try: orig = object.__getattribute__(self, 'orig') except AttributeError: object.__setattr__(self, name, value) else: object.__setattr__(orig, name, value) def __getattribute__(self, name): """ Get an attribute present on my original using L{object.__getattribute__}, not the overridden version. """ return object.__getattribute__(object.__getattribute__(self, 'orig'), name) ALLOWED_ON_PROXY = ['__provides__', '__dict__'] class SharedProxy(object): """ A shared proxy is a dynamic proxy which provides exposes methods and attributes declared by shared interfaces on a given Item. These are returned from L{Role.getShare} and yielded from L{Role.asAccessibleTo}. Shared proxies are unlike regular items because they do not have 'storeID' or 'store' attributes (unless explicitly exposed). They are designed to make security easy to implement: if you have a shared proxy, you can access any attribute or method on it without having to do explicit permission checks. If you *do* want to perform an explicit permission check, for example, to render some UI associated with a particular permission, it can be performed as a functionality check instead. For example, C{if getattr(proxy, 'feature', None) is None:} or, more formally, C{IFeature.providedBy(proxy)}. If your interfaces are all declared and implemented properly everywhere, these checks will work both with shared proxies and with the original Items that they represent (but of course, the original Items will always provide all of their features). (Note that object.__getattribute__ still lets you reach inside any object, so don't imagine this makes you bulletproof -- you have to cooperate with it.) """ def __init__(self, sharedItem, sharedInterfaces, shareID): """ Create a shared proxy for a given item. @param sharedItem: The original item that was shared. @param sharedInterfaces: a list of interfaces which C{sharedItem} implements that this proxy should allow access to. @param shareID: the external identifier that the shared item was shared as. """ rself = _really(self) rself._sharedItem = sharedItem rself._shareID = shareID rself._adapterCache = {} # Drop all duplicate shared interfaces. uniqueInterfaces = list(sharedInterfaces) # XXX there _MUST_ Be a better algorithm for this for left in sharedInterfaces: for right in sharedInterfaces: if left.extends(right) and right in uniqueInterfaces: uniqueInterfaces.remove(right) for eachInterface in uniqueInterfaces: if not eachInterface.providedBy(sharedItem): impl = eachInterface(sharedItem, None) if impl is not None: rself._adapterCache[eachInterface] = impl rself._sharedInterfaces = uniqueInterfaces # Make me look *exactly* like the item I am proxying for, at least for # the purposes of adaptation # directlyProvides(self, providedBy(sharedItem)) directlyProvides(self, uniqueInterfaces) def __repr__(self): """ Return a pretty string representation of this shared proxy. """ rself = _really(self) return 'SharedProxy(%r, %r, %r)' % ( rself._sharedItem, rself._sharedInterfaces, rself._shareID) def __getattribute__(self, name): """ @return: attributes from my shared item, present in the shared interfaces list for this proxy. @param name: the name of the attribute to retrieve. @raise AttributeError: if the attribute was not found or access to it was denied. """ if name in ALLOWED_ON_PROXY: return object.__getattribute__(self, name) rself = _really(self) if name == 'sharedInterfaces': return rself._sharedInterfaces elif name == 'shareID': return rself._shareID for iface in rself._sharedInterfaces: if name in iface: if iface in rself._adapterCache: return getattr(rself._adapterCache[iface], name) return getattr(rself._sharedItem, name) raise AttributeError("%r has no attribute %r" % (self, name)) def __setattr__(self, name, value): """ Set an attribute on the shared item. If the name of the attribute is in L{ALLOWED_ON_PROXY}, set it on this proxy instead. @param name: the name of the attribute to set @param value: the value of the attribute to set @return: None """ if name in ALLOWED_ON_PROXY: self.__dict__[name] = value else: raise AttributeError("unsettable: "+repr(name)) def _interfacesToNames(interfaces): """ Convert from a list of interfaces to a unicode string of names suitable for storage in the database. @param interfaces: an iterable of Interface objects. @return: a unicode string, a comma-separated list of names of interfaces. @raise ConflictingNames: if any of the names conflict: see L{_checkConflictingNames}. """ if interfaces is ALL_IMPLEMENTED: names = ALL_IMPLEMENTED_DB else: _checkConflictingNames(interfaces) names = u','.join(map(qual, interfaces)) return names class Share(Item): """ A Share is a declaration that users with a given role can access a given set of functionality, as described by an Interface object. They should be created with L{Role.shareItem} and retrieved with L{Role.asAccessibleTo} and L{Role.getShare}. """ schemaVersion = 2 typeName = 'sharing_share' shareID = text( doc=""" The shareID is the externally-visible identifier for this share. It is free-form text, which users may enter to access this share. Currently the only concrete use of this attribute is in HTTP[S] URLs, but in the future it will be used in menu entries. """, allowNone=False) sharedItem = reference( doc=""" The sharedItem attribute is a reference to the item which is being provided. """, allowNone=False, whenDeleted=reference.CASCADE) sharedTo = reference( doc=""" The sharedTo attribute is a reference to the Role which this item is shared with. """, allowNone=False) sharedInterfaceNames = text( doc=""" This is an internal implementation detail of the sharedInterfaces attribute. """, allowNone=False) def __init__(self, **kw): """ Create a share. Consider this interface private; use L{shareItem} instead. """ # XXX TODO: All I really want to do here is to have enforcement of # allowNone happen at the _end_ of __init__; axiom should probably do # that by default, since there are several __init__s like this which # don't really do anything scattered throughout the codebase. kw['sharedInterfaceNames'] = _interfacesToNames(kw.pop('sharedInterfaces')) super(Share, self).__init__(**kw) def sharedInterfaces(): """ This attribute is the public interface for code which wishes to discover the list of interfaces allowed by this Share. It is a list of Interface objects. """ def get(self): if not self.sharedInterfaceNames: return () if self.sharedInterfaceNames == ALL_IMPLEMENTED_DB: I = implementedBy(self.sharedItem.__class__) L = list(I) T = tuple(L) return T else: return tuple(map(namedAny, self.sharedInterfaceNames.split(u','))) def set(self, newValue): self.sharedAttributeNames = _interfacesToNames(newValue) return get, set sharedInterfaces = property( doc=sharedInterfaces.__doc__, *sharedInterfaces()) def upgradeShare1to2(oldShare): "Upgrader from Share version 1 to version 2." sharedInterfaces = [] attrs = set(oldShare.sharedAttributeNames.split(u',')) for iface in implementedBy(oldShare.sharedItem.__class__): if set(iface) == attrs or attrs == set('*'): sharedInterfaces.append(iface) newShare = oldShare.upgradeVersion('sharing_share', 1, 2, shareID=oldShare.shareID, sharedItem=oldShare.sharedItem, sharedTo=oldShare.sharedTo, sharedInterfaces=sharedInterfaces) return newShare registerUpgrader(upgradeShare1to2, 'sharing_share', 1, 2) def genShareID(store): """ Generate a new, randomized share-ID for use as the default of shareItem, if none is specified. @return: a random share-ID. @rtype: unicode. """ return unicode(os.urandom(16).encode('hex'), 'ascii') def getEveryoneRole(store): """ Get a base 'Everyone' role for this store, which is the role that every user, including the anonymous user, has. """ return store.findOrCreate(Role, externalID=u'Everyone') def getAuthenticatedRole(store): """ Get the base 'Authenticated' role for this store, which is the role that is given to every user who is explicitly identified by a non-anonymous username. """ def tx(): def addToEveryone(newAuthenticatedRole): newAuthenticatedRole.becomeMemberOf(getEveryoneRole(store)) return newAuthenticatedRole return store.findOrCreate(Role, addToEveryone, externalID=u'Authenticated') return store.transact(tx) def getPrimaryRole(store, primaryRoleName, createIfNotFound=False): """ Get Role object corresponding to an identifier name. If the role name passed is the empty string, it is assumed that the user is not authenticated, and the 'Everybody' role is primary. If the role name passed is non-empty, but has no corresponding role, the 'Authenticated' role - which is a member of 'Everybody' - is primary. Finally, a specific role can be primary if one exists for the user's given credentials, that will automatically always be a member of 'Authenticated', and by extension, of 'Everybody'. @param primaryRoleName: a unicode string identifying the role to be retrieved. This corresponds to L{Role}'s externalID attribute. @param createIfNotFound: a boolean. If True, create a role for the given primary role name if no exact match is found. The default, False, will instead retrieve the 'nearest match' role, which can be Authenticated or Everybody depending on whether the user is logged in or not. @return: a L{Role}. """ if not primaryRoleName: return getEveryoneRole(store) ff = store.findUnique(Role, Role.externalID == primaryRoleName, default=None) if ff is not None: return ff authRole = getAuthenticatedRole(store) if createIfNotFound: role = Role(store=store, externalID=primaryRoleName) role.becomeMemberOf(authRole) return role return authRole def getSelfRole(store): """ Retrieve the Role which corresponds to the user to whom the given store belongs. """ return getAccountRole(store, userbase.getAccountNames(store)) def getAccountRole(store, accountNames): """ Retrieve the first Role in the given store which corresponds an account name in C{accountNames}. Note: the implementation currently ignores all of the values in C{accountNames} except for the first. @param accountNames: A C{list} of two-tuples of account local parts and domains. @raise ValueError: If C{accountNames} is empty. @rtype: L{Role} """ for (localpart, domain) in accountNames: return getPrimaryRole(store, u'%s@%s' % (localpart, domain), createIfNotFound=True) raise ValueError("Cannot get named role for unnamed account.") def shareItem(sharedItem, toRole=None, toName=None, shareID=None, interfaces=ALL_IMPLEMENTED): """ Share an item with a given role. This provides a way to expose items to users for later retrieval with L{Role.getShare}. This API is slated for deprecation. Prefer L{Role.shareItem} in new code. @param sharedItem: an item to be shared. @param toRole: a L{Role} instance which represents the group that has access to the given item. May not be specified if toName is also specified. @param toName: a unicode string which uniquely identifies a L{Role} in the same store as the sharedItem. @param shareID: a unicode string. If provided, specify the ID under which the shared item will be shared. @param interfaces: a list of Interface objects which specify the methods and attributes accessible to C{toRole} on C{sharedItem}. @return: a L{Share} which records the ability of the given role to access the given item. """ warnings.warn("Use Role.shareItem() instead of sharing.shareItem().", PendingDeprecationWarning, stacklevel=2) if toRole is None: if toName is not None: toRole = getPrimaryRole(sharedItem.store, toName, True) else: toRole = getEveryoneRole(sharedItem.store) return toRole.shareItem(sharedItem, shareID, interfaces) def _linearize(interface): """ Return a list of all the bases of a given interface in depth-first order. @param interface: an Interface object. @return: a L{list} of Interface objects, the input in all its bases, in subclass-to-base-class, depth-first order. """ L = [interface] for baseInterface in interface.__bases__: if baseInterface is not Interface: L.extend(_linearize(baseInterface)) return L def _commonParent(zi1, zi2): """ Locate the common parent of two Interface objects. @param zi1: a zope Interface object. @param zi2: another Interface object. @return: the rightmost common parent of the two provided Interface objects, or None, if they have no common parent other than Interface itself. """ shorter, longer = sorted([_linearize(x)[::-1] for x in zi1, zi2], key=len) for n in range(len(shorter)): if shorter[n] != longer[n]: if n == 0: return None return shorter[n-1] return shorter[-1] def _checkConflictingNames(interfaces): """ Raise an exception if any of the names present in the given interfaces conflict with each other. @param interfaces: a list of Zope Interface objects. @return: None @raise ConflictingNames: if any of the attributes of the provided interfaces are the same, and they do not have a common base interface which provides that name. """ names = {} for interface in interfaces: for name in interface: if name in names: otherInterface = names[name] parent = _commonParent(interface, otherInterface) if parent is None or name not in parent: raise ConflictingNames("%s conflicts with %s over %s" % ( interface, otherInterface, name)) names[name] = interface def getShare(store, role, shareID): """ Retrieve the accessible facet of an Item previously shared with L{shareItem}. This method is pending deprecation, and L{Role.getShare} should be preferred in new code. @param store: an axiom store (XXX must be the same as role.store) @param role: a L{Role}, the primary role for a user attempting to retrieve the given item. @return: a L{SharedProxy}. This is a wrapper around the shared item which only exposes those interfaces explicitly allowed for the given role. @raise: L{NoSuchShare} if there is no item shared to the given role for the given shareID. """ warnings.warn("Use Role.getShare() instead of sharing.getShare().", PendingDeprecationWarning, stacklevel=2) return role.getShare(shareID) def asAccessibleTo(role, query): """ Return an iterable which yields the shared proxies that are available to the given role, from the given query. This method is pending deprecation, and L{Role.asAccessibleTo} should be preferred in new code. @param role: The role to retrieve L{SharedProxy}s for. @param query: An Axiom query describing the Items to retrieve, which this role can access. @type query: an L{iaxiom.IQuery} provider. """ warnings.warn( "Use Role.asAccessibleTo() instead of sharing.asAccessibleTo().", PendingDeprecationWarning, stacklevel=2) return role.asAccessibleTo(query) def itemFromProxy(obj): """ Retrieve the real, underlying Item based on a L{SharedProxy} object, so that you can access all of its attributes and methods. This function is provided because sometimes it's hard to figure out how to cleanly achieve some behavior, especially running a query which relates to a shared proxy which you have retrieved. However, if you find yourself calling it a lot, that's a very bad sign: calling this method is implicitly a breach of the security that the sharing system tries to provide. Normally, if your code is acting as an agent of role X, it has access to a L{SharedProxy} that only provides interfaces explicitly allowed to X. If you make a mistake and call a method that the user is not supposed to be able to access, the user will receive an exception rather than be allowed to violate the system's security constraints. However, once you have retrieved the underlying item, all bets are off, and you have to perform your own security checks. This is error-prone, and should be avoided. We suggest, instead, adding explicitly allowed methods for performing any queries which your objects need. @param obj: a L{SharedProxy} instance @return: the underlying Item instance of the given L{SharedProxy}, with all of its methods and attributes exposed. """ return object.__getattribute__(obj, '_sharedItem') def unShare(sharedItem): """ Remove all instances of this item from public or shared view. """ sharedItem.store.query(Share, Share.sharedItem == sharedItem).deleteFromStore() def randomEarlyShared(store, role): """ If there are no explicitly-published public index pages to display, find a shared item to present to the user as first. """ for r in role.allRoles(): share = store.findFirst(Share, Share.sharedTo == r, sort=Share.storeID.ascending) if share is not None: return share.sharedItem raise NoSuchShare("Why, that user hasn't shared anything at all!") PK�����9F.y!��!�����xmantissa/signup.py# -*- test-case-name: xmantissa.test.test_signup,xmantissa.test.test_password_reset -*- import os, rfc822, hashlib, time, random from itertools import chain from zope.interface import Interface, implements from twisted.cred.portal import IRealm from twisted.python.components import registerAdapter from twisted.mail import smtp, relaymanager from twisted.python.util import sibpath from twisted.python import log from twisted import plugin from epsilon import extime from axiom.item import Item, transacted, declareLegacyItem from axiom.attributes import integer, reference, text, timestamp, AND from axiom.iaxiom import IBeneficiary from axiom import userbase, upgrade from axiom.userbase import getDomainNames from axiom.dependency import installOn from nevow.rend import Page, NotFound from nevow.url import URL from nevow.inevow import IResource, ISession from nevow import inevow, tags, athena from nevow.athena import expose from xmantissa.ixmantissa import ( ISiteRootPlugin, IStaticShellContent, INavigableElement, INavigableFragment, ISignupMechanism, ITemplateNameResolver) from xmantissa.website import PrefixURLMixin, WebSite from xmantissa.websession import usernameFromRequest from xmantissa.publicweb import PublicAthenaLivePage, PublicPage from xmantissa.webnav import Tab from xmantissa.webtheme import ThemedDocumentFactory, getLoader from xmantissa.webapp import PrivateApplication from xmantissa import plugins, liveform from xmantissa.websession import PersistentSession from xmantissa.smtp import parseAddress from xmantissa.error import ArgumentError from xmantissa.product import Product _theMX = None def getMX(): """ Retrieve the single MXCalculator instance, creating it first if necessary. """ global _theMX if _theMX is None: _theMX = relaymanager.MXCalculator() return _theMX def _sendEmail(_from, to, msg): def gotMX(mx): return smtp.sendmail(str(mx.name), _from, [to], msg) return getMX().getMX(to.split('@', 1)[1]).addCallback(gotMX) class PasswordResetResource(PublicPage): """ I handle the user-facing parts of password reset - the web form junk and sending of emails. The user sees a password reset form. The form asks the user for the username they use on this server. The form data is posted back to the C{PasswordResetResource}. On render, the resource creates a new 'attempt' (a L{_PasswordResetAttempt}) and sends an email to one of user's external addresses, if such a thing exists. The email contains a URL which is a child of this page, the last segment being the attempt ID. The user can then check their external email account and follow the link. Clicking the link loads the stored attempt and presents a password change form. The user can then specify a new password, click submit and their password will be reset. Currently unspecified behavior: - What happens when the provided username doesn't exist. - What happens when the provided passwords mismatch. - What happens when the user doesn't have an external account registered. @ivar store: a site store containing a L{WebSite}. @type store: L{axiom.store.Store} @ivar templateResolver: a template resolver instance that will return the appropriate doc factory. """ attempt = None def __init__(self, store, templateResolver=None): if templateResolver is None: templateResolver = ITemplateNameResolver(store) PublicPage.__init__(self, None, store, templateResolver.getDocFactory('reset'), None, None, templateResolver) self.store = store self.loginSystem = store.findUnique(userbase.LoginSystem, default=None) def locateChild(self, ctx, segments): """ Initialize self with the given key's L{_PasswordResetAttempt}, if any. @param segments: a L{_PasswordResetAttempt} key (hopefully) @return: C{(self, ())} with C{self.attempt} initialized, or L{NotFound} @see: L{attemptByKey} """ if len(segments) == 1: attempt = self.attemptByKey(unicode(segments[0])) if attempt is not None: self.attempt = attempt return (self, ()) return NotFound def renderHTTP(self, ctx): """ Handle the password reset form. The following exchange describes the process: S: Render C{reset} C: POST C{username} or C{email} S: L{handleRequestForUser}, render C{reset-check-email} (User follows the emailed reset link) S: Render C{reset-step-two} C: POST C{password1} S: L{resetPassword}, render C{reset-done} """ req = inevow.IRequest(ctx) if req.method == 'POST': if req.args.get('username', [''])[0]: user = unicode(usernameFromRequest(req), 'ascii') self.handleRequestForUser(user, URL.fromContext(ctx)) self.fragment = self.templateResolver.getDocFactory( 'reset-check-email') elif req.args.get('email', [''])[0]: email = req.args['email'][0].decode('ascii') acct = self.accountByAddress(email) if acct is not None: username = '@'.join( userbase.getAccountNames(acct.avatars.open()).next()) self.handleRequestForUser(username, URL.fromContext(ctx)) self.fragment = self.templateResolver.getDocFactory('reset-check-email') elif 'password1' in req.args: (password,) = req.args['password1'] self.resetPassword(self.attempt, unicode(password)) self.fragment = self.templateResolver.getDocFactory('reset-done') else: # Empty submit; redirect back to self return URL.fromContext(ctx) elif self.attempt: self.fragment = self.templateResolver.getDocFactory('reset-step-two') return PublicPage.renderHTTP(self, ctx) def handleRequestForUser(self, username, url): """ User C{username} wants to reset their password. Create an attempt item, and send them an email if the username is valid """ attempt = self.newAttemptForUser(username) account = self.accountByAddress(username) if account is None: # do we want to disclose this to the user? return email = self.getExternalEmail(account) if email is not None: self.sendEmail(url, attempt, email) def sendEmail(self, url, attempt, email, _sendEmail=_sendEmail): """ Send an email for the given L{_PasswordResetAttempt}. @type url: L{URL} @param url: The URL of the password reset page. @type attempt: L{_PasswordResetAttempt} @param attempt: An L{Item} representing a particular user's attempt to reset their password. @type email: C{str} @param email: The email will be sent to this address. """ host = url.netloc.split(':', 1)[0] from_ = 'reset@' + host body = file(sibpath(__file__, 'reset.rfc2822')).read() body %= {'from': from_, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': url.child(attempt.key)} _sendEmail(from_, email, body) def attemptByKey(self, key): """ Locate the L{_PasswordResetAttempt} that corresponds to C{key} """ return self.store.findUnique(_PasswordResetAttempt, _PasswordResetAttempt.key == key, default=None) def _makeKey(self, usern): """ Make a new, probably unique key. This key will be sent in an email to the user and is used to access the password change form. """ return unicode(hashlib.md5(str((usern, time.time(), random.random()))).hexdigest()) def newAttemptForUser(self, user): """ Create an L{_PasswordResetAttempt} for the user whose username is C{user} @param user: C{unicode} username """ # we could query for other attempts by the same # user within some timeframe and raise an exception, # if we wanted return _PasswordResetAttempt(store=self.store, username=user, timestamp=extime.Time(), key=self._makeKey(user)) def getExternalEmail(self, account): """ @return: str which is an external email address for the C{account}. None if there is no such address. """ # XXX - shouldn't userbase do this for me? - jml method = self.store.findFirst( userbase.LoginMethod, AND (userbase.LoginMethod.account == account, userbase.LoginMethod.internal == False)) if method is None: return None else: return '%s@%s' % (method.localpart, method.domain) def accountByAddress(self, username): """ @return: L{userbase.LoginAccount} for C{username} or None """ userAndDomain = username.split('@', 1) if len(userAndDomain) != 2: return None return self.loginSystem.accountByAddress(*userAndDomain) def resetPassword(self, attempt, newPassword): """ @param attempt: L{_PasswordResetAttempt} reset the password of the user who initiated C{attempt} to C{newPassword}, and afterward, delete the attempt and any persistent sessions that belong to the user """ self.accountByAddress(attempt.username).password = newPassword self.store.query( PersistentSession, PersistentSession.authenticatedAs == str(attempt.username) ).deleteFromStore() attempt.deleteFromStore() class _PasswordResetAttempt(Item): """ I represent as as-yet incomplete attempt at password reset """ typeName = 'password_reset_attempt' schemaVersion = 1 key = text() username = text() timestamp = timestamp() class PasswordReset(Item): """ I was an item that contained some of the model functionality of L{PasswordResetResource}, but now I am just a shell item (see L{passwordReset1to2}) and my functionality has been moved to L{PasswordResetResource}. """ typeName = 'password_reset' schemaVersion = 2 installedOn = reference() def passwordReset1to2(old): """ Power down and delete the item """ new = old.upgradeVersion(old.typeName, 1, 2, installedOn=None) for iface in new.store.interfacesFor(new): new.store.powerDown(new, iface) new.deleteFromStore() upgrade.registerUpgrader(passwordReset1to2, 'password_reset', 1, 2) class NoSuchFactory(Exception): """ An attempt was made to create a signup page using the name of a benefactor factory which did not correspond to anything in the database. """ class TicketClaimer(Page): def childFactory(self, ctx, name): for T in self.original.store.query( Ticket, AND(Ticket.booth == self.original, Ticket.nonce == unicode(name, 'ascii'))): something = T.claim() res = IResource(something) lgo = getattr(res, 'logout', lambda : None) ISession(ctx).setDefaultResource(res, lgo) return URL.fromContext(ctx).click("/private") return None class TicketBooth(Item, PrefixURLMixin): implements(ISiteRootPlugin) typeName = 'ticket_powerup' schemaVersion = 1 sessioned = True claimedTicketCount = integer(default=0) createdTicketCount = integer(default=0) defaultTicketEmail = text(default=None) prefixURL = 'ticket' def createResource(self): return TicketClaimer(self) def createTicket(self, issuer, email, product): t = self.store.findOrCreate( Ticket, product=product, booth=self, avatar=None, issuer=issuer, email=email) return t createTicket = transacted(createTicket) def ticketClaimed(self, ticket): self.claimedTicketCount += 1 def ticketLink(self, domainName, httpPortNumber, nonce): httpPort = '' httpScheme = 'http' if httpPortNumber == 443: httpScheme = 'https' elif httpPortNumber != 80: httpPort = ':' + str(httpPortNumber) return '%s://%s%s/%s/%s' % ( httpScheme, domainName, httpPort, self.prefixURL, nonce) def issueViaEmail(self, issuer, email, product, templateData, domainName, httpPort=80): """ Send a ticket via email to the supplied address, which, when claimed, will create an avatar and allow the given product to endow it with things. @param issuer: An object, preferably a user, to track who issued this ticket. @param email: a str, formatted as an rfc2821 email address (user@domain) -- source routes not allowed. @param product: an instance of L{Product} @param domainName: a domain name, used as the domain part of the sender's address, and as the web server to generate a link to within the email. @param httpPort: a port number for the web server running on domainName @param templateData: A string containing an rfc2822-format email message, which will have several python values interpolated into it dictwise: %(from)s: To be used for the From: header; will contain an rfc2822-format address. %(to)s: the address that we are going to send to. %(date)s: an rfc2822-format date. %(message-id)s: an rfc2822 message-id %(link)s: an HTTP URL that we are generating a link to. """ ticket = self.createTicket(issuer, unicode(email, 'ascii'), product) nonce = ticket.nonce signupInfo = {'from': 'signup@'+domainName, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': self.ticketLink(domainName, httpPort, nonce)} msg = templateData % signupInfo return ticket, _sendEmail(signupInfo['from'], email, msg) def _generateNonce(): return unicode(os.urandom(16).encode('hex'), 'ascii') class ITicketIssuer(Interface): def issueTicket(emailAddress): pass class SignupMechanism(object): """ I am a Twisted plugin helper. Instantiate me at module scope in a xmantissa.plugins submodule, including a name and description for the administrator. """ implements(ISignupMechanism, plugin.IPlugin) def __init__(self, name, description, itemClass, configuration): """ @param name: the name (a short string) to display to the administrator for selecting this signup mechanism. @param description: the description (a long string) for this signup mechanism. @param itemClass: a reference to a callable which takes keyword arguments described by L{configuration}, in addition to: store: the store to create the item in booth: a reference to a L{TicketBooth} that can create tickets for the created signup mechanism product: the product being installed by this signup. emailTemplate: a template for the email to be sent to the user prompt: a short unicode string describing this signup mechanism, as distinct from others. For example: "Student Sign Up", or "Faculty Sign Up" @param configuration: a list of LiveForm arguments. """ self.name = name self.description = description self.itemClass = itemClass self.configuration = configuration freeTicketSignupConfiguration = [ liveform.Parameter('prefixURL', liveform.TEXT_INPUT, unicode, u'The web location at which users will be able to request tickets.', default=u'signup')] class FreeTicketSignup(Item, PrefixURLMixin): implements(ISiteRootPlugin) typeName = 'free_signup' schemaVersion = 6 sessioned = True prefixURL = text(allowNone=False) booth = reference() product = reference(doc="An instance of L{product.Product} to install on" " the new user's store") emailTemplate = text() prompt = text() def createResource(self): return PublicAthenaLivePage( self.store, getLoader("signup"), IStaticShellContent(self.store, None), None, iface = ITicketIssuer, rootObject = self) def issueTicket(self, url, emailAddress): domain, port = url.get('hostname'), int(url.get('port') or 80) if os.environ.get('CC_DEV'): ticket = self.booth.createTicket(self, emailAddress, self.product) return '<a href="%s">Claim Your Account</a>' % ( self.booth.ticketLink(domain, port, ticket.nonce),) else: ticket, issueDeferred = self.booth.issueViaEmail( self, emailAddress.encode('ascii'), # heh self.product, self.emailTemplate, domain, port) issueDeferred.addCallback( lambda result: u'Please check your email for a ticket!') return issueDeferred def freeTicketSignup1To2(old): return old.upgradeVersion('free_signup', 1, 2, prefixURL=old.prefixURL, booth=old.booth, benefactor=old.benefactor) upgrade.registerUpgrader(freeTicketSignup1To2, 'free_signup', 1, 2) def freeTicketSignup2To3(old): emailTemplate = file(sibpath(__file__, 'signup.rfc2822')).read() emailTemplate %= {'blurb': u'', 'subject': 'Welcome to a Generic Axiom Application!', 'linktext': "Click here to claim your 'generic axiom application' account"} return old.upgradeVersion('free_signup', 2, 3, prefixURL=old.prefixURL, booth=old.booth, benefactor=old.benefactor, emailTemplate=emailTemplate) upgrade.registerUpgrader(freeTicketSignup2To3, 'free_signup', 2, 3) declareLegacyItem(typeName='free_signup', schemaVersion=3, attributes=dict(prefixURL=text(), booth=reference(), benefactor=reference(), emailTemplate=text())) def freeTicketSignup3To4(old): return old.upgradeVersion('free_signup', 3, 4, prefixURL=old.prefixURL, booth=old.booth, benefactor=old.benefactor, emailTemplate=old.emailTemplate, prompt=u'Sign Up') upgrade.registerUpgrader(freeTicketSignup3To4, 'free_signup', 3, 4) declareLegacyItem(typeName='free_signup', schemaVersion=4, attributes=dict(prefixURL=text(), booth=reference(), benefactor=reference(), emailTemplate=text(), prompt=text())) def freeTicketSignup4To5(old): return old.upgradeVersion('free_signup', 4, 5, prefixURL=old.prefixURL, booth=old.booth, benefactor=old.benefactor, emailTemplate=old.emailTemplate, prompt=old.prompt) upgrade.registerUpgrader(freeTicketSignup4To5, 'free_signup', 4, 5) declareLegacyItem(typeName='free_signup', schemaVersion=5, attributes=dict(prefixURL=text(), booth=reference(), benefactor=reference(), emailTemplate=text(), prompt=text())) def freeTicketSignup5To6(old): newProduct = old.store.findOrCreate(Product, types=list( chain(*[b.powerupNames for b in old.benefactor.benefactors('ascending')]))) return old.upgradeVersion('free_signup', 5, 6, prefixURL=old.prefixURL, booth=old.booth, product=newProduct, emailTemplate=old.emailTemplate, prompt=old.prompt) upgrade.registerUpgrader(freeTicketSignup5To6, "free_signup", 5, 6) class ValidatingSignupForm(liveform.LiveForm): jsClass = u'Mantissa.Validate.SignupForm' _parameterNames = [ 'realName', 'username', 'domain', 'password', 'emailAddress'] docFactory = ThemedDocumentFactory("user-info-signup", "templateResolver") def __init__(self, uis): self.userInfoSignup = uis self.templateResolver = ITemplateNameResolver(uis.store) super(ValidatingSignupForm, self).__init__( uis.createUser, [liveform.Parameter(pname, liveform.TEXT_INPUT, unicode) for pname in self._parameterNames]) def getInitialArguments(self): """ Retrieve a domain name from the user info signup item and return it so the client will know what domain it can sign up in. """ return (self.userInfoSignup.getAvailableDomains()[0],) def usernameAvailable(self, username, domain): return self.userInfoSignup.usernameAvailable(username, domain) athena.expose(usernameAvailable) class UserInfo(Item): """ An Item which stores information gleaned from the signup process of L{UserInfoSignup}. The L{UserInfo} will reside in the substore of the user (which was created during the signup process), and will record information about its owner. """ schemaVersion = 2 realName = text( doc=""" The name entered at signup time by the user as their I{real} name. """) def upgradeUserInfo1to2(oldUserInfo): """ Concatenate the I{firstName} and I{lastName} attributes from the old user info item and set the result as the I{realName} attribute of the upgraded item. """ newUserInfo = oldUserInfo.upgradeVersion( UserInfo.typeName, 1, 2, realName=oldUserInfo.firstName + u" " + oldUserInfo.lastName) return newUserInfo upgrade.registerUpgrader(upgradeUserInfo1to2, UserInfo.typeName, 1, 2) class UserInfoSignup(Item, PrefixURLMixin): """ This signup page provides a way to sign up while including some relevant information about yourself, including the selection of a username. """ implements(ISiteRootPlugin) powerupInterfaces = (ISiteRootPlugin,) schemaVersion = 2 sessioned = True booth = reference() product = reference( doc=""" An instance of L{product.Product} to install on the new user's store. """) emailTemplate = text() prompt = text() # ISiteRootPlugin prefixURL = text(allowNone=False) def createResource(self): page = PublicAthenaLivePage( self.store, ValidatingSignupForm(self), IStaticShellContent(self.store, None)) page.needsSecure = True return page # UserInfoSignup def getAvailableDomains(self): """ Return a list of domain names available on this site. """ return getDomainNames(self.store) def usernameAvailable(self, username, domain): """ Check to see if a username is available for the user to select. """ if len(username) < 2: return [False, u"Username too short"] for char in u"[ ,:;<>@()!\"'%&\\|\t\b": if char in username: return [False, u"Username contains invalid character: '%s'" % char] # The localpart is acceptable if it can be parsed as the local part # of an RFC 2821 address. try: parseAddress("<%s@example.com>" % (username,)) except ArgumentError: return [False, u"Username fails to parse"] # The domain is acceptable if it is one which we actually host. if domain not in self.getAvailableDomains(): return [False, u"Domain not allowed"] query = self.store.query(userbase.LoginMethod, AND(userbase.LoginMethod.localpart == username, userbase.LoginMethod.domain == domain)) return [not bool(query.count()), u"Username already taken"] def createUser(self, realName, username, domain, password, emailAddress): """ Create a user, storing some associated metadata in the user's store, i.e. their first and last names (as a L{UserInfo} item), and a L{axiom.userbase.LoginMethod} allowing them to login with their email address. @param realName: the real name of the user. @type realName: C{unicode} @param username: the user's username. they will be able to login with this. @type username: C{unicode} @param domain: the local domain - used internally to turn C{username} into a localpart@domain style string . @type domain: C{unicode} @param password: the password to be used for the user's account. @type password: C{unicode} @param emailAddress: the user's external email address. they will be able to login with this also. @type emailAddress: C{unicode} @rtype: C{NoneType} """ # XXX This method should be called in a transaction, it shouldn't # start a transaction itself. def _(): loginsystem = self.store.findUnique(userbase.LoginSystem) # Create an account with the credentials they specified, # making it internal since it belongs to us. acct = loginsystem.addAccount(username, domain, password, verified=True, internal=True) # Create an external login method associated with the email # address they supplied, as well. This creates an association # between that external address and their account object, # allowing password reset emails to be sent and letting them log # in to this account using that address as a username. emailPart, emailDomain = emailAddress.split("@") acct.addLoginMethod(emailPart, emailDomain, protocol=u"email", verified=False, internal=False) substore = IBeneficiary(acct) # Record some of that signup information in case application # objects are interested in it. UserInfo(store=substore, realName=realName) self.product.installProductOn(substore) self.store.transact(_) declareLegacyItem(typeName=UserInfoSignup.typeName, schemaVersion=1, attributes=dict(booth = reference(), benefactor = reference(), emailTemplate = text(), prompt = text(), prefixURL = text(allowNone=False))) def userInfoSignup1To2(old): newProduct = old.store.findOrCreate(Product, types=list( chain(*[b.powerupNames for b in old.benefactor.benefactors('ascending')]))) return old.upgradeVersion(UserInfoSignup.typeName, 1, 2, booth=old.booth, product=newProduct, emailTemplate=old.emailTemplate, prompt=old.prompt, prefixURL=old.prefixURL) upgrade.registerUpgrader(userInfoSignup1To2, UserInfoSignup.typeName, 1, 2) class InitializerBenefactor(Item): typeName = 'initializer_benefactor' schemaVersion = 1 realBenefactor = reference() def endow(self, ticket, beneficiary): beneficiary.findOrCreate(WebSite).installOn(beneficiary) beneficiary.findOrCreate(PrivateApplication).installOn(beneficiary) # They may have signed up in the past - if so, they already # have a password, and we should skip the initializer phase. substore = beneficiary.store.parent.getItemByID(beneficiary.store.idInParent) for acc in self.store.query(userbase.LoginAccount, userbase.LoginAccount.avatars == substore): if acc.password: self.realBenefactor.endow(ticket, beneficiary) else: beneficiary.findOrCreate(Initializer).installOn(beneficiary) break def resumeSignup(self, ticket, avatar): self.realBenefactor.endow(ticket, avatar) class Initializer(Item): implements(INavigableElement) typeName = 'password_initializer' schemaVersion = 1 installedOn = reference() powerupInterfaces = (INavigableElement,) def getTabs(self): # This won't ever actually show up return [Tab('Preferences', self.storeID, 1.0)] def setPassword(self, password): substore = self.store.parent.getItemByID(self.store.idInParent) for acc in self.store.parent.query(userbase.LoginAccount, userbase.LoginAccount.avatars == substore): acc.password = password self._delegateToBenefactor(acc) return def _delegateToBenefactor(self, loginAccount): site = self.store.parent ticket = site.findUnique(Ticket, Ticket.avatar == loginAccount) benefactor = ticket.benefactor benefactor.resumeSignup(ticket, self.store) self.store.powerDown(self, INavigableElement) self.deleteFromStore() class InitializerPage(PublicPage): def __init__(self, original): for resource, domain in userbase.getAccountNames(original.installedOn): username = '%s@%s' % (resource, domain) break else: username = None PublicPage.__init__(self, original, original.store.parent, getLoader('initialize'), IStaticShellContent(original.installedOn, None), username) def render_head(self, ctx, data): tag = PublicPage.render_head(self, ctx, data) return tag[tags.script(src='/Mantissa/js/initialize.js')] def renderHTTP(self, ctx): req = inevow.IRequest(ctx) password = req.args.get('password', [None])[0] if password is None: return Page.renderHTTP(self, ctx) self.original.store.transact(self.original.setPassword, unicode(password)) # XXX TODO: select # proper decoding # strategy. return URL.fromString('/') registerAdapter(InitializerPage, Initializer, inevow.IResource) class Ticket(Item): schemaVersion = 2 typeName = 'ticket' issuer = reference(allowNone=False) booth = reference(allowNone=False) avatar = reference() claimed = integer(default=0) product = reference(allowNone=False) email = text() nonce = text() def __init__(self, **kw): super(Ticket, self).__init__(**kw) self.booth.createdTicketCount += 1 self.nonce = _generateNonce() def claim(self): if not self.claimed: log.msg("Claiming a ticket for the first time for %r" % (self.email,)) username, domain = self.email.split('@', 1) realm = IRealm(self.store) acct = realm.accountByAddress(username, domain) if acct is None: acct = realm.addAccount(username, domain, None) self.avatar = acct self.claimed += 1 self.booth.ticketClaimed(self) self.product.installProductOn(IBeneficiary(self.avatar)) else: log.msg("Ignoring re-claim of ticket for: %r" % (self.email,)) return self.avatar claim = transacted(claim) def ticket1to2(old): """ change Ticket to refer to Products and not benefactor factories. """ if isinstance(old.benefactor, Multifactor): types = list(chain(*[b.powerupNames for b in old.benefactor.benefactors('ascending')])) elif isinstance(old.benefactor, InitializerBenefactor): #oh man what a mess types = list(chain(*[b.powerupNames for b in old.benefactor.realBenefactor.benefactors('ascending')])) newProduct = old.store.findOrCreate(Product, types=types) if old.issuer is None: issuer = old.store.findOrCreate(TicketBooth) else: issuer = old.issuer t = old.upgradeVersion(Ticket.typeName, 1, 2, product = newProduct, issuer = issuer, booth = old.booth, avatar = old.avatar, claimed = old.claimed, email = old.email, nonce = old.nonce) upgrade.registerUpgrader(ticket1to2, Ticket.typeName, 1, 2) class _DelegatedBenefactor(Item): typeName = 'mantissa_delegated_benefactor' schemaVersion = 1 benefactor = reference(allowNone=False) multifactor = reference(allowNone=False) order = integer(allowNone=False, indexed=True) class Multifactor(Item): """ A benefactor with no behavior of its own, but which collects references to other benefactors and delegates endowment responsibility to them. """ typeName = 'mantissa_multi_benefactor' schemaVersion = 1 order = integer(default=0) def benefactors(self, order): for deleg in self.store.query(_DelegatedBenefactor, _DelegatedBenefactor.multifactor == self, sort=getattr(_DelegatedBenefactor.order, order)): yield deleg.benefactor class _SignupTracker(Item): """ Signup-system private Item used to track which signup mechanisms have been created. """ signupItem = reference() createdOn = timestamp() createdBy = text() def _getPublicSignupInfo(siteStore): """ Get information about public web-based signup mechanisms. @param siteStore: a store with some signups installed on it (as indicated by _SignupTracker instances). @return: a generator which yields 2-tuples of (prompt, url) where 'prompt' is unicode briefly describing the signup mechanism (e.g. "Sign Up"), and 'url' is a (unicode) local URL linking to a page where an anonymous user can access it. """ # Note the underscore; this _should_ be a public API but it is currently an # unfortunate hack; there should be a different powerup interface that # requires prompt and prefixURL attributes rather than _SignupTracker. # -glyph for tr in siteStore.query(_SignupTracker): si = tr.signupItem p = getattr(si, 'prompt', None) u = getattr(si, 'prefixURL', None) if p is not None and u is not None: yield (p, u'/'+u) class SignupConfiguration(Item): """ Provide administrative configuration tools for the signup options available on a Mantissa server. """ typeName = 'mantissa_signup_configuration' schemaVersion = 1 installedOn = reference() powerupInterfaces = (INavigableElement,) def getTabs(self): return [Tab('Admin', self.storeID, 0.5, [Tab('Signup', self.storeID, 0.7)], authoritative=False)] def getSignupSystems(self): return dict((p.name, p) for p in plugin.getPlugins(ISignupMechanism, plugins)) def createSignup(self, creator, signupClass, signupConf, product, emailTemplate, prompt): """ Create a new signup facility in the site store's database. @param creator: a unicode string describing the creator of the new signup mechanism, for auditing purposes. @param signupClass: the item type of the signup mechanism to create. @param signupConf: a dictionary of keyword arguments for L{signupClass}'s constructor. @param product: A Product instance, describing the powerups to be installed with this signup. @param emailTemplate: a unicode string which contains some text that will be sent in confirmation emails generated by this signup mechanism (if any) @param prompt: a short unicode string describing this signup mechanism, as distinct from others. For example: "Student Sign Up", or "Faculty Sign Up" @return: a newly-created, database-resident instance of signupClass. """ siteStore = self.store.parent booth = siteStore.findOrCreate(TicketBooth, lambda booth: installOn(booth, siteStore)) signupItem = signupClass( store=siteStore, booth=booth, product=product, emailTemplate=emailTemplate, prompt=prompt, **signupConf) siteStore.powerUp(signupItem) _SignupTracker(store=siteStore, signupItem=signupItem, createdOn=extime.Time(), createdBy=creator) return signupItem class ProductFormMixin(object): """ Utility functions for rendering a form for choosing products to install. """ def makeProductPicker(self): """ Make a LiveForm with radio buttons for each Product in the store. """ productPicker = liveform.LiveForm( self.coerceProduct, [liveform.Parameter( str(id(product)), liveform.FORM_INPUT, liveform.LiveForm( lambda selectedProduct, product=product: selectedProduct and product, [liveform.Parameter( 'selectedProduct', liveform.RADIO_INPUT, bool, repr(product))] )) for product in self.original.store.parent.query(Product)], u"Product to Install") return productPicker def coerceProduct(self, **kw): """ Convert the return value from the form to a list of Products. """ return filter(None, kw.values())[0] class SignupFragment(athena.LiveFragment, ProductFormMixin): fragmentName = 'signup-configuration' live = 'athena' def head(self): # i think this is the lesser evil. # alternatives being: # * mangle form element names so we can put these in mantissa.css # without interfering with similarly named things # * put the following line of CSS into it's own file that is included # by only this page # * remove these styles entirely (makes the form unusable, the # type="text" inputs are *tiny*) return tags.style(type='text/css')[''' input[name=linktext], input[name=subject], textarea[name=blurb] { width: 40em } '''] def render_signupConfigurationForm(self, ctx, data): def makeSignupCoercer(signupPlugin): """ Return a function that converts a selected flag and a set of keyword arguments into either None (if not selected) or a 2-tuple of (signupClass, kwargs). signupClass is a callable which takes the kwargs as keyword arguments and returns an Item (a signup mechanism plugin gizmo). """ def signupCoercer(selectedSignup, **signupConf): """ Receive coerced values from the form post, massage them as described above. """ if selectedSignup: return signupPlugin.itemClass, signupConf return None return signupCoercer def coerceSignup(**kw): return filter(None, kw.values())[0] signupMechanismConfigurations = liveform.LiveForm( # makeSignupCoercer sets it up, we knock it down. (Nones returned # are ignored, there will be exactly one selected). coerceSignup, [liveform.Parameter( signupMechanism.name, liveform.FORM_INPUT, liveform.LiveForm( makeSignupCoercer(signupMechanism), [liveform.Parameter( 'selectedSignup', liveform.RADIO_INPUT, bool, signupMechanism.description)] + signupMechanism.configuration, signupMechanism.name)) for signupMechanism in self.original.getSignupSystems().itervalues()], u"Signup Type") def coerceEmailTemplate(**k): return file(sibpath(__file__, 'signup.rfc2822')).read() % k emailTemplateConfiguration = liveform.LiveForm( coerceEmailTemplate, [liveform.Parameter('subject', liveform.TEXT_INPUT, unicode, u'Email Subject', default='Welcome to a Generic Axiom Application!'), liveform.Parameter('blurb', liveform.TEXTAREA_INPUT, unicode, u'Blurb', default=''), liveform.Parameter('linktext', liveform.TEXT_INPUT, unicode, u'Link Text', default="Click here to claim your 'generic axiom application' account")], description='Email Template') emailTemplateConfiguration.docFactory = getLoader('liveform-compact') existing = list(self.original.store.parent.query(_SignupTracker)) if 0 < len(existing): deleteSignupForm = liveform.LiveForm( lambda **kw: self._deleteTrackers(k for (k, v) in kw.itervalues() if v), [liveform.Parameter('signup-' + str(i), liveform.CHECKBOX_INPUT, lambda wasSelected, tracker=tracker: (tracker, wasSelected), repr(tracker.signupItem)) for (i, tracker) in enumerate(existing)], description='Delete Existing Signups') deleteSignupForm.setFragmentParent(self) else: deleteSignupForm = '' productPicker = self.makeProductPicker() createSignupForm = liveform.LiveForm( self.createSignup, [liveform.Parameter('signupPrompt', liveform.TEXT_INPUT, unicode, u'Descriptive, user-facing prompt for this signup', default=u'Sign Up'), liveform.Parameter('product', liveform.FORM_INPUT, productPicker, u'Pick some product'), liveform.Parameter('signupTuple', liveform.FORM_INPUT, signupMechanismConfigurations, u'Pick just one dude'), liveform.Parameter('emailTemplate', liveform.FORM_INPUT, emailTemplateConfiguration, u'You know you want to')], description='Create Signup') createSignupForm.setFragmentParent(self) return [deleteSignupForm, createSignupForm] def data_configuredSignupMechanisms(self, ctx, data): for _signupTracker in self.original.store.parent.query(_SignupTracker): yield { 'typeName': _signupTracker.signupItem.__class__.__name__, 'createdBy': _signupTracker.createdBy, 'createdOn': _signupTracker.createdOn.asHumanly()} def createSignup(self, signupPrompt, signupTuple, product, emailTemplate): """ @param signupPrompt: a short unicode string describing this new signup mechanism to disambiguate it from others. For example: "sign up". @param signupTuple: a 2-tuple of (signupMechanism, signupConfig), """ (signupMechanism, signupConfig) = signupTuple t = self.original.store.transact t(self.original.createSignup, self.page.username, signupMechanism, signupConfig, product, emailTemplate, signupPrompt) return u'Great job.' expose(createSignup) def _deleteTrackers(self, trackers): """ Delete the given signup trackers and their associated signup resources. @param trackers: sequence of L{_SignupTrackers} """ for tracker in trackers: if tracker.store is None: # we're not updating the list of live signups client side, so # we might get a signup that has already been deleted continue sig = tracker.signupItem # XXX the only reason we're doing this here is that we're afraid to # add a whenDeleted=CASCADE to powerups because it's inefficient, # however, this is arguably the archetypical use of # whenDeleted=CASCADE. Soon we need to figure out a real solution # (but I have no idea what it is). -glyph for iface in sig.store.interfacesFor(sig): sig.store.powerDown(sig, iface) tracker.deleteFromStore() sig.deleteFromStore() registerAdapter(SignupFragment, SignupConfiguration, INavigableFragment) PK�����9FŊ}P��P�����xmantissa/smtp.py# -*- test-case-name: xmantissa.test.test_smtp -*- """ An RFC 2821 address parser. """ import inspect, re from twisted.internet import address from xmantissa.error import ( AddressTooLong, InvalidAddress, InvalidTrailingBytes) class Address(object): """ An RFC 2821 path. @ivar route: C{None} or a C{list} of C{str} giving the source-specified route of this message. This is obsolete and should not be respected, but is made available for completeness. @ivar localpart: C{None} or a C{str} giving the local part of this address. @ivar domain: C{None} or a C{str}, L{IPv4Address} or L{IPv6Address} giving the domain part of this address. """ route = localpart = domain = None def __init__(self, route, localpart, domain): assert (localpart is None) == (domain is None) self.route = route self.localpart = localpart self.domain = domain def __eq__(self, other): """ Compare this address to another for equality. Two addresses are equal if their route, localpart, and domain attributes are equal. Comparison against non-Address objects is unimplemented. """ if isinstance(other, Address): a = (self.route, self.localpart, self.domain) b = (other.route, other.localpart, other.domain) return a == b return NotImplemented def __ne__(self, other): """ Compare this address to another for inequality. See L{__eq__}. """ result = self.__eq__(other) if result is NotImplemented: return result return not result def __str__(self): """ Format this address as an RFC 2821 path string. """ mailbox = '' if self.localpart is not None: mailbox = '%s@%s' % (self.localpart, self.domain) route = '' if self.route is not None: route = '@' + ',@'.join(self.route) + ':' return '<%s%s>' % (route, mailbox) def __repr__(self): return 'Address(%r, %r, %r)' % (self.route, self.localpart, self.domain) class _AddressParser(object): def f(s): d = inspect.currentframe().f_back.f_locals return "(?:" + (s % d) + ")" alpha = 'a-zA-Z' digit = '0-9' hexdigit = 'a-fA-F0-9' letDig = f(r"[%(alpha)s%(digit)s]") ldhStr = f(r"[%(alpha)s%(digit)s-]*%(letDig)s") sNum = f(r"[%(digit)s]{1,3}") ipv4AddressLiteral = f(r"%(sNum)s(?:\.%(sNum)s){3}") ipv6Hex = f(r"[%(hexdigit)s]") ipv6Full = f(r"%(ipv6Hex)s(?::%(ipv6Hex)s){7}") ipv6Comp = f(r"(?:%(ipv6Hex)s(?::%(ipv6Hex)s){0,5})?::(?:%(ipv6Hex)s(?::%(ipv6Hex)s){0,5})?") ipv6v4Full = f(r"%(ipv6Hex)s(?::%(ipv6Hex)s){5}:%(ipv4AddressLiteral)s") ipv6v4Comp = f(r"(?:%(ipv6Hex)s(?::%(ipv6Hex)s){0,3})?::(?:%(ipv6Hex)s(?::%(ipv6Hex)s){0,3})?%(ipv4AddressLiteral)s") ipv6Address = f(r"%(ipv6Full)s|%(ipv6Comp)s|%(ipv6v4Full)s|%(ipv6v4Comp)s") ipv6AddressLiteral = f(r"IPv6:%(ipv6Address)s") standardizedTag = ldhStr noWsCtl = '\x01-\x08\x0B\x0C\x0E-\x7F' somePrintableUSAscii = '\x21-\x5A\x5E-\x7E' text = '\x01-\x09\x0B\x0C\x0E-\x7F' quotedPair = f(r"\\[%(text)s]") # XXX obs-qp dtext = f(r"[%(noWsCtl)s%(somePrintableUSAscii)s]") dcontent = f(r"%(dtext)s|%(quotedPair)s") generalAddressLiteral = f(r"%(standardizedTag)s:%(dcontent)s+") qtext = f(r"%(noWsCtl)s|[\x21\x23-\x5B\x5D-\x7F]") qcontent = f(r"%(qtext)s|%(quotedPair)s") quotedString = f(r"\"(?:%(qcontent)s)*\"") addressLiteral = f(r"%(ipv4AddressLiteral)s|%(ipv6AddressLiteral)s|%(generalAddressLiteral)s") addressLiteralNamed = f( r"(?P<ipv4Literal>%(ipv4AddressLiteral)s)|" r"(?P<ipv6Literal>%(ipv6AddressLiteral)s)|" r"(?P<generalLiteral>%(generalAddressLiteral)s)") subdomain = f(r"%(letDig)s(?:%(ldhStr)s)?") domain = f(r"(?:%(subdomain)s(?:\.%(subdomain)s)+)|%(addressLiteral)s") domainNamed = f(r"(?P<domain>(?:%(subdomain)s(?:\.%(subdomain)s)+)|%(addressLiteralNamed)s)") atDomain = f(r"@%(domain)s") atext = f(r"%(letDig)s|[!#$%%&'*+-/=?^_`{|}-]") atom = f(r"%(atext)s+") dotString = f(r"%(atom)s(\.%(atom)s)*") localPart = f(r"%(dotString)s|%(quotedString)s") adl = f(r"(?P<adl>%(atDomain)s(?:,%(atDomain)s)*)") mailbox = f(r"(?P<localPart>%(localPart)s)?(?P<at>@)%(domainNamed)s?") path = f(r"<(?:%(adl)s:)?(?:%(mailbox)s)?>") address = re.compile("^" + path + "$") def __call__(self, arglist, line): # RFC 2821 4.5.3.1 if len(line) > 256: raise AddressTooLong() match = self.address.match(line) if match is None: raise InvalidAddress() d = match.groupdict() if d['adl']: route = d['adl'][1:].split(',@') else: route = None localpart = d['localPart'] if d['domain']: domain = d['domain'] elif d['ipv4Literal']: domain = address.IPv4Address(d['ipv4Literal']) elif d['ipv6Literal']: # Chop off the leading 'IPv6:' domain = address.IPv6Address(d['ipv6Literal'][5:]) else: domain = d['generalLiteral'] at = d['at'] if at: localpart = localpart or '' domain = domain or '' arglist.append(Address(route, localpart, domain)) return match.end() def parseAddress(address): """ Parse the given RFC 2821 email address into a structured object. @type address: C{str} @param address: The address to parse. @rtype: L{Address} @raise xmantissa.error.ArgumentError: The given string was not a valid RFC 2821 address. """ parts = [] parser = _AddressParser() end = parser(parts, address) if end != len(address): raise InvalidTrailingBytes() return parts[0] PK�����9FZKQ#��#�����xmantissa/stats.py# -*- test-case-name: xmantissa.test.test_stats -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Statistics collection and recording facility. This is a system for sending locally recorded stats data to remote observers for further processing or storage. Example:: twisted.python.log.msg(interface=axiom.iaxiom.IStatEvent, foo=1, bar=2, ...) """ from zope.interface import implements from twisted.application import service from twisted.protocols import policies from twisted.python import log from twisted.protocols.amp import AMP, Command, AmpList, Unicode from axiom import iaxiom, item, attributes, upgrade from xmantissa.ixmantissa import IBoxReceiverFactory class StatUpdate(Command): """ L{StatUpdate} is sent by the server to clients connected to it each time a stat log event is observed. The command contains the information from the event. """ requiresAnswer = False arguments = [ ('data', AmpList([("key", Unicode()), ("value", Unicode())])), ] class RemoteStatsCollectorFactory(item.Item): """ A collector of stats for things that are remote (factory). """ powerupInterfaces = (IBoxReceiverFactory,) implements(*powerupInterfaces) protocol = u"http://divmod.org/ns/mantissa-stats" _unused = attributes.integer( doc=""" meaningless attribute, only here to satisfy Axiom requirement for at least one attribute. """) def getBoxReceiver(self): """ Create a new AMP protocol for delivering stats information to a remote process. """ return RemoteStatsCollector() class RemoteStatsCollector(AMP): """ An AMP protocol for sending stats to remote observers. """ def startReceivingBoxes(self, sender): """ Start observing log events for stat events to send. """ AMP.startReceivingBoxes(self, sender) log.addObserver(self._emit) def stopReceivingBoxes(self, reason): """ Stop observing log events. """ AMP.stopReceivingBoxes(self, reason) log.removeObserver(self._emit) def _emit(self, event): """ If the given event is a stat event, send a I{StatUpdate} command. """ if (event.get('interface') is not iaxiom.IStatEvent and 'athena_send_messages' not in event and 'athena_received_messages' not in event): return out = [] for k, v in event.iteritems(): if k in ('system', 'message', 'interface', 'isError'): continue if not isinstance(v, unicode): v = str(v).decode('ascii') out.append(dict(key=k.decode('ascii'), value=v)) self.callRemote(StatUpdate, data=out) class BandwidthMeasuringProtocol(policies.ProtocolWrapper): """ Wraps a Protocol and sends bandwidth stats to a BandwidthMeasuringFactory. """ def write(self, data): self.factory.registerWritten(len(data)) policies.ProtocolWrapper.write(self, data) def writeSequence(self, seq): self.factory.registerWritten(sum(map(len, seq))) policies.ProtocolWrapper.writeSequence(self, seq) def dataReceived(self, data): self.factory.registerRead(len(data)) policies.ProtocolWrapper.dataReceived(self, data) class BandwidthMeasuringFactory(policies.WrappingFactory): """ Collects stats on the number of bytes written and read by protocol instances from the wrapped factory. """ protocol = BandwidthMeasuringProtocol def __init__(self, wrappedFactory, protocolName): policies.WrappingFactory.__init__(self, wrappedFactory) self.name = protocolName def registerWritten(self, length): log.msg(interface=iaxiom.IStatEvent, **{"stat_bandwidth_" + self.name + "_up": length}) def registerRead(self, length): log.msg(interface=iaxiom.IStatEvent, **{"stat_bandwidth_" + self.name + "_down": length}) __all__ = [ 'StatUpdate', 'RemoteStatsCollectorFactory', 'RemoteStatsCollector', ] # Older stuff, mostly deprecated or not useful, you probably don't want to look # below here. -exarkun statDescriptions = { "page_renders": "Nevow page renders per minute", "messages_grabbed": "POP3 messages grabbed per minute", "messagesSent": "SMTP messages sent per minute", "messagesReceived": "SMTP messages received per minute", "mimePartsCreated": "MIME parts created per minute", "cache_hits": "Axiom cache hits per minute", "cursor_execute_time": "Seconds spent in cursor.execute per minute", "cursor_blocked_time": ("Seconds spent waiting for the database lock per " "minute"), "commits": "Axiom commits per minute", "cache_misses": "Axiom cache misses per minute", "autocommits": "Axiom autocommits per minute", "athena_messages_sent": "Athena messages sent per minute", "athena_messages_received": "Athena messages received per minute", "actionDuration": "Seconds/Minute spent executing Imaginary Commands", "actionExecuted": "Imaginary Commands/Minute executed", "bandwidth_http_up": "HTTP KB/sec received", "bandwidth_http_down": "HTTP KB/sec sent", "bandwidth_https_up": "HTTPS KB/sec sent", "bandwidth_https_down": "HTTPS KB/sec received", "bandwidth_pop3_up": "POP3 server KB/sec sent", "bandwidth_pop3_down":"POP3 server KB/sec received", "bandwidth_pop3s_up":"POP3S server KB/sec sent", "bandwidth_pop3s_down": "POP3S server KB/sec received", "bandwidth_smtp_up": "SMTP server KB/sec sent", "bandwidth_smtp_down": "SMTP server KB/sec received", "bandwidth_smtps_up": "SMTPS server KB/sec sent", "bandwidth_smtps_down": "SMTPS server KB/sec received", "bandwidth_pop3-grabber_up": "POP3 grabber KB/sec sent", "bandwidth_pop3-grabber_down": "POP3 grabber KB/sec received", "bandwidth_sip_up": "SIP KB/sec sent", "bandwidth_sip_down": "SIP KB/sec received", "bandwidth_telnet_up": "Telnet KB/sec sent", "bandwidth_telnet_down": "Telnet KB/sec received", "bandwidth_ssh_up": "SSH KB/sec sent", "bandwidth_ssh_down": "SSH KB/sec received", "Imaginary logins": "Imaginary Logins/Minute", "Web logins": "Web Logins/Minute", "SMTP logins": "SMTP Logins/Minute", "POP3 logins": "POP3 Logins/Minute", } class StatBucket(item.Item): """ Obsolete. Only present for schema compatibility. Do not use. """ schemaVersion = 2 type = attributes.text(doc="A stat name, such as 'messagesReceived'") value = attributes.ieee754_double(default=0.0, doc='Total number of events for this time period') interval = attributes.text(doc='A time period, e.g. "quarter-hour" or "minute" or "day"') index = attributes.integer(doc='The position in the round-robin list for non-daily stats') time = attributes.timestamp(doc='When this bucket was last updated') attributes.compoundIndex(interval, type, index) attributes.compoundIndex(index, interval) class QueryStatBucket(item.Item): """ Obsolete. Only present for schema compatibility. Do not use. """ type = attributes.text("the SQL query string") value = attributes.ieee754_double(default=0.0, doc='Total number of events for this time period') interval = attributes.text(doc='A time period, e.g. "quarter-hour" or "minute" or "day"') index = attributes.integer(doc='The position in the round-robin list for non-daily stats') time = attributes.timestamp(doc='When this bucket was last updated') attributes.compoundIndex(interval, type, index) class StatSampler(item.Item): """ Obsolete. Only present for schema compatibility. Do not use. """ service = attributes.reference() def run(self): """ Obsolete. Only present to prevent errors in existing systems. Do not use. """ class StatsService(item.Item, service.Service): """ Obsolete. Only present for schema compatibility. Do not use. """ installedOn = attributes.reference() parent = attributes.inmemory() running = attributes.inmemory() name = attributes.inmemory() statoscope = attributes.inmemory() queryStatoscope = attributes.inmemory() statTypes = attributes.inmemory() currentMinuteBucket = attributes.integer(default=0) currentQuarterHourBucket = attributes.integer(default=0) observers = attributes.inmemory() loginInterfaces = attributes.inmemory() userStats = attributes.inmemory() powerupInterfaces = (service.IService,) class RemoteStatsObserver(item.Item): """ Obsolete. Only present for schema compatibility. Do not use. """ hostname = attributes.bytes(doc="A host to send stat updates to") port = attributes.integer(doc="The port to send stat updates to") protocol = attributes.inmemory(doc="The juice protocol instance to send stat updates over") def upgradeStatBucket1to2(bucket): bucket.deleteFromStore() upgrade.registerUpgrader(upgradeStatBucket1to2, 'xmantissa_stats_statbucket', 1, 2) PK�����9Fɻ �� �����xmantissa/suspension.pyfrom twisted.python.components import registerAdapter from axiom.attributes import reference from axiom.item import Item from nevow.page import Element from xmantissa.ixmantissa import INavigableElement, INavigableFragment from xmantissa.webnav import Tab from zope.interface import implements, Interface class ISuspender(Interface): """ Marker interface for suspended powerup facades. """ class SuspendedNavigableElement(Item): implements(INavigableElement, ISuspender) powerupInterfaces = (INavigableElement, ISuspender) originalNE = reference() def getTabs(self): origTabs = self.originalNE.getTabs() def proxyTabs(tabs): for tab in tabs: yield Tab(tab.name, self.storeID, tab.priority, proxyTabs(tab.children), authoritative=tab.authoritative, linkURL=tab.linkURL) return proxyTabs(origTabs) class SuspendedFragment(Element): """ Temporary account-suspended fragment. """ fragmentName = 'suspend' live = False implements(INavigableFragment) def head(self): pass registerAdapter(SuspendedFragment, SuspendedNavigableElement, INavigableFragment) def suspendJustTabProviders(installation): """ Replace INavigableElements with facades that indicate their suspension. """ if installation.suspended: raise RuntimeError("Installation already suspended") powerups = list(installation.allPowerups) for p in powerups: if INavigableElement.providedBy(p): p.store.powerDown(p, INavigableElement) sne = SuspendedNavigableElement(store=p.store, originalNE=p) p.store.powerUp(sne, INavigableElement) p.store.powerUp(sne, ISuspender) installation.suspended = True def unsuspendTabProviders(installation): """ Remove suspension facades and replace them with their originals. """ if not installation.suspended: raise RuntimeError("Installation not suspended") powerups = list(installation.allPowerups) allSNEs = list(powerups[0].store.powerupsFor(ISuspender)) for p in powerups: for sne in allSNEs: if sne.originalNE is p: p.store.powerDown(sne, INavigableElement) p.store.powerDown(sne, ISuspender) p.store.powerUp(p, INavigableElement) sne.deleteFromStore() installation.suspended = False PK�����9F-��-�����xmantissa/tdb.py# -*- test-case-name: xmantissa.test.test_tdb -*- """ This module is a deprecated version of the code present in L{xmantissa.scrolltable}. Look there instead. """ import operator import math import warnings from xmantissa.ixmantissa import IColumn from xmantissa.error import Unsortable from xmantissa.scrolltable import AttributeColumn as _STAttributeColumn class AttributeColumn(_STAttributeColumn): """ This a deprecated name for L{xmantissa.scrolltable.AttributeColumn}. Use that instead. """ def __init__(self, *a, **k): super(AttributeColumn, self).__init__(*a, **k) warnings.warn("tdb.AttributeColumn is deprecated. " "Use scrolltable.AttributeColumn instead.", DeprecationWarning, stacklevel=2) from axiom.attributes import AND from axiom.queryutil import AttributeTuple class TabularDataModel: """ I represent a window onto a query that can be paged backwards and forward, and re-sorted. @ivar pageNumber: the number of the current page. @ivar totalPages: the total number of pages accessible to the query this table is browsing. @ivar firstItem: the first visible item @ivar lastItem: the last visible item @ivar totalItems: the total number of items accessible to the query this table is browsing. @ivar absolute: whether this pagination result is definitely correct. in some cases it may be necessary to approximate or estimate the total number of results, and the UI should reflect this. True if the number is definitely correct, False otherwise. """ def __init__(self, store, primaryTableClass, columns, baseComparison=None, itemsPerPage=20, defaultSortColumn=None, defaultSortAscending=True): """@param columns: sequence of objects adaptable to L{xmantissa.ixmantissa.IColumn}""" self.store = store self._currentResults = [] self.primaryTableClass = primaryTableClass assert columns, "You've got to pass some columns" cols = self.columns = {} for col in map(IColumn, columns): if defaultSortColumn is None: defaultSortColumn = col.attributeID cols[col.attributeID] = col self.itemsPerPage = itemsPerPage self.baseComparison = baseComparison self.isAscending = self.defaultSortAscending = defaultSortAscending self.resort(defaultSortColumn, defaultSortAscending) currentSortColumn = None # this is set in __init__ by resort() # so client code will never see this value def resort(self, attributeID, isAscending=None): """Sort by one of my specified columns, identified by attributeID """ if isAscending is None: isAscending = self.defaultSortAscending newSortColumn = self.columns[attributeID] if newSortColumn.sortAttribute() is None: raise Unsortable('column %r has no sort attribute' % (attributeID,)) if self.currentSortColumn == newSortColumn: # if this query is to be re-sorted on the same column, but in the # opposite direction to our last query, then use the first item in # the result set as the marker if self.isAscending == isAscending: offset = 0 else: # otherwise use the last offset = -1 else: offset = 0 self.currentSortColumn = newSortColumn self.isAscending = isAscending self._updateResults(self._sortAttributeValue(offset), True) def currentPage(self): """ Return a sequence of mappings of attribute IDs to column values, to display to the user. nextPage/prevPage will strive never to skip items whose column values have not been returned by this method. This is best explained by a demonstration. Let's say you have a table viewing an item with attributes 'a' and 'b', like this: oid | a | b ----+---+-- 0 | 1 | 2 1 | 3 | 4 2 | 5 | 6 3 | 7 | 8 4 | 9 | 0 The table has 2 items per page. You call currentPage and receive a page which contains items oid 0 and oid 1. item oid 1 is deleted. If the next thing you do is to call nextPage, the result of currentPage following that will be items beginning with item oid 2. This is because although there are no longer enough items to populate a full page from 0-1, the user has never seen item #2 on a page, so the 'next' page from the user's point of view contains #2. If instead, at that same point, the next thing you did was to call currentPage, *then* nextPage and currentPage again, the first currentPage results would contain items #0 and #2; the following currentPage results would contain items #3 and #4. In this case, the user *has* seen #2 already, so the user expects to see the following item, not the same item again. """ self._updateResults(self._sortAttributeValue(0), equalToStart=True, refresh=True) return self._currentResults def _updateResults(self, primaryColumnStart=None, equalToStart=False, backwards=False, refresh=False): results = self._performQuery(primaryColumnStart, equalToStart, backwards) if not refresh and len(results) == 0: # If we're at the end and going forwards, or at the beginning and # going backwards, there are no more page results. In these cases # we should hang on to our previous results, because the user is # still looking at the same page, and will expect the next and # prev buttons to do the appropriate things. Realistically # speaking, it is a UI bug if this case ever occurs, since the UI # should disable the 'next' and 'previous' buttons using the # hasNextPage and hasPrevPage methods. We gracefully handle it # anyway simply because we expect multiple frontends for this # model, and multiple frontends means lots of places for bugs. self.totalItems = self.totalPages = self.pageNumber = 0 return self._currentResults = results self._paginate() def _determineQuery(self, primaryColumnStart, equalToStart, backwards, limit): sortAttribute = self.currentSortColumn.sortAttribute() switch = (self.isAscending ^ backwards) # if we were sorting ascendingly, or are moving backward in the result # set, but not both, then sort this query ascendingly # use storeID as a tiebreaker, as in _sortAttributeValue tiebreaker = sortAttribute.type.storeID if switch: sortOrder = sortAttribute.ascending, tiebreaker.ascending else: # otherwise not sortOrder = sortAttribute.descending, tiebreaker.descending # if we were passed a value to use as a marker indicating our position # in the result set (typically it would be either the first or last # item from the last query) if primaryColumnStart is not None: # if we want to set the threshold at values equal to the marker # value we were given if equalToStart: # then use <= and >= ltgt = operator.__le__, operator.__ge__ else: # otherwise < and > ltgt = operator.__lt__, operator.__gt__ # produce an axiom comparison object by comparing the sort # attribute and the marker value using the appropriate operator offsetComparison = ltgt[switch]( AttributeTuple(sortAttribute, tiebreaker), primaryColumnStart) # if we were instantiated with an additional comparison, then add # that to the comparison object also if self.baseComparison is not None: comparisonObj = AND(self.baseComparison, offsetComparison) else: comparisonObj = offsetComparison else: # If we've never loaded a page before, start at the beginning comparisonObj = self.baseComparison return self.store.query(self.primaryTableClass, comparisonObj, sort=sortOrder, limit=limit) def _performQuery(self, primaryColumnStart=None, equalToStart=False, backwards=False): q = self._determineQuery(primaryColumnStart, equalToStart, backwards, self.itemsPerPage) results = [] for eachItem in q: rowDict = dict(__item__=eachItem) for c in self.columns.itervalues(): rowDict[c.attributeID] = c.extractValue(self, eachItem) results.append(rowDict) if backwards: results.reverse() return results def _sortAttributeValue(self, offset): """ return the value of the sort attribute for the item at 'offset' in the results of the last query, otherwise None. """ if self._currentResults: pageStart = (self._currentResults[offset][ self.currentSortColumn.attributeID], self._currentResults[offset][ '__item__'].storeID) else: pageStart = None return pageStart def nextPage(self): self._updateResults(self._sortAttributeValue(-1)) def hasNextPage(self): return bool(self._performQuery(self._sortAttributeValue(-1))) def firstPage(self): self._updateResults() def prevPage(self): self._updateResults(self._sortAttributeValue(0), backwards=True) def hasPrevPage(self): return bool(self._performQuery(self._sortAttributeValue(0), backwards=True)) def lastPage(self): self._updateResults(backwards=True) def _paginate(self): rslts = self._currentResults self.totalItems = self.store.query(self.primaryTableClass, self.baseComparison).count() self.totalPages = int(math.ceil(float(self.totalItems) / self.itemsPerPage)) itemsBeforeThisPage = self._determineQuery( self._sortAttributeValue(0), equalToStart=False, backwards=True, limit=None).count() itemsAfterThisPage = self.totalItems - len(rslts) - itemsBeforeThisPage self.firstItem = itemsBeforeThisPage + 1 self.lastItem = (self.firstItem + len(rslts)) - 1 self.pageNumber = self.totalPages - ( int(math.ceil(float(itemsAfterThisPage) / self.itemsPerPage))) class PaginationParameters: absolute = True def __init__(self, tdm): self.tdm = tdm self._calculate() def _calculate(self): # XXX TODO: optimize the crap out of this by pushing its implementation # into nextPage/prevPage etc. import pprint pprint.pprint(self.__dict__) PK�����9Fۭ!,��,�����xmantissa/tdbview.pyfrom zope.interface import implements from nevow import tags, athena, flat, loaders from nevow.athena import expose from formless.annotate import nameToLabel from xmantissa.fragmentutils import PatternDictionary, dictFillSlots from xmantissa import ixmantissa # review the need to pass around instances of columns, # rather than classes, same for actions also class ColumnViewBase(object): def __init__(self, attributeID, displayName=None, width=None, typeHint=None): """@param typeHint: text|datetime|action or None""" self.attributeID = attributeID if displayName is None: displayName = nameToLabel(attributeID) self.displayName = displayName self.width = width self.typeHint = typeHint def stanFromValue(self, idx, item, value): # called with the result of extractValue return unicode(value) def getWidth(self): if self.width is None: return '' else: return self.width def onclick(self, idx, item, value): return None class EditableColumnView(ColumnViewBase): """Column View that can be edited within the table.""" def __init__(self, attributeID, displayName=None, width=None, typeHint=None, coercer=unicode, getter=None, setter=None, **kw): super(EditableColumnView, self).__init__(attributeID=attributeID, displayName=displayName, width=width, typeHint=typeHint, **kw) if getter is not None: self.getter = getter if setter is not None: self.setter = setter def stanFromValue(self, idx, item, value): return EditableColumnField(self, idx, item, value) # custom getter/setter can be given as kwargs to override default def setter(self, item, value): setattr(item, self.attributeID, value) def getter(self, item): return getattr(item, self.attributeID) class EditableColumnField(athena.LiveFragment): """Renders the input field in the table cell to edit data in-place.""" docFactory = loaders.stan( tags.span(render=tags.directive('liveFragment'))[ tags.span(render=tags.directive('field')) ]) def __init__(self, original, idx, item, value, *a, **kw): athena.LiveFragment.__init__(self, original, *a, **kw) self.idx = idx self.item = item self.value = value def render_field(self, ctx, data): handler = "Nevow.Athena.Widget.get(this).callRemote('handleValueChange', this.value)" t = tags.input(type="text", onchange=handler, value=self.original.getter(self.item)) return t def handleValueChange(self, value): return self.original.setter(self.item, self.original.coercer(value)) expose(handleValueChange) class DateColumnView(ColumnViewBase): def __init__(self, attributeID, displayName=None, width=None, typeHint='datetime'): ColumnViewBase.__init__(self, attributeID, displayName, width, typeHint) def stanFromValue(self, idx, item, value): # XXX timezones return value.asHumanly() class ActionsColumnView(ColumnViewBase): def __init__(self, actions, width=None, typeHint='actions'): ColumnViewBase.__init__(self, 'Actions', width=width, typeHint=typeHint) self.actions = actions def stanFromValue(self, idx, item, value): # Value will generally be 'None' in this case... tag = tags.div() for action in self.actions: actionable = action.actionable(item) if actionable: iconURL = action.iconURL else: iconURL = action.disabledIconURL stan = tags.img(src=iconURL, **{'class' : 'tdb-action'}) if actionable: linkstan = action.toLinkStan(idx, item) if linkstan is None: handler = 'Mantissa.TDB.Controller.get(this).performAction(%r, %r); return false' handler %= (action.actionID, idx) stan = tags.a(href='#', onclick=handler)[stan] else: stan = linkstan tag[stan] # at some point give the javascript the description to show # or something return tag class Action(object): def __init__(self, actionID, iconURL, description, disabledIconURL=None): self.actionID = actionID self.iconURL = iconURL self.disabledIconURL = disabledIconURL self.description = description def performOn(self, item): """perform this action on the given item, returning None or a completion message that might be useful to the originator of the action""" raise NotImplementedError() def actionable(self, item): """return a boolean indicating whether it makes sense to perform this action on the given item""" raise NotImplementedError() def toLinkStan(self, idx, item): return None class ToggleAction(Action): def actionable(self, item): return True def isOn(self, idx, item): raise NotImplementedError() def toLinkStan(self, idx, item): handler = 'Mantissa.TDB.Controller.get(this).performAction(%r, %r); return false' handler %= (self.actionID, idx) iconURL = (self.disabledIconURL, self.iconURL)[self.isOn(idx, item)] img = tags.img(src=iconURL, **{'class': 'tdb-action'}) return tags.a(href='#', onclick=handler)[img] class TabularDataView(athena.LiveFragment): implements(ixmantissa.INavigableFragment) jsClass = u'Mantissa.TDB.Controller' fragmentName = 'tdb' live = 'athena' title = '' patterns = None def __init__(self, model, columnViews, actions=(), width='', itemsCalled='Items'): """ @param itemsCalled: something describing what a collection of tdb items is called, e.g. 'Phone Numbers' or 'Clicks' """ super(TabularDataView, self).__init__(model) self.columnViews = list(columnViews) if actions: self.columnViews.append(ActionsColumnView(actions)) self.actions = {} for action in actions: self.actions[action.actionID] = action self.width = width self.itemsCalled = itemsCalled def constructTable(self): if self.patterns is None: self.patterns = PatternDictionary(self.docFactory) modelData = self.original.currentPage() if len(modelData) == 0: return self.patterns['no-rows'].fillSlots( 'items-called', self.itemsCalled) tablePattern = self.patterns['table'] headers = self.constructColumnHeaders() rows = self.constructRows(modelData) tablePattern = tablePattern.fillSlots('column-headers', list(headers)) return tablePattern.fillSlots( 'rows', list(rows)).fillSlots('width', self.width) def constructColumnHeaders(self): for cview in self.columnViews: model = self.original column = model.columns.get(cview.attributeID) if column is None: sortable = False else: sortable = column.sortAttribute() is not None if cview.attributeID == model.currentSortColumn.attributeID: headerPatternName = ['sorted-column-header-descending', 'sorted-column-header-ascending'][model.isAscending] else: headerPatternName = ['column-header', 'sortable-column-header'][sortable] header = self.patterns[headerPatternName].fillSlots( 'name', cview.displayName).fillSlots( 'width', cview.getWidth()) if sortable: header = header.fillSlots('onclick', 'Mantissa.TDB.Controller.get(this).clickSort("%s")'% (cview.attributeID,)) yield header def constructRows(self, modelData): rowPattern = self.patterns['row'] cellPattern = self.patterns['cell'] for idx, row in enumerate(modelData): cells = [] for cview in self.columnViews: value = row.get(cview.attributeID) cellContents = cview.stanFromValue( idx, row['__item__'], value) if hasattr(cellContents, 'setFragmentParent'): cellContents.setFragmentParent(self) handler = cview.onclick(idx, row['__item__'], value) cellStan = dictFillSlots(cellPattern, {'value': cellContents, 'onclick': handler, 'class': cview.typeHint}) cells.append(cellStan) yield dictFillSlots(rowPattern, {'cells': cells, 'class': 'tdb-row-%s' % (idx,)}) def getInitialArguments(self): return self.getPageState() def render_navigation(self, ctx, data): patterns = PatternDictionary(self.docFactory) return ctx.tag[patterns['navigation']()] def render_actions(self, ctx, data): return '(Actions not yet implemented)' def render_table(self, ctx, data): return self.constructTable() def replaceTable(self): # XXX TODO: the flatten here is encoding/decoding like 4 times; this # could be a lot faster. return unicode(flat.flatten(self.constructTable()), 'utf-8'), self.getPageState() expose(replaceTable) def getPageState(self): tdm = self.original return (tdm.hasPrevPage(), tdm.hasNextPage(), tdm.pageNumber, tdm.itemsPerPage, tdm.totalItems) expose(getPageState) def nextPage(self): self.original.nextPage() return self.replaceTable() expose(nextPage) def prevPage(self): self.original.prevPage() return self.replaceTable() expose(prevPage) def firstPage(self): self.original.firstPage() return self.replaceTable() expose(firstPage) def lastPage(self): self.original.lastPage() return self.replaceTable() expose(lastPage) def itemFromTargetID(self, targetID): modelData = list(self.original.currentPage()) return modelData[targetID]['__item__'] def performAction(self, actionID, targetID): target = self.itemFromTargetID(int(targetID)) action = self.actions[actionID] result = action.performOn(target) return result, self.replaceTable() expose(performAction) def clickSort(self, attributeID): if attributeID == self.original.currentSortColumn.attributeID: self.original.resort(attributeID, not self.original.isAscending) else: self.original.resort(attributeID) self.original.firstPage() return self.replaceTable() expose(clickSort) def head(self): return None PK�����9FS2؄+��+�����xmantissa/terminal.py# -*- test-case-name: xmantissa.test.test_terminal -*- # Copyright 2009 Divmod, Inc. See LICENSE file for details """ This module provides an extensible terminal server for Mantissa, exposed over L{SSHv2<twisted.conch>}. The server supports username/password authentication and encrypted communication. It can be extended by applications which provide L{ITerminalServerFactory} powerups to create L{ITerminalProtocol} providers. """ from hashlib import md5 from Crypto.PublicKey import RSA from zope.interface import implements from twisted.python.randbytes import secureRandom from twisted.python.components import Componentized from twisted.cred.portal import IRealm, Portal from twisted.cred.checkers import ICredentialsChecker from twisted.conch.interfaces import IConchUser, ISession from twisted.conch.ssh.factory import SSHFactory from twisted.conch.ssh.keys import Key from twisted.conch.manhole_ssh import TerminalUser, TerminalSession, TerminalSessionTransport from twisted.conch.insults.insults import ServerProtocol, TerminalProtocol from twisted.conch.insults.window import TopWindow, VBox, Border, Button from twisted.conch.manhole import ColoredManhole from axiom.iaxiom import IPowerupIndirector from axiom.item import Item from axiom.attributes import bytes from axiom.dependency import dependsOn from axiom.userbase import LoginSystem, getAccountNames from xmantissa.ixmantissa import IProtocolFactoryFactory, ITerminalServerFactory from xmantissa.ixmantissa import IViewer from xmantissa.sharing import getAccountRole __metaclass__ = type def _generate(): """ Generate a new SSH key pair. """ key = RSA.generate(1024, secureRandom) return Key(key).toString('openssh') class _AuthenticatedShellViewer: """ L{_AuthenticatedShellViewer} is an L{IViewer} implementation used to indicate to shell applications which user is trying to access them. @ivar _accountNames: A list of account names associated with this viewer. This is in the same form as the return value of L{getAccountNames}. """ implements(IViewer) def __init__(self, accountNames): self._accountNames = accountNames def roleIn(self, userStore): """ Get the authenticated role for the user represented by this view in the given user store. """ return getAccountRole(userStore, self._accountNames) class SecureShellConfiguration(Item): """ Configuration object for a Mantissa SSH server. """ powerupInterfaces = (IProtocolFactoryFactory,) implements(*powerupInterfaces) loginSystem = dependsOn(LoginSystem) hostKey = bytes( doc=""" An OpenSSH-format string giving the host key for this server. """, allowNone=False, defaultFactory=_generate) def __repr__(self): """ Return a summarized representation of this item. """ fmt = "SecureShellConfiguration(storeID=%d, hostKeyFingerprint='%s')" privateKey = Key.fromString(data=self.hostKey) publicKeyBlob = privateKey.blob() fingerprint = md5(publicKeyBlob).hexdigest() return fmt % (self.storeID, fingerprint) def getFactory(self): """ Create an L{SSHFactory} which allows access to Mantissa accounts. """ privateKey = Key.fromString(data=self.hostKey) public = privateKey.public() factory = SSHFactory() factory.publicKeys = {'ssh-rsa': public} factory.privateKeys = {'ssh-rsa': privateKey} factory.portal = Portal( IRealm(self.store), [ICredentialsChecker(self.store)]) return factory class _ReturnToMenuWrapper: """ L{ITerminalTransport} wrapper which changes only the behavior of the C{loseConnection} method. Rather than allowing the terminal to be disconnected, a L{ShellServer} is re-activated. @ivar _shell: The L{ShellServer} to re-activate when disconnection is attempted. @ivar _terminal: The L{ITerminalTransport} to which to proxy all attribute lookups other than C{loseConnection}. """ def __init__(self, shell, terminal): self._shell = shell self._terminal = terminal def loseConnection(self): self._shell.reactivate() def __getattr__(self, name): return getattr(self._terminal, name) class ShellServer(TerminalProtocol): """ A terminal protocol which finds L{ITerminalServerFactory} powerups in the same store and presents the option of beginning a session with one of them. @ivar _store: The L{Store} which will be searched for L{ITerminalServerFactory} powerups. @ivar _protocol: If an L{ITerminalServerFactory} has been selected to interact with, then this attribute refers to the L{ITerminalProtocol} produced by that factory's C{buildTerminalProtocol} method. Input from the terminal is delivered to this protocol. This attribute is C{None} whenever the "main menu" user interface is being displayed. @ivar _window: A L{TopWindow} instance which contains the "main menu" user interface. Whenever the C{_protocol} attribute is C{None}, input is directed to this object instead. Whenever the C{_protocol} attribute is not C{None}, this window is hidden. """ _width = 80 _height = 24 _protocol = None def __init__(self, store): TerminalProtocol.__init__(self) self._store = store def _draw(self): """ Call the drawing API for the main menu widget with the current known terminal size and the terminal. """ self._window.draw(self._width, self._height, self.terminal) def _appButtons(self): for factory in self._store.powerupsFor(ITerminalServerFactory): yield Button( factory.name.encode('utf-8'), lambda factory=factory: self.switchTo(factory)) def _logoffButton(self): return Button("logoff", self.logoff) def _makeWindow(self): buttons = VBox() for button in self._appButtons(): buttons.addChild(Border(button)) buttons.addChild(Border(self._logoffButton())) from twisted.internet import reactor window = TopWindow(self._draw, lambda f: reactor.callLater(0, f)) window.addChild(Border(buttons)) return window def connectionMade(self): """ Reset the terminal and create a UI for selecting an application to use. """ self.terminal.reset() self._window = self._makeWindow() def reactivate(self): """ Called when a sub-protocol is finished. This disconnects the sub-protocol and redraws the main menu UI. """ self._protocol.connectionLost(None) self._protocol = None self.terminal.reset() self._window.filthy() self._window.repaint() def switchTo(self, app): """ Use the given L{ITerminalServerFactory} to create a new L{ITerminalProtocol} and connect it to C{self.terminal} (such that it cannot actually disconnect, but can do most anything else). Control of the terminal is delegated to it until it gives up that control by disconnecting itself from the terminal. @type app: L{ITerminalServerFactory} provider @param app: The factory which will be used to create a protocol instance. """ viewer = _AuthenticatedShellViewer(list(getAccountNames(self._store))) self._protocol = app.buildTerminalProtocol(viewer) self._protocol.makeConnection(_ReturnToMenuWrapper(self, self.terminal)) def keystrokeReceived(self, keyID, modifier): """ Forward input events to the application-supplied protocol if one is currently active, otherwise forward them to the main menu UI. """ if self._protocol is not None: self._protocol.keystrokeReceived(keyID, modifier) else: self._window.keystrokeReceived(keyID, modifier) def logoff(self): """ Disconnect from the terminal completely. """ self.terminal.loseConnection() class _BetterTerminalSession(TerminalSession): """ L{TerminalSession} is missing C{windowChanged} and C{eofReceived} for some reason. Add it here until it's fixed in Twisted. See Twisted ticket #3303. """ def windowChanged(self, newWindowSize): """ Ignore window size change events. """ def eofReceived(self): """ Ignore the eof event. """ class _BetterTerminalUser(TerminalUser): """ L{TerminalUser} is missing C{conn} for some reason reason (probably the reason that it's not a very great thing and generally an implementation will be missing it for a while). Add it here until it's fixed in Twisted. See Twisted ticket #3863. """ # Some code in conch will rudely rebind this attribute later. For now, # make sure that it is at least bound to something so that the object # appears to fully implement IConchUser. Most likely, TerminalUser should # be taking care of this, not us. Or even better, this attribute shouldn't # be part of the interface; some better means should be provided for # informing the IConchUser avatar of the connection object (I'm not even # sure why the avatar would care about having a reference to the connection # object). conn = None class ShellAccount(Item): """ Axiom cred hook for creating SSH avatars. """ powerupInterfaces = (IConchUser,) implements(IPowerupIndirector) garbage = bytes() def indirect(self, interface): """ Create an L{IConchUser} avatar which will use L{ShellServer} to interact with the connection. """ if interface is IConchUser: componentized = Componentized() user = _BetterTerminalUser(componentized, None) session = _BetterTerminalSession(componentized) session.transportFactory = TerminalSessionTransport session.chainedProtocolFactory = lambda: ServerProtocol(ShellServer, self.store) componentized.setComponent(IConchUser, user) componentized.setComponent(ISession, session) return user raise NotImplementedError(interface) class TerminalManhole(Item): """ A terminal application which presents an interactive Python session running in the primary Mantissa server process. """ powerupInterfaces = (ITerminalServerFactory,) implements(*powerupInterfaces) shell = dependsOn(ShellAccount) name = 'manhole' def buildTerminalProtocol(self, shellViewer): """ Create and return a L{ColoredManhole} which includes this item's store in its namespace. """ return ColoredManhole({'db': self.store, 'viewer': shellViewer}) __all__ = ['SecureShellConfiguration', 'ShellAccount', 'TerminalManhole'] PK�����9F%YI:��I:�����xmantissa/web.py# -*- test-case-name: xmantissa.test.test_website -*- # Copyright 2005 Divmod, Inc. # See LICENSE file for details """ Mantissa web presence. """ from zope.interface import implements from twisted.python.filepath import FilePath from twisted.internet.defer import maybeDeferred from twisted.cred.portal import Portal from twisted.cred.checkers import AllowAnonymousAccess from nevow.inevow import IRequest, IResource from nevow.url import URL from nevow.appserver import NevowSite, NevowRequest from nevow.rend import NotFound from nevow.static import File from nevow.athena import LivePage from epsilon.structlike import record from axiom.item import Item from axiom.attributes import path, text from axiom.dependency import dependsOn from axiom.userbase import LoginSystem from xmantissa.ixmantissa import ISiteURLGenerator, IProtocolFactoryFactory, IOfferingTechnician, ISessionlessSiteRootPlugin from xmantissa.port import TCPPort, SSLPort from xmantissa.cachejs import theHashModuleProvider from xmantissa.websession import PersistentSessionWrapper class AxiomRequest(NevowRequest): def __init__(self, store, *a, **kw): NevowRequest.__init__(self, *a, **kw) self.store = store def process(self, *a, **kw): return self.store.transact(NevowRequest.process, self, *a, **kw) class AxiomSite(NevowSite): def __init__(self, store, *a, **kw): NevowSite.__init__(self, *a, **kw) self.store = store self.requestFactory = lambda *a, **kw: AxiomRequest(self.store, *a, **kw) class SiteConfiguration(Item): """ Configuration object for a Mantissa HTTP server. """ powerupInterfaces = (ISiteURLGenerator, IProtocolFactoryFactory) implements(*powerupInterfaces) loginSystem = dependsOn(LoginSystem) # I don't really want this to have a default value at all, but an Item # which can't be instantiated with only a store parameter can't be used as # a siteRequirement in an Offering. See #538 about offering configuration. # -exarkun hostname = text( doc=""" The primary hostname by which this website will be accessible. This will be superceded by a one-to-many relationship in the future, allowing a host to have multiple recognized hostnames. See #2501. """, allowNone=False, default=u"localhost") httpLog = path(default=None) def _root(self, scheme, hostname, portObj, standardPort): # TODO - real unicode support (but punycode is so bad) if portObj is None: return None portNumber = portObj.portNumber port = portObj.listeningPort if hostname is None: hostname = self.hostname else: hostname = hostname.split(':')[0].encode('ascii') if portNumber == 0: if port is None: return None else: portNumber = port.getHost().port # At some future point, we may want to make pathsegs persistently # configurable - perhaps scheme and hostname as well - in order to # easily support reverse proxying configurations, particularly where # Mantissa is being "mounted" somewhere other than /. See also rootURL # which has some overlap with this method (the difference being # universal vs absolute URLs - rootURL may want to call cleartextRoot # or encryptedRoot in the future). See #417 and #2309. pathsegs = [''] if portNumber != standardPort: hostname = '%s:%d' % (hostname, portNumber) return URL(scheme, hostname, pathsegs) def _getPort(self, portType): return self.store.findFirst( portType, portType.factory == self, default=None) def cleartextRoot(self, hostname=None): """ Return a string representing the HTTP URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of the C{hostname} attribute of this item. """ return self._root('http', hostname, self._getPort(TCPPort), 80) def encryptedRoot(self, hostname=None): """ Return a string representing the HTTPS URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of the C{hostname} attribute of this item. """ return self._root('https', hostname, self._getPort(SSLPort), 443) def rootURL(self, request): """ Return the URL for the root of this website which is appropriate to use in links generated in response to the given request. @type request: L{twisted.web.http.Request} @param request: The request which is being responded to. @rtype: L{URL} @return: The location at which the root of the resource hierarchy for this website is available. """ host = request.getHeader('host') or self.hostname if ':' in host: host = host.split(':', 1)[0] if (host == self.hostname or host.startswith('www.') and host[len('www.'):] == self.hostname): return URL(scheme='', netloc='', pathsegs=['']) else: if request.isSecure(): return self.encryptedRoot(self.hostname) else: return self.cleartextRoot(self.hostname) def getFactory(self): """ Create an L{AxiomSite} which supports authenticated and anonymous access. """ checkers = [self.loginSystem, AllowAnonymousAccess()] guardedRoot = PersistentSessionWrapper( self.store, Portal(self.loginSystem, checkers), domains=[self.hostname]) unguardedRoot = UnguardedWrapper(self.store, guardedRoot) securingRoot = SecuringWrapper(self, unguardedRoot) logPath = None if self.httpLog is not None: logPath = self.httpLog.path return AxiomSite(self.store, securingRoot, logPath=logPath) class UnguardedWrapper(record('siteStore guardedRoot')): """ Resource which wraps the top of the Mantissa resource hierarchy and adds resources which must be available to all clients all the time. @ivar siteStore: The site L{Store} for the resource hierarchy being wrapped. @ivar guardedRoot: The root resource of the hierarchy being wrapped. """ implements(IResource) def child_live(self, ctx): """ The 'live' namespace is reserved for Athena LivePages. By default in Athena applications these resources are child resources of whatever URL the live page ends up at, but this root URL is provided so that the reliable message queuing logic can sidestep all resource traversal, and therefore, all database queries. This is an important optimization, since Athena's implementation assumes that HTTP hits to the message queue resource are cheap. @return: an L{athena.LivePage} instance. """ return LivePage(None, None) def child___jsmodule__(self, ignored): """ __jsmodule__ child which provides support for Athena applications to use a centralized URL to deploy JavaScript code. """ return theHashModuleProvider def child_Mantissa(self, ctx): """ Serve files from C{xmantissa/static/} at the URL C{/Mantissa}. """ # Cheating! It *looks* like there's an app store, but there isn't # really, because this is the One Store To Bind Them All. # We shouldn't really cheat here. It would be better to have one real # Mantissa offering that has its static content served up the same way # every other offering's content is served. There's already a # /static/mantissa-static/. This child definition is only still here # because some things still reference this URL. For example, # JavaScript files and any CSS file which uses Mantissa content but is # from an Offering which does not provide a staticContentPath. # See #2469. -exarkun return File(FilePath(__file__).sibling("static").path) def child_static(self, context): """ Serve a container page for static content for Mantissa and other offerings. """ offeringTech = IOfferingTechnician(self.siteStore) installedOfferings = offeringTech.getInstalledOfferings() offeringsWithContent = dict([ (offering.name, offering.staticContentPath) for offering in installedOfferings.itervalues() if offering.staticContentPath]) # If you wanted to do CSS rewriting for all CSS files served beneath # /static/, you could do it by passing a processor for ".css" here. # eg: # # website = IResource(self.store) # factory = StylesheetFactory( # offeringsWithContent.keys(), website.rootURL) # StaticContent(offeringsWithContent, { # ".css": factory.makeStylesheetResource}) return StaticContent(offeringsWithContent, {}) def locateChild(self, context, segments): """ Return a statically defined child or a child defined by a sessionless site root plugin or an avatar from guard. """ shortcut = getattr(self, 'child_' + segments[0], None) if shortcut: res = shortcut(context) if res is not None: return res, segments[1:] req = IRequest(context) for plg in self.siteStore.powerupsFor(ISessionlessSiteRootPlugin): spr = getattr(plg, 'sessionlessProduceResource', None) if spr is not None: childAndSegments = spr(req, segments) else: childAndSegments = plg.resourceFactory(segments) if childAndSegments is not None: return childAndSegments return self.guardedRoot.locateChild(context, segments) class SecuringWrapper(record('urlGenerator wrappedResource')): """ Resource wrapper which enforces HTTPS under certain circumstances. If a child resource ultimately obtained from this wrapper is going to be rendered, if it has a C{needsSecure} attribute set to a true value and there is an HTTPS server available and the request was made over HTTP, the client will be redirected to an HTTPS location first. Otherwise the resource will be rendered as usual. @ivar urlGenerator: The L{ISiteURLGenerator} which will provide an HTTPS URL. @ivar wrappedResource: The resource which will be used to locate children or which will be rendered. """ implements(IResource) def locateChild(self, context, segments): """ Unwrap the wrapped resource if HTTPS is already being used, otherwise wrap it in a helper which will preserve the wrapping all the way down to the final resource. """ request = IRequest(context) if request.isSecure(): return self.wrappedResource, segments return _SecureWrapper(self.urlGenerator, self.wrappedResource), segments def renderHTTP(self, context): """ Render the wrapped resource if HTTPS is already being used, otherwise invoke a helper which may generate a redirect. """ request = IRequest(context) if request.isSecure(): renderer = self.wrappedResource else: renderer = _SecureWrapper(self.urlGenerator, self.wrappedResource) return renderer.renderHTTP(context) class _SecureWrapper(record('urlGenerator wrappedResource')): """ Helper class for L{SecuringWrapper} which preserves wrapping to the ultimate resource and which implements HTTPS redirect logic if necessary. @ivar urlGenerator: The L{ISiteURLGenerator} which will provide an HTTPS URL. @ivar wrappedResource: The resource which will be used to locate children or which will be rendered. """ implements(IResource) def __init__(self, *a, **k): super(_SecureWrapper, self).__init__(*a, **k) self.wrappedResource = IResource(self.wrappedResource) def locateChild(self, context, segments): """ Delegate child lookup to the wrapped resource but wrap whatever results in another instance of this wrapper. """ childDeferred = maybeDeferred( self.wrappedResource.locateChild, context, segments) def childLocated((resource, segments)): if (resource, segments) == NotFound: return NotFound return _SecureWrapper(self.urlGenerator, resource), segments childDeferred.addCallback(childLocated) return childDeferred def renderHTTP(self, context): """ Check to see if the wrapped resource wants to be rendered over HTTPS and generate a redirect if this is so, if HTTPS is available, and if the request is not already over HTTPS. """ if getattr(self.wrappedResource, 'needsSecure', False): request = IRequest(context) url = self.urlGenerator.encryptedRoot() if url is not None: for seg in request.prepath: url = url.child(seg) return url return self.wrappedResource.renderHTTP(context) class StaticContent(record('staticPaths processors')): """ Parent resource for all static content provided by all installed offerings. This resource has a child by the name of each offering which declares a static content path which serves that path. @ivar staticPaths: A C{dict} mapping offering names to L{FilePath} instances for each offering which should be able to publish static content. @ivar processors: A C{dict} mapping extensions (with leading ".") to two-argument callables. These processors will be attached to the L{nevow.static.File} returned by C{locateChild}. """ implements(IResource) def locateChild(self, context, segments): """ Find the offering with the name matching the first segment and return a L{File} for its I{staticContentPath}. """ name = segments[0] try: staticContent = self.staticPaths[name] except KeyError: return NotFound else: resource = File(staticContent.path) resource.processors = self.processors return resource, segments[1:] return NotFound PK�����9Fˌs��s�����xmantissa/webadmin.py# -*- test-case-name: xmantissa.test.test_admin -*- import random, string from zope.interface import implements from twisted.python.components import registerAdapter from twisted.python.util import sibpath from twisted.python.filepath import FilePath from twisted.python import log from twisted.application.service import IService, Service from twisted.conch import manhole from twisted.cred.portal import IRealm from nevow.page import renderer from nevow.athena import expose from epsilon import extime from axiom.attributes import (integer, boolean, timestamp, bytes, reference, inmemory, AND) from axiom.item import Item, declareLegacyItem from axiom import userbase from axiom.dependency import installOn, dependsOn from axiom.upgrade import registerUpgrader from xmantissa.liveform import LiveForm, Parameter, ChoiceParameter from xmantissa.liveform import TEXT_INPUT ,CHECKBOX_INPUT from xmantissa import webtheme, liveform, webnav, offering, signup from xmantissa.port import TCPPort, SSLPort from xmantissa.product import ProductConfiguration, Product, Installation from xmantissa.suspension import suspendJustTabProviders, unsuspendTabProviders from xmantissa.tdb import AttributeColumn from xmantissa.scrolltable import ScrollingFragment from xmantissa.webapp import PrivateApplication from xmantissa.website import WebSite, PrefixURLMixin from xmantissa.terminal import TerminalManhole from xmantissa.ixmantissa import ( INavigableElement, INavigableFragment, ISessionlessSiteRootPlugin, IProtocolFactoryFactory) from nevow import rend, athena, static, tags as T class DeveloperSite(Item, PrefixURLMixin): """ Provides static content sessionlessly for the developer application. """ implements(ISessionlessSiteRootPlugin) typeName = 'developer_site' schemaVersion = 1 sessionless = True prefixURL = 'static/webadmin' # Counts of each kind of user. These are not maintained, they should be # upgraded away at some point. -exarkun developers = integer(default=0) administrators = integer(default=0) def createResource(self): return static.File(sibpath(__file__, 'static')) class AdminStatsApplication(Item): """ Obsolete. Only present for schema compatibility. Do not use. """ powerupInterfaces = (INavigableElement,) implements(INavigableElement) schemaVersion = 2 typeName = 'administrator_application' updateInterval = integer(default=5) privateApplication = dependsOn(PrivateApplication) def getTabs(self): return [] declareLegacyItem(AdminStatsApplication, 1, dict(updateInterval=integer(default=5))) def _adminStatsApplication1to2(old): new = old.upgradeVersion(AdminStatsApplication.typeName, 1, 2, updateInterval=old.updateInterval, privateApplication=old.store.findOrCreate(PrivateApplication)) return new registerUpgrader(_adminStatsApplication1to2, AdminStatsApplication.typeName, 1, 2) class LocalUserBrowser(Item): """ XXX I am an unfortunate necessity. This class shouldn't exist, and in fact, will be destroyed at the first possible moment. It's stateless, existing only to serve as a web lookup hook for the UserInteractionFragment view class. """ implements(INavigableElement) typeName = 'local_user_browser' schemaVersion = 2 privateApplication = dependsOn(PrivateApplication) powerupInterfaces = (INavigableElement,) def getTabs(self): return [webnav.Tab('Admin', self.storeID, 0.0, [webnav.Tab('Local Users', self.storeID, 0.1)], authoritative=False)] declareLegacyItem(LocalUserBrowser.typeName, 1, dict(garbage=integer(default=0))) def _localUserBrowser1to2(old): new = old.upgradeVersion(LocalUserBrowser.typeName, 1, 2, privateApplication=old.store.findOrCreate(PrivateApplication)) return new registerUpgrader(_localUserBrowser1to2, LocalUserBrowser.typeName, 1, 2) class UserInteractionFragment(webtheme.ThemedElement): """ Contains two other user-interface elements which allow existing users to be browsed and new users to be created, respectively. """ fragmentName = 'admin-user-interaction' def __init__(self, userBrowser): """ @param userBrowser: a LocalUserBrowser instance """ super(UserInteractionFragment, self).__init__() self.browser = userBrowser def userBrowser(self, request, tag): """ Render a TDB of local users. """ f = LocalUserBrowserFragment(self.browser) f.docFactory = webtheme.getLoader(f.fragmentName) f.setFragmentParent(self) return f renderer(userBrowser) def userCreate(self, request, tag): """ Render a form for creating new users. """ userCreator = liveform.LiveForm( self.createUser, [liveform.Parameter( "localpart", liveform.TEXT_INPUT, unicode, "localpart"), liveform.Parameter( "domain", liveform.TEXT_INPUT, unicode, "domain"), liveform.Parameter( "password", liveform.PASSWORD_INPUT, unicode, "password")]) userCreator.setFragmentParent(self) return userCreator renderer(userCreate) def createUser(self, localpart, domain, password=None): """ Create a new, blank user account with the given name and domain and, if specified, with the given password. @type localpart: C{unicode} @param localpart: The local portion of the username. ie, the C{'alice'} in C{'alice@example.com'}. @type domain: C{unicode} @param domain: The domain portion of the username. ie, the C{'example.com'} in C{'alice@example.com'}. @type password: C{unicode} or C{None} @param password: The password to associate with the new account. If C{None}, generate a new password automatically. """ loginSystem = self.browser.store.parent.findUnique(userbase.LoginSystem) if password is None: password = u''.join([random.choice(string.ascii_letters + string.digits) for i in xrange(8)]) loginSystem.addAccount(localpart, domain, password) registerAdapter(UserInteractionFragment, LocalUserBrowser, INavigableFragment) class LocalUserBrowserFragment(ScrollingFragment): jsClass = u'Mantissa.Admin.LocalUserBrowser' def __init__(self, userBrowser): ScrollingFragment.__init__(self, userBrowser.store.parent, userbase.LoginMethod, userbase.LoginMethod.domain != None, (userbase.LoginMethod.localpart, userbase.LoginMethod.domain, userbase.LoginMethod.verified), defaultSortColumn=userbase.LoginMethod.domain, defaultSortAscending=True) def linkToItem(self, item): # no IWebTranslator. better ideas? # will (localpart, domain, protocol) always be unique? return unicode(item.storeID) def itemFromLink(self, link): return self.store.getItemByID(int(link)) def doAction(self, loginMethod, actionClass): """ Show the form for the requested action. """ loginAccount = loginMethod.account return actionClass( self, loginMethod.localpart + u'@' + loginMethod.domain, loginAccount) def action_installOn(self, loginMethod): return self.doAction(loginMethod, EndowFragment) def action_uninstallFrom(self, loginMethod): return self.doAction(loginMethod, DepriveFragment) def action_suspend(self, loginMethod): return self.doAction(loginMethod, SuspendFragment) def action_unsuspend(self, loginMethod): return self.doAction(loginMethod, UnsuspendFragment) class EndowDepriveFragment(webtheme.ThemedElement): fragmentName = 'user-detail' def __init__(self, fragmentParent, username, loginAccount, which): super(EndowDepriveFragment, self).__init__(fragmentParent) self.account = loginAccount self.which = which self.username = username def _endow(self, **kw): subs = self.account.avatars.open() def endowall(): for product in kw.values(): if product is not None: getattr(product, self.which)(subs) subs.transact(endowall) def productForm(self, request, tag): """ Render a L{liveform.LiveForm} -- the main purpose of this fragment -- which will allow the administrator to endow or deprive existing users using Products. """ def makeRemover(i): def remover(s3lected): if s3lected: return self.products[i] return None return remover f = liveform.LiveForm( self._endow, [liveform.Parameter( 'products' + str(i), liveform.FORM_INPUT, liveform.LiveForm( makeRemover(i), [liveform.Parameter( 's3lected', liveform.RADIO_INPUT, bool, repr(p), )], '', ), ) for (i, p) in enumerate(self.products)], self.which.capitalize() + u' ' + self.username) f.setFragmentParent(self) return f renderer(productForm) class EndowFragment(EndowDepriveFragment): def __init__(self, fragmentParent, username, loginAccount): EndowDepriveFragment.__init__(self, fragmentParent, username, loginAccount, 'installProductOn') allProducts = list(self.account.store.query(Product)) self.products = [p for p in allProducts if not self.account.avatars.open().findUnique(Installation, Installation.types == p.types, None)] self.desc = "Install on" class DepriveFragment(EndowDepriveFragment): def __init__(self, fragmentParent, username, loginAccount): EndowDepriveFragment.__init__(self, fragmentParent, username, loginAccount, 'removeProductFrom') allProducts = list(self.account.store.query(Product)) self.products = [p for p in allProducts if self.account.avatars.open().findUnique(Installation, Installation.types == p.types, None)] self.desc = "Remove from" class SuspendFragment(EndowDepriveFragment): def __init__(self, fragmentParent, username, loginAccount): self.func = suspendJustTabProviders EndowDepriveFragment.__init__(self, fragmentParent, username, loginAccount, 'suspend') allProducts = list(self.account.store.query(Product)) self.products = [p for p in allProducts if self.account.avatars.open().findUnique(Installation, AND(Installation.types == p.types, Installation.suspended == False, ), [])] self.desc = "Suspend" def _endow(self, **kw): subs = self.account.avatars.open() def suspend(): for product in kw.values(): if product is not None: i = subs.findUnique(Installation, Installation.types == product.types, None) self.func(i) subs.transact(suspend) class UnsuspendFragment(SuspendFragment): def __init__(self, fragmentParent, username, loginAccount): self.func = unsuspendTabProviders EndowDepriveFragment.__init__(self, fragmentParent, username, loginAccount, 'unsuspend') allProducts = list(self.account.store.query(Product)) self.products = [p for p in allProducts if self.account.avatars.open().findUnique(Installation, AND(Installation.types == p.types, Installation.suspended == True), None)] self.desc = "Unsuspend" class DeveloperApplication(Item): """ """ implements(INavigableElement) schemaVersion = 2 typeName = 'developer_application' privateApplication = dependsOn(PrivateApplication) statementCount = integer(default=0) powerupInterfaces = (INavigableElement,) def deletedFromStore(self, *a, **kw): return super(DeveloperApplication, self).deletedFromStore(*a, **kw) # INavigableElement def getTabs(self): return [webnav.Tab('Admin', self.storeID, 0.0, [webnav.Tab('REPL', self.storeID, 0.0)], authoritative=False)] declareLegacyItem(DeveloperApplication.typeName, 1, dict(statementCount=integer(default=0))) def _developerApplication1to2(old): new = old.upgradeVersion(DeveloperApplication.typeName, 1, 2, statementCount=old.statementCount, privateApplication=old.store.findOrCreate(PrivateApplication)) return new registerUpgrader(_developerApplication1to2, DeveloperApplication.typeName, 1, 2) class REPL(athena.LiveFragment): """ Provides an interactive Read-Eval-Print loop. On a web page (duh). """ implements(INavigableFragment) jsClass = u'Mantissa.InterpreterWidget' fragmentName = 'admin-python-repl' live = 'athena' def __init__(self, *a, **kw): rend.Fragment.__init__(self, *a, **kw) self.namespace = {'s': self.original.store, 'getStore': self.getStore} self.interpreter = manhole.ManholeInterpreter( self, self.namespace) def getStore(self, name, domain): """Convenience method for the REPL. I got tired of typing this string every time I logged in.""" return IRealm(self.original.store.parent).accountByAddress(name, domain).avatars.open() def head(self): return () def addOutput(self, output, async=False): self.callRemote('addOutputLine', unicode(output, 'ascii')) def evaluateInputLine(self, inputLine): return self.interpreter.push(inputLine) expose(evaluateInputLine) registerAdapter(REPL, DeveloperApplication, INavigableFragment) class Traceback(Item): typeName = 'mantissa_traceback' schemaVersion = 1 when = timestamp() traceback = bytes() collector = reference() def __init__(self, store, collector, failure): when = extime.Time() traceback = failure.getTraceback() super(Traceback, self).__init__( store=store, traceback=traceback, when=when, collector=collector) class TracebackCollector(Item, Service): implements(IService) typeName = 'mantissa_traceback_collector' schemaVersion = 1 tracebackCount = integer(default=0) parent = inmemory() running = inmemory() name = inmemory() powerupInterfaces = (IService,) def installed(self): self.setServiceParent(self.store) def startService(self): log.addObserver(self.emit) def stopService(self): log.removeObserver(self.emit) def emit(self, event): if event.get('isError') and event.get('failure') is not None: f = event['failure'] def txn(): self.tracebackCount += 1 Traceback(store=self.store, collector=self, failure=f) self.store.transact(txn) def getTracebacks(self): """ Return an iterable of Tracebacks that have been collected. """ return self.store.query(Traceback, Traceback.collector == self) class TracebackViewer(Item): implements(INavigableElement) typeName = 'mantissa_tb_viewer' schemaVersion = 2 allowDeletion = boolean(default=False) privateApplication = dependsOn(PrivateApplication) powerupInterfaces = (INavigableElement,) def getTabs(self): return [webnav.Tab('Admin', self.storeID, 0.0, [webnav.Tab('Errors', self.storeID, 0.3)], authoritative=False)] def _getCollector(self): def ifCreate(coll): installOn(coll, self.store.parent) return self.store.parent.findOrCreate(TracebackCollector, ifCreate) # this needs to be moved somewhere else, topPanelContent is no more #def topPanelContent(self): # # XXX There should really be a juice protocol for this. # return '%d errors logged' % (self._getCollector().tracebackCount,) declareLegacyItem(TracebackViewer, 1, dict(allowDeletion=boolean(default=False))) def _tracebackViewer1to2(old): return old.upgradeVersion(TracebackViewer.typeName, 1, 2, allowDeletion=old.allowDeletion, privateApplication=old.store.findOrCreate(PrivateApplication)) registerUpgrader(_tracebackViewer1to2, TracebackViewer.typeName, 1, 2) class TracebackViewerFragment(rend.Fragment): implements(INavigableFragment) live = False fragmentName = 'traceback-viewer' def head(self): return () def render_tracebacks(self, ctx, data): for tb in self.original._getCollector().getTracebacks(): yield T.div[T.code[T.pre[tb.traceback]]] registerAdapter(TracebackViewerFragment, TracebackViewer, INavigableFragment) class PortConfiguration(Item): """ Marker powerup which allows those on whom it is installed to modify the configuration of listening ports in this server. """ implements(INavigableElement) powerupInterfaces = (INavigableElement,) # Only present because Axiom requires at least one attribute on an Item. garbage = integer(default=12345678653) def getTabs(self): """ Add this object to the tab navigation so it can display configuration information and allow configuration to be modified. """ return [webnav.Tab('Admin', self.storeID, 0.0, [webnav.Tab('Ports', self.storeID, 0.4)], authoritative=False)] def createPort(self, portNumber, ssl, certPath, factory, interface=u''): """ Create a new listening port. @type portNumber: C{int} @param portNumber: Port number on which to listen. @type ssl: C{bool} @param ssl: Indicates whether this should be an SSL port or not. @type certPath: C{str} @param ssl: If C{ssl} is true, a path to a certificate file somewhere within the site store's files directory. Ignored otherwise. @param factory: L{Item} which provides L{IProtocolFactoryFactory} which will be used to get a protocol factory to associate with this port. @return: C{None} """ store = self.store.parent if ssl: port = SSLPort(store=store, portNumber=portNumber, certificatePath=FilePath(certPath), factory=factory, interface=interface) else: port = TCPPort(store=store, portNumber=portNumber, factory=factory, interface=interface) installOn(port, store) class FactoryColumn(AttributeColumn): """ Display the name of the class of items referred to by a reference attribute. """ def extractValue(self, model, item): """ Get the class name of the factory referenced by a port. @param model: Either a TabularDataModel or a ScrollableView, depending on what this column is part of. @param item: A port item instance (as defined by L{xmantissa.port}). @rtype: C{unicode} @return: The name of the class of the item to which this column's attribute refers. """ factory = super(FactoryColumn, self).extractValue(model, item) return factory.__class__.__name__.decode('ascii') class CertificateColumn(AttributeColumn): """ Display a path attribute as a unicode string. """ def extractValue(self, model, item): """ Get the path referenced by this column's attribute. @param model: Either a TabularDataModel or a ScrollableView, depending on what this column is part of. @param item: A port item instance (as defined by L{xmantissa.port}). @rtype: C{unicode} """ certPath = super(CertificateColumn, self).extractValue(model, item) return certPath.path.decode('utf-8', 'replace') class PortScrollingFragment(ScrollingFragment): """ A scrolling fragment for TCPPorts and SSLPorts which knows how to link to them and how to delete them. @ivar userStore: The store of the user viewing these ports. @ivar siteStore: The site store, where TCPPorts and SSLPorts are loaded from. """ jsClass = u'Mantissa.Admin.PortBrowser' def __init__(self, userStore, portType, columns): super(PortScrollingFragment, self).__init__( userStore.parent, portType, None, columns) self.userStore = userStore self.siteStore = userStore.parent self.webTranslator = self.userStore.findUnique(PrivateApplication) def itemFromLink(self, link): """ @type link: C{unicode} @param link: A webID to translate into an item. @rtype: L{Item} @return: The item to which the given link referred. """ return self.siteStore.getItemByID(self.webTranslator.linkFrom(link)) def action_delete(self, port): """ Delete the given port. """ port.deleteFromStore() class PortConfigurationFragment(webtheme.ThemedElement): """ Provide the view for L{PortConfiguration}. Specifically, three renderers are offered: the first two, L{tcpPorts} and L{sslPorts}, add a L{PortScrollingFragment} to their tag as a child; the last, L{createPortForm} adds a L{LiveForm} for adding new ports to its tag as a child. @ivar portConf: The L{PortConfiguration} item. @ivar store: The user store. """ implements(INavigableFragment) fragmentName = 'port-configuration' def __init__(self, portConf): super(PortConfigurationFragment, self).__init__() self.portConf = portConf self.store = portConf.store def head(self): return () def tcpPorts(self, req, tag): """ Create and return a L{PortScrollingFragment} for the L{TCPPort} items in site store. """ f = PortScrollingFragment( self.store, TCPPort, (TCPPort.portNumber, TCPPort.interface, FactoryColumn(TCPPort.factory))) f.setFragmentParent(self) f.docFactory = webtheme.getLoader(f.fragmentName) return tag[f] renderer(tcpPorts) def sslPorts(self, req, tag): """ Create and return a L{PortScrollingFragment} for the L{SSLPort} items in the site store. """ f = PortScrollingFragment( self.store, SSLPort, (SSLPort.portNumber, SSLPort.interface, CertificateColumn(SSLPort.certificatePath), FactoryColumn(SSLPort.factory))) f.setFragmentParent(self) f.docFactory = webtheme.getLoader(f.fragmentName) return tag[f] renderer(sslPorts) def createPortForm(self, req, tag): """ Create and return a L{LiveForm} for adding a new L{TCPPort} or L{SSLPort} to the site store. """ def port(s): n = int(s) if n < 0 or n > 65535: raise ValueError(s) return n factories = [] for f in self.store.parent.powerupsFor(IProtocolFactoryFactory): factories.append((f.__class__.__name__.decode('ascii'), f, False)) f = LiveForm( self.portConf.createPort, [Parameter('portNumber', TEXT_INPUT, port, 'Port Number', 'Integer 0 <= n <= 65535 giving the TCP port to bind.'), Parameter('interface', TEXT_INPUT, unicode, 'Interface', 'Hostname to bind to, or blank for all interfaces.'), Parameter('ssl', CHECKBOX_INPUT, bool, 'SSL', 'Select to indicate port should use SSL.'), # Text area? File upload? What? Parameter('certPath', TEXT_INPUT, unicode, 'Certificate Path', 'Path to a certificate file on the server, if SSL is to be used.'), ChoiceParameter('factory', factories, 'Protocol Factory', 'Which pre-existing protocol factory to associate with this port.')]) f.setFragmentParent(self) # f.docFactory = webtheme.getLoader(f.fragmentName) return tag[f] renderer(createPortForm) registerAdapter(PortConfigurationFragment, PortConfiguration, INavigableFragment) class AdministrativeBenefactor(Item): typeName = 'mantissa_administrative_benefactor' schemaVersion = 1 endowed = integer(default=0) powerupNames = ["xmantissa.webadmin.AdminStatsApplication", "xmantissa.webadmin.DeveloperApplication", "xmantissa.signup.SignupConfiguration", "xmantissa.webadmin.TracebackViewer", "xmantissa.webadmin.BatchManholePowerup", "xmantissa.webadmin.LocalUserBrowser"] def endowAdminPowerups(userStore): powerups = [ # Install a web site for the individual user as well. # This is necessary because although we have a top-level # website for everybody, not all users should be allowed # to log in through the web (like UNIX's "system users", # "nobody", "database", etc.) Note, however, that there # is no port number, because the WebSite's job in this # case is to be a web *resource*, not a web *server*. WebSite, # Now we install the 'private application' plugin for # 'admin', on admin's private store, This provides the URL # "/private", but only when 'admin' is logged in. It is a # hook to hang other applications on. (XXX Rename: # PrivateApplication should probably be called # PrivateAppShell) PrivateApplication, # This is a plugin *for* the PrivateApplication; it publishes # objects via the tab-based navigation (a Python interactive # interpreter). DeveloperApplication, #ProductConfiguration lets admins collect powerups into #Products users can sign up for. ProductConfiguration, # And another one: SignupConfiguration allows the # administrator to add signup forms which grant various # kinds of account. signup.SignupConfiguration, # This one lets the administrator view unhandled # exceptions which occur in the server. TracebackViewer, # Allow the administrator to set the ports associated with # different network services. PortConfiguration, # And this one gives the administrator a page listing all # users which exist in this site's credentials database. LocalUserBrowser, # Grant Python REPL access to the server. TerminalManhole, ] for powerup in powerups: installOn(powerup(store=userStore), userStore) # This is another PrivateApplication plugin. It allows # the administrator to configure the services offered # here. oc = offering.OfferingConfiguration(store=userStore) installOn(oc, userStore) PK�����9F{Sg��g�����xmantissa/webapp.py# -*- test-case-name: xmantissa.test.test_webapp -*- """ This module is the basis for Mantissa-based web applications. It provides several basic pluggable application-level features, most notably Powerup-based integration of the extensible hierarchical navigation system defined in xmantissa.webnav """ import os from zope.interface import implements from epsilon.structlike import record from axiom.iaxiom import IPowerupIndirector from axiom.item import Item, declareLegacyItem from axiom.attributes import text, integer, reference from axiom import upgrade from axiom.dependency import dependsOn from axiom.userbase import getAccountNames from nevow.rend import Page from nevow import livepage, athena from nevow.inevow import IRequest from nevow import tags as t from nevow import url from xmantissa.publicweb import CustomizedPublicPage, renderShortUsername from xmantissa.ixmantissa import ( INavigableElement, ISiteRootPlugin, IWebTranslator, IStaticShellContent, ITemplateNameResolver, ISiteURLGenerator, IWebViewer) from xmantissa.website import PrefixURLMixin, JUST_SLASH, WebSite, APIKey from xmantissa.website import MantissaLivePage from xmantissa.webtheme import getInstalledThemes from xmantissa.webnav import getTabs, startMenu, settingsLink, applicationNavigation from xmantissa.sharing import getPrimaryRole from xmantissa._webidgen import genkey, storeIDToWebID, webIDToStoreID from xmantissa._webutil import MantissaViewHelper, WebViewerHelper from xmantissa.offering import getInstalledOfferings from xmantissa.webgestalt import AuthenticationApplication from xmantissa.prefs import PreferenceAggregator, DefaultPreferenceCollection from xmantissa.search import SearchAggregator def _reorderForPreference(themeList, preferredThemeName): """ Re-order the input themeList according to the preferred theme. Returns None. """ for theme in themeList: if preferredThemeName == theme.themeName: themeList.remove(theme) themeList.insert(0, theme) return class _WebIDFormatException(TypeError): """ An inbound web ID was not formatted as expected. """ class _AuthenticatedWebViewer(WebViewerHelper): """ Implementation of L{IWebViewer} for authenticated users. @ivar _privateApplication: the L{PrivateApplication} for the authenticated user that this view is rendering. """ implements(IWebViewer) def __init__(self, privateApp): """ @param privateApp: Probably something abstract but really it's just a L{PrivateApplication}. """ WebViewerHelper.__init__( self, privateApp.getDocFactory, privateApp._preferredThemes) self._privateApplication = privateApp # IWebViewer def roleIn(self, userStore): """ Get the authenticated role for the user represented by this view in the given user store. """ return getPrimaryRole(userStore, self._privateApplication._getUsername()) # Complete WebViewerHelper implementation def _wrapNavFrag(self, frag, useAthena): """ Wrap the given L{INavigableFragment} in an appropriate L{_FragmentWrapperMixin} subclass. """ username = self._privateApplication._getUsername() cf = getattr(frag, 'customizeFor', None) if cf is not None: frag = cf(username) if useAthena: pageClass = GenericNavigationAthenaPage else: pageClass = GenericNavigationPage return pageClass(self._privateApplication, frag, self._privateApplication.getPageComponents(), username) class _ShellRenderingMixin(object): """ View mixin for Pages which use the I{shell} template. This class provides somewhat sensible default implementations for a number of the renderers required by the I{shell} template. @ivar webapp: The L{PrivateApplication} of the user for whom this is a view. This must provide the I{rootURL} method as well as L{IWebTranslator} and L{ITemplateNameResolver}. This must be an item in a user store associated with the site store (so that the site store is available). """ fragmentName = 'main' searchPattern = None def __init__(self, webapp, pageComponents, username): self.webapp = self.translator = self.resolver = webapp self.pageComponents = pageComponents self.username = username def _siteStore(self): """ Get the site store from C{self.webapp}. """ return self.webapp.store.parent def getDocFactory(self, fragmentName, default=None): """ Retrieve a Nevow document factory for the given name. This implementation merely defers to the underlying L{PrivateApplication}. @param fragmentName: a short string that names a fragment template. @param default: value to be returned if the named template is not found. """ return self.webapp.getDocFactory(fragmentName, default) def render_content(self, ctx, data): raise NotImplementedError("implement render_context in subclasses") def render_title(self, ctx, data): return ctx.tag[self.__class__.__name__] def render_rootURL(self, ctx, data): """ Add the WebSite's root URL as a child of the given tag. The root URL is the location of the resource beneath which all standard Mantissa resources (such as the private application and static content) is available. This can be important if a page is to be served at a location which is different from the root URL in order to make links in static XHTML templates resolve correctly (for example, by adding this value as the href of a <base> tag). """ site = ISiteURLGenerator(self._siteStore()) return ctx.tag[site.rootURL(IRequest(ctx))] def render_head(self, ctx, data): return ctx.tag def render_header(self, ctx, data): staticShellContent = self.pageComponents.staticShellContent if staticShellContent is None: return ctx.tag header = staticShellContent.getHeader() if header is not None: return ctx.tag[header] else: return ctx.tag def render_startmenu(self, ctx, data): """ Add start-menu style navigation to the given tag. @see {xmantissa.webnav.startMenu} """ return startMenu( self.translator, self.pageComponents.navigation, ctx.tag) def render_settingsLink(self, ctx, data): """ Add the URL of the settings page to the given tag. @see L{xmantissa.webnav.settingsLink} """ return settingsLink( self.translator, self.pageComponents.settings, ctx.tag) def render_applicationNavigation(self, ctx, data): """ Add primary application navigation to the given tag. @see L{xmantissa.webnav.applicationNavigation} """ return applicationNavigation( ctx, self.translator, self.pageComponents.navigation) def render_urchin(self, ctx, data): """ Render the code for recording Google Analytics statistics, if so configured. """ key = APIKey.getKeyForAPI(self._siteStore(), APIKey.URCHIN) if key is None: return '' return ctx.tag.fillSlots('urchin-key', key.apiKey) def render_search(self, ctx, data): searchAggregator = self.pageComponents.searchAggregator if searchAggregator is None or not searchAggregator.providers(): return '' return ctx.tag.fillSlots( 'form-action', self.translator.linkTo(searchAggregator.storeID)) def render_username(self, ctx, data): return renderShortUsername(ctx, self.username) def render_logout(self, ctx, data): return ctx.tag def render_authenticateLinks(self, ctx, data): return '' def _getVersions(self): versions = [] for (name, offering) in getInstalledOfferings(self._siteStore()).iteritems(): if offering.version is not None: v = offering.version versions.append(str(v).replace(v.package, name)) return ' '.join(versions) def render_footer(self, ctx, data): footer = [self._getVersions()] staticShellContent = self.pageComponents.staticShellContent if staticShellContent is not None: f = staticShellContent.getFooter() if f is not None: footer.append(f) return ctx.tag[footer] INSPECTROFY = os.environ.get('MANTISSA_DEV') class _FragmentWrapperMixin(MantissaViewHelper): def __init__(self, fragment, pageComponents): self.fragment = fragment fragment.page = self self.pageComponents = pageComponents def beforeRender(self, ctx): return getattr(self.fragment, 'beforeRender', lambda x: None)(ctx) def render_introspectionWidget(self, ctx, data): "Until we have eliminated everything but GenericAthenaLivePage" if INSPECTROFY: return ctx.tag['No debugging on crap-ass bad pages'] else: return '' def render_head(self, ctx, data): req = IRequest(ctx) userStore = self.webapp.store siteStore = userStore.parent site = ISiteURLGenerator(siteStore) l = self.pageComponents.themes _reorderForPreference(l, self.webapp.preferredTheme) extras = [] for theme in l: extra = theme.head(req, site) if extra is not None: extras.append(extra) headMethod = getattr(self.fragment, 'head', None) if headMethod is not None: extra = headMethod() if extra is not None: extras.append(extra) return ctx.tag[extras] def render_title(self, ctx, data): """ Return the current context tag containing C{self.fragment}'s C{title} attribute, or "Divmod". """ return ctx.tag[getattr(self.fragment, 'title', 'Divmod')] def render_content(self, ctx, data): return ctx.tag[self.fragment] class GenericNavigationPage(_FragmentWrapperMixin, Page, _ShellRenderingMixin): def __init__(self, webapp, fragment, pageComponents, username): Page.__init__(self, docFactory=webapp.getDocFactory('shell')) _ShellRenderingMixin.__init__(self, webapp, pageComponents, username) _FragmentWrapperMixin.__init__(self, fragment, pageComponents) class GenericNavigationLivePage(_FragmentWrapperMixin, livepage.LivePage, _ShellRenderingMixin): def __init__(self, webapp, fragment, pageComponents, username): livepage.LivePage.__init__(self, docFactory=webapp.getDocFactory('shell')) _ShellRenderingMixin.__init__(self, webapp, pageComponents, username) _FragmentWrapperMixin.__init__(self, fragment, pageComponents) # XXX TODO: support live nav, live fragments somehow def render_head(self, ctx, data): ctx.tag[t.invisible(render=t.directive("liveglue"))] return _FragmentWrapperMixin.render_head(self, ctx, data) def goingLive(self, ctx, client): getattr(self.fragment, 'goingLive', lambda x, y: None)(ctx, client) def locateHandler(self, ctx, path, name): handler = getattr(self.fragment, 'locateHandler', None) if handler is None: return getattr(self.fragment, 'handle_' + name) else: return handler(ctx, path, name) class GenericNavigationAthenaPage(_FragmentWrapperMixin, MantissaLivePage, _ShellRenderingMixin): """ This class provides the generic navigation elements for surrounding all pages navigated under the /private/ namespace. """ def __init__(self, webapp, fragment, pageComponents, username): """ Top-level container for Mantissa application views. @param webapp: a C{PrivateApplication}. @param fragment: The C{Element} or C{Fragment} to display as content. @param pageComponents a C{_PageComponent}. This page draws its HTML from the 'shell' template in the preferred theme for the store. If loaded in a browser that does not support Athena, the page provided by the 'athena-unsupported' template will be displayed instead. @see: L{PrivateApplication.preferredTheme} """ userStore = webapp.store siteStore = userStore.parent MantissaLivePage.__init__( self, ISiteURLGenerator(siteStore), getattr(fragment, 'iface', None), fragment, jsModuleRoot=None, docFactory=webapp.getDocFactory('shell')) _ShellRenderingMixin.__init__(self, webapp, pageComponents, username) _FragmentWrapperMixin.__init__(self, fragment, pageComponents) self.unsupportedBrowserLoader = (webapp .getDocFactory("athena-unsupported")) def beforeRender(self, ctx): """ Call the C{beforeRender} implementations on L{MantissaLivePage} and L{_FragmentWrapperMixin}. """ MantissaLivePage.beforeRender(self, ctx) return _FragmentWrapperMixin.beforeRender(self, ctx) def render_head(self, ctx, data): ctx.tag[t.invisible(render=t.directive("liveglue"))] return _FragmentWrapperMixin.render_head(self, ctx, data) def render_introspectionWidget(self, ctx, data): if INSPECTROFY: f = athena.IntrospectionFragment() f.setFragmentParent(self) return ctx.tag[f] else: return '' class _PrivateRootPage(Page, _ShellRenderingMixin): """ L{_PrivateRootPage} is the resource present for logged-in users at "/private", providing a direct interface to the objects located in the user's personal user-store. It is created by L{PrivateApplication.createResourceWith}. """ addSlash = True def __init__(self, webapp, pageComponents, username, webViewer): self.username = username self.webViewer = webViewer Page.__init__(self, docFactory=webapp.getDocFactory('shell')) _ShellRenderingMixin.__init__(self, webapp, pageComponents, username) def child_(self, ctx): navigation = self.pageComponents.navigation if not navigation: return self # /private/XXXX -> click = self.webapp.linkTo(navigation[0].storeID) return url.URL.fromContext(ctx).click(click) def render_content(self, ctx, data): return """ You have no default root page set, and no navigation plugins installed. I don't know what to do. """ def render_title(self, ctx, data): return ctx.tag['Private Root Page (You Should Not See This)'] def childFactory(self, ctx, name): """ Return a shell page wrapped around the Item model described by the webID, or return None if no such item can be found. """ try: o = self.webapp.fromWebID(name) except _WebIDFormatException: return None if o is None: return None return self.webViewer.wrapModel(o) class _PageComponents(record('navigation searchAggregator staticShellContent settings themes')): """ I encapsulate various plugin objects that have some say in determining the available functionality on a given page """ pass class PrivateApplication(Item, PrefixURLMixin): """ This is the root of a private, navigable web application. It is designed to be installed on avatar stores after installing WebSite. To plug into it, install powerups of the type INavigableElement on the user's store. Their tabs will be retrieved and items that are part of those powerups will be linked to; provide adapters for said items to either INavigableFragment or IResource. Note: IResource adapters should be used sparingly, for example, for specialized web resources which are not 'nodes' within the application; for example, that need to set a custom content/type or that should not display any navigation elements because they will be displayed only within IFRAME nodes. Do _NOT_ use IResource adapters to provide a customized look-and-feel; instead use mantissa themes. (XXX document webtheme.py more thoroughly) @ivar preferredTheme: A C{unicode} string naming the preferred theme for this application. Templates and suchlike will be looked up for this theme first. @ivar privateKey: A random integer used to deterministically but unpredictably perturb link generation to avoid being the target of XSS attacks. @ivar privateIndexPage: A reference to the Item whose IResource or INavigableFragment adapter will be displayed on login and upon viewing the 'root' page, /private/. """ implements(ISiteRootPlugin, IWebTranslator, ITemplateNameResolver, IPowerupIndirector) powerupInterfaces = (IWebTranslator, ITemplateNameResolver, IWebViewer) typeName = 'private_web_application' schemaVersion = 5 preferredTheme = text() privateKey = integer(defaultFactory=genkey) website = dependsOn(WebSite) customizedPublicPage = dependsOn(CustomizedPublicPage) authenticationApplication = dependsOn(AuthenticationApplication) preferenceAggregator = dependsOn(PreferenceAggregator) defaultPreferenceCollection = dependsOn(DefaultPreferenceCollection) searchAggregator = dependsOn(SearchAggregator) #XXX Nothing ever uses this privateIndexPage = reference() prefixURL = 'private' sessioned = True sessionless = False def getPageComponents(self): navigation = getTabs(self.store.powerupsFor(INavigableElement)) staticShellContent = IStaticShellContent(self.store, None) return _PageComponents(navigation, self.searchAggregator, staticShellContent, self.store.findFirst(PreferenceAggregator), getInstalledThemes(self.store.parent)) def _getUsername(self): """ Return a localpart@domain style string naming the owner of our store. @rtype: C{unicode} """ for (l, d) in getAccountNames(self.store): return l + u'@' + d def createResourceWith(self, webViewer): return _PrivateRootPage(self, self.getPageComponents(), self._getUsername(), webViewer) # ISiteRootPlugin def produceResource(self, req, segments, webViewer): if segments == JUST_SLASH: return self.createResourceWith(webViewer), JUST_SLASH else: return super(PrivateApplication, self).produceResource( req, segments, webViewer) # IWebTranslator def linkTo(self, obj): # currently obj must be a storeID, but other types might come eventually return '/%s/%s' % (self.prefixURL, storeIDToWebID(self.privateKey, obj)) def linkToWithActiveTab(self, childItem, parentItem): """ Return a URL which will point to the web facet of C{childItem}, with the selected nav tab being the one that represents C{parentItem} """ return self.linkTo(parentItem.storeID) + '/' + self.toWebID(childItem) def linkFrom(self, webid): return webIDToStoreID(self.privateKey, webid) def fromWebID(self, webID): storeID = self.linkFrom(webID) if storeID is None: # This is not a very good interface, but I don't want to change the # calling code right now as I'm neither confident in its test # coverage nor looking to go on a test-writing rampage through this # code for a minor fix. raise _WebIDFormatException("%r didn't look like a webID" % (webID,)) webitem = self.store.getItemByID(storeID, None) return webitem def toWebID(self, item): return storeIDToWebID(self.privateKey, item.storeID) def _preferredThemes(self): """ Return a list of themes in the order of preference that this user has selected via L{PrivateApplication.preferredTheme}. """ themes = getInstalledThemes(self.store.parent) _reorderForPreference(themes, self.preferredTheme) return themes #ITemplateNameResolver def getDocFactory(self, fragmentName, default=None): """ Retrieve a Nevow document factory for the given name. @param fragmentName: a short string that names a fragment template. @param default: value to be returned if the named template is not found. """ themes = self._preferredThemes() for t in themes: fact = t.getDocFactory(fragmentName, None) if fact is not None: return fact return default # IPowerupIndirector def indirect(self, interface): """ Indirect the implementation of L{IWebViewer} to L{_AuthenticatedWebViewer}. """ if interface == IWebViewer: return _AuthenticatedWebViewer(self) return self PrivateApplicationV2 = declareLegacyItem(PrivateApplication.typeName, 2, dict( installedOn = reference(), preferredTheme = text(), hitCount = integer(default=0), privateKey = integer(), privateIndexPage = reference(), )) PrivateApplicationV3 = declareLegacyItem(PrivateApplication.typeName, 3, dict( preferredTheme=text(), hitCount=integer(default=0), privateKey=integer(), privateIndexPage=reference(), customizedPublicPage=reference("dependsOn(CustomizedPublicPage)"), authenticationApplication=reference("dependsOn(AuthenticationApplication)"), preferenceAggregator=reference("dependsOn(PreferenceAggregator)"), defaultPreferenceCollection=reference("dependsOn(DefaultPreferenceCollection)"), searchAggregator=reference("dependsOn(SearchAggregator)"), website=reference(), )) def upgradePrivateApplication1To2(oldApp): newApp = oldApp.upgradeVersion( 'private_web_application', 1, 2, installedOn=oldApp.installedOn, preferredTheme=oldApp.preferredTheme, privateKey=oldApp.privateKey, privateIndexPage=oldApp.privateIndexPage) newApp.store.powerup(newApp.store.findOrCreate( CustomizedPublicPage), ISiteRootPlugin, -257) return newApp upgrade.registerUpgrader(upgradePrivateApplication1To2, 'private_web_application', 1, 2) def _upgradePrivateApplication2to3(old): pa = old.upgradeVersion(PrivateApplication.typeName, 2, 3, preferredTheme=old.preferredTheme, privateKey=old.privateKey, privateIndexPage=old.privateIndexPage) pa.customizedPublicPage = old.store.findOrCreate(CustomizedPublicPage) pa.authenticationApplication = old.store.findOrCreate(AuthenticationApplication) pa.preferenceAggregator = old.store.findOrCreate(PreferenceAggregator) pa.defaultPreferenceCollection = old.store.findOrCreate(DefaultPreferenceCollection) pa.searchAggregator = old.store.findOrCreate(SearchAggregator) pa.website = old.store.findOrCreate(WebSite) return pa upgrade.registerUpgrader(_upgradePrivateApplication2to3, PrivateApplication.typeName, 2, 3) def upgradePrivateApplication3to4(old): """ Upgrade L{PrivateApplication} from schema version 3 to schema version 4. Copy all existing attributes to the new version and use the L{PrivateApplication} to power up the item it is installed on for L{ITemplateNameResolver}. """ new = old.upgradeVersion( PrivateApplication.typeName, 3, 4, preferredTheme=old.preferredTheme, privateKey=old.privateKey, website=old.website, customizedPublicPage=old.customizedPublicPage, authenticationApplication=old.authenticationApplication, preferenceAggregator=old.preferenceAggregator, defaultPreferenceCollection=old.defaultPreferenceCollection, searchAggregator=old.searchAggregator) # Almost certainly this would be more correctly expressed as # installedOn(new).powerUp(...), however the 2 to 3 upgrader failed to # translate the installedOn attribute to state which installedOn can # recognize, consequently installedOn(new) will return None for an item # which was created at schema version 2 or earlier. It's not worth dealing # with this inconsistency, since PrivateApplication is always only # installed on its store. -exarkun new.store.powerUp(new, ITemplateNameResolver) return new upgrade.registerUpgrader(upgradePrivateApplication3to4, PrivateApplication.typeName, 3, 4) PrivateApplicationV4 = declareLegacyItem( 'private_web_application', 4, dict(authenticationApplication=reference(), customizedPublicPage=reference(), defaultPreferenceCollection=reference(), hitCount=integer(), preferenceAggregator=reference(), preferredTheme=text(), privateIndexPage=reference(), privateKey=integer(), searchAggregator=reference(), website=reference())) def upgradePrivateApplication4to5(old): """ Install the newly required powerup. """ new = old.upgradeVersion( PrivateApplication.typeName, 4, 5, preferredTheme=old.preferredTheme, privateKey=old.privateKey, website=old.website, customizedPublicPage=old.customizedPublicPage, authenticationApplication=old.authenticationApplication, preferenceAggregator=old.preferenceAggregator, defaultPreferenceCollection=old.defaultPreferenceCollection, searchAggregator=old.searchAggregator) new.store.powerUp(new, IWebViewer) return new upgrade.registerUpgrader(upgradePrivateApplication4to5, PrivateApplication.typeName, 4, 5) PK�����9FiE}�������xmantissa/webgestalt.py """ Account configuration and management features, via the web. This is a pitiful implementation of these concepts (hence the pitiful module name). It will be replaced by a real implementation when clustering is ready for general use. """ import pytz from zope.interface import implements from twisted.python.components import registerAdapter from twisted.internet import defer from nevow import inevow, athena from nevow.athena import expose from epsilon import extime from axiom import item, attributes, userbase from xmantissa import ixmantissa, websession class InvalidPassword(Exception): pass class NonExistentAccount(Exception): pass class NoSuchSession(Exception): pass class AuthenticationApplication(item.Item): typeName = 'mantissa_web_authentication_application' schemaVersion = 1 lastCredentialsChange = attributes.timestamp(allowNone=False) def __init__(self, **kw): if 'lastCredentialsChange' not in kw: kw['lastCredentialsChange'] = extime.Time() super(AuthenticationApplication, self).__init__(**kw) def _account(self): substore = self.store.parent.getItemByID(self.store.idInParent) for account in self.store.parent.query(userbase.LoginAccount, userbase.LoginAccount.avatars == substore): return account raise NonExistentAccount() def _username(self): for (localpart, domain) in userbase.getAccountNames(self.store): return (localpart + '@' + domain).encode('utf-8') def hasCurrentPassword(self): return defer.succeed(self._account().password is not None) def changePassword(self, oldPassword, newPassword): account = self._account() if account.password is not None and account.password != oldPassword: raise InvalidPassword() else: account.password = newPassword self.lastCredentialsChange = extime.Time() def persistentSessions(self): username = self._username() return self.store.parent.query( websession.PersistentSession, websession.PersistentSession.authenticatedAs == username) def cancelPersistentSession(self, uid): username = self._username() for sess in self.store.parent.query(websession.PersistentSession, attributes.AND(websession.PersistentSession.authenticatedAs == username, websession.PersistentSession.sessionKey == uid)): sess.deleteFromStore() break else: raise NoSuchSession() class AuthenticationFragment(athena.LiveFragment): implements(ixmantissa.INavigableFragment) fragmentName = 'authentication-configuration' live = 'athena' title = 'Change Password' jsClass = u'Mantissa.Authentication' def __init__(self, original): self.store = original.store athena.LiveFragment.__init__(self, original) def head(self): return None def render_currentPasswordField(self, ctx, data): d = self.original.hasCurrentPassword() def cb(result): if result: patName = 'current-password' else: patName = 'no-current-password' return inevow.IQ(self.docFactory).onePattern(patName) return d.addCallback(cb) def render_cancel(self, ctx, data): # XXX See previous XXX 19th handler = 'Mantissa.Authentication.get(this).cancel(%r); return false' return ctx.tag(onclick=handler % (data['session'].sessionKey,)) def changePassword(self, currentPassword, newPassword): try: self.original.changePassword(unicode(currentPassword), unicode(newPassword)) except NonExistentAccount: raise NonExistentAccount('You do not seem to exist. Password unchanged.') except InvalidPassword: raise InvalidPassword('Incorrect password! Nothing changed.') else: return u'Password Changed!' expose(changePassword) def cancel(self, uid): try: self.original.cancelPersistentSession(str(uid)) except NoSuchSession: raise NoSuchSession('That session seems to have vanished.') else: return u'Session discontinued' expose(cancel) def data_persistentSessions(self, ctx, data): zone = pytz.timezone('US/Eastern') for session in self.original.persistentSessions(): yield dict(lastUsed=session.lastUsed.asHumanly(zone) + ' ' + zone.zone, session=session) registerAdapter(AuthenticationFragment, AuthenticationApplication, ixmantissa.INavigableFragment) PK�����9F3>'��>'�����xmantissa/webnav.py# -*- test-case-name: xmantissa.test.test_webnav -*- from epsilon.structlike import record from zope.interface import implements from nevow.inevow import IQ from nevow import url from nevow.stan import NodeNotFound from xmantissa.ixmantissa import ITab from xmantissa.fragmentutils import dictFillSlots class TabMisconfiguration(Exception): def __init__(self, info, tab): Exception.__init__( self, "Inconsistent tab item factory information", info, tab) TabInfo = record('priority storeID children linkURL authoritative', authoritative=None) class Tab(object): """ Represent part or all of the layout of a single navigation tab. @ivar name: This tab's name. @type storeID: C{int} @ivar storeID: The Axiom store identifier of the Item to which the user should be directed when this tab is activated. @ivar priority: A float between 0 and 1 indicating the relative ordering of this tab amongst its peers. Higher priorities sort sooner. @ivar children: A tuple of tabs beneath this one. @ivar authoritative: A flag indicating whether this instance of the conceptual tab with this name takes precedent over any other instance of the conceptual tab with this name. It is an error for two instances of the same conceptual tab to be authoritative. @type linkURL: C{NoneType} or C{str} @ivar linkURL: If not C{None}, the location to which the user should be directed when this tab is activated. This will override whatever value is supplied for C{storeID}. """ _item = None implements(ITab) def __init__(self, name, storeID, priority, children=(), authoritative=True, linkURL=None): self.name = name self.storeID = storeID self.priority = priority self.children = tuple(children) self.authoritative = authoritative self.linkURL = linkURL def __repr__(self): return '<%s%s %r/%0.3f %r [%r]>' % (self.authoritative and '*' or '', self.__class__.__name__, self.name, self.priority, self.storeID, self.children) def __iter__(self): raise TypeError("%r are not iterable" % (self.__class__.__name__,)) def __getitem__(self, key): """Retrieve a sub-tab from this tab by name. """ tabs = [t for t in self.children if t.name == key] assert len(tabs) < 2, "children mis-specified for " + repr(self) if tabs: return tabs[0] raise KeyError(key) def pathFromItem(self, item, avatar): """ @param item: A thing that we linked to, and such. @return: a list of [child, grandchild, great-grandchild, ...] that indicates a path from me to the navigation for that item, or [] if there is no path from here to there. """ for subnav in self.children: subpath = subnav.pathFromItem(item, avatar) if subpath: subpath.insert(0, self) return subpath else: myItem = self.loadForAvatar(avatar) if myItem is item: return [self] return [] def getTabs(navElements): # XXX TODO: multiple levels of nesting, this is hard-coded to 2. # Map primary tab names to a TabInfo primary = {} # Merge tab information from all nav plugins into one big structure for plg in navElements: for tab in plg.getTabs(): if tab.name not in primary: primary[tab.name] = TabInfo( priority=tab.priority, storeID=tab.storeID, children=list(tab.children), linkURL=tab.linkURL) else: info = primary[tab.name] if info.authoritative: if tab.authoritative: raise TabMisconfiguration(info, tab) else: if tab.authoritative: info.authoritative = True info.priority = tab.priority info.storeID = tab.storeID info.linkURL = tab.linkURL info.children.extend(tab.children) # Sort the tabs and their children by their priority def key(o): return -o.priority resultTabs = [] for (name, info) in primary.iteritems(): info.children.sort(key=key) resultTabs.append( Tab(name, info.storeID, info.priority, info.children, linkURL=info.linkURL)) resultTabs.sort(key=key) return resultTabs def setTabURLs(tabs, webTranslator): """ Sets the C{linkURL} attribute on each L{Tab} instance in C{tabs} that does not already have it set @param tabs: sequence of L{Tab} instances @param webTranslator: L{xmantissa.ixmantissa.IWebTranslator} implementor @return: None """ for tab in tabs: if not tab.linkURL: tab.linkURL = webTranslator.linkTo(tab.storeID) setTabURLs(tab.children, webTranslator) def getSelectedTab(tabs, forURL): """ Returns the tab that should be selected when the current resource lives at C{forURL}. Call me after L{setTabURLs} @param tabs: sequence of L{Tab} instances @param forURL: L{nevow.url.URL} @return: L{Tab} instance """ flatTabs = [] def flatten(tabs): for t in tabs: flatTabs.append(t) flatten(t.children) flatten(tabs) forURL = '/' + forURL.path for t in flatTabs: if forURL == t.linkURL: return t flatTabs.sort(key=lambda t: len(t.linkURL), reverse=True) for t in flatTabs: if not t.linkURL.endswith('/'): linkURL = t.linkURL + '/' else: linkURL = t.linkURL if forURL.startswith(linkURL): return t def startMenu(translator, navigation, tag): """ Drop-down menu-style navigation view. For each primary navigation element available, a copy of the I{tab} pattern will be loaded from the tag. It will have its I{href} slot filled with the URL for that navigation item. It will have its I{name} slot filled with the user-visible name of the navigation element. It will have its I{kids} slot filled with a list of secondary navigation for that element. For each secondary navigation element available beneath each primary navigation element, a copy of the I{subtabs} pattern will be loaded from the tag. It will have its I{kids} slot filled with a self-similar structure. @type translator: L{IWebTranslator} provider @type navigation: L{list} of L{Tab} @rtype: {nevow.stan.Tag} """ setTabURLs(navigation, translator) getp = IQ(tag).onePattern def fillSlots(tabs): for tab in tabs: if tab.children: kids = getp('subtabs').fillSlots('kids', fillSlots(tab.children)) else: kids = '' yield dictFillSlots(getp('tab'), dict(href=tab.linkURL, name=tab.name, kids=kids)) return tag.fillSlots('tabs', fillSlots(navigation)) def settingsLink(translator, settings, tag): """ Render the URL of the settings page. """ return tag[translator.linkTo(settings.storeID)] # This is somewhat redundant with startMenu. The selected/not feature of this # renderer should be added to startMenu and then templates can just use that # and this can be deleted. def applicationNavigation(ctx, translator, navigation): """ Horizontal, primary-only navigation view. For the navigation element currently being viewed, copies of the I{selected-app-tab} and I{selected-tab-contents} patterns will be loaded from the tag. For all other navigation elements, copies of the I{app-tab} and I{tab-contents} patterns will be loaded. For either case, the former pattern will have its I{name} slot filled with the name of the navigation element and its I{tab-contents} slot filled with the latter pattern. The latter pattern will have its I{href} slot filled with a link to the corresponding navigation element. The I{tabs} slot on the tag will be filled with all the I{selected-app-tab} or I{app-tab} pattern copies. @type ctx: L{nevow.context.WebContext} @type translator: L{IWebTranslator} provider @type navigation: L{list} of L{Tab} @rtype: {nevow.stan.Tag} """ setTabURLs(navigation, translator) selectedTab = getSelectedTab(navigation, url.URL.fromContext(ctx)) getp = IQ(ctx.tag).onePattern tabs = [] for tab in navigation: if tab == selectedTab or selectedTab in tab.children: p = 'selected-app-tab' contentp = 'selected-tab-contents' else: p = 'app-tab' contentp = 'tab-contents' childTabs = [] for subtab in tab.children: try: subtabp = getp("subtab") except NodeNotFound: continue childTabs.append( dictFillSlots(subtabp, { 'name': subtab.name, 'href': subtab.linkURL, 'tab-contents': getp("subtab-contents") })) tabs.append(dictFillSlots( getp(p), {'name': tab.name, 'tab-contents': getp(contentp).fillSlots( 'href', tab.linkURL), 'subtabs': childTabs})) ctx.tag.fillSlots('tabs', tabs) return ctx.tag PK�����9Fb+Z8+��+�����xmantissa/websession.py# -*- test-case-name: xmantissa.test.test_websession -*- # Copyright 2005 Divmod, Inc. See LICENSE file for details """Sessions that persist in the database. Every SESSION_CLEAN_FREQUENCY seconds, a pass is made over all persistant sessions, and those that are more than PERSISTENT_SESSION_LIFETIME seconds old are deleted. Transient sessions die after TRANSIENT_SESSION_LIFETIME seconds. These three globals can be overridden by passing appropriate values to the PersistentSessionWrapper constructor: sessionCleanFrequency, persistentSessionLifetime, and transientSessionLifetime. """ from twisted.cred import credentials from epsilon import extime from axiom import attributes, item, userbase from nevow import guard SESSION_CLEAN_FREQUENCY = 60 * 60 * 25 # 1 day, almost PERSISTENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 * 2 # 2 weeks TRANSIENT_SESSION_LIFETIME = 60 * 12 + 32 # 12 minutes, 32 seconds. def usernameFromRequest(request): """ Take a HTTP request and return a username of the form <user>@<domain>. @type request: L{inevow.IRequest} @param request: A HTTP request @return: A C{str} """ username = request.args.get('username', [''])[0] if '@' not in username: username = '%s@%s' % (username, request.getHeader('host').split(':')[0]) return username class PersistentSession(item.Item): """A session that persists on the database. These sessions should not store any state, but are used only to determine that the user has previously authenticated and should be given a transient session (a regular guard session, not database persistant) without providing credentials again. """ typeName = 'persistent_session' schemaVersion = 1 sessionKey = attributes.bytes() lastUsed = attributes.timestamp() authenticatedAs = attributes.bytes() # The username and domain # that this session was # authenticated as. def __init__(self, **kw): assert kw.get('sessionKey') is not None, "None cookie propogated to PersistentSession" kw['lastUsed'] = extime.Time() super(PersistentSession, self).__init__(**kw) def __repr__(self): return "PersistentSession(%r)" % (self.sessionKey, ) def renew(self): """Renew the lifetime of this object. Call this when the user logs in so this session does not expire. """ self.lastUsed = extime.Time() class DBPassthrough(object): """A dictionaryish thing that manages sessions and interfaces with guard. This is set as the sessions attribute on a nevow.guard.SessionWrapper instance, or in this case, a subclass. Guard uses a vanilla dict by default; here we pretend to be a dict and introduce presistant-session behaviour. """ def __init__(self, wrapper): self.wrapper = wrapper self._transientSessions = {} def __contains__(self, key): # we use __get__ here so that transient sessions are always created. # Otherwise, sometimes guard will call __contains__ and assume the # transient session is there, without creating it. try: self[key] except KeyError: return False return True has_key = __contains__ def __getitem__(self, key): if key is None: raise KeyError("None is not a valid session key") try: return self._transientSessions[key] except KeyError: if self.wrapper.authenticatedUserForKey(key): session = self.wrapper.sessionFactory(self.wrapper, key) self._transientSessions[key] = session session.setLifetime(self.wrapper.sessionLifetime) # screw you guard! session.checkExpired() return session raise def __setitem__(self, key, value): self._transientSessions[key] = value def __delitem__(self, key): del self._transientSessions[key] def __repr__(self): return 'DBPassthrough at %i; %r, with embelishments' % (id(self), self._transientSessions) class PersistentSessionWrapper(guard.SessionWrapper): """ Extends nevow.guard.SessionWrapper to reauthenticate previously authenticated users. There are 4 possible states: 1) new user, no persistent session, no transient session 2) anonymous user, no persistent session, transient session 3) returning user, persistent session, no transient session 4) active user, persistent session, transient session Guard will look it the sessions dict, and if it finds a key matching a cookie sent by the client, will return the value as the session. However, if a user has a persistent session cookie, but no transient session, one is created here. """ def __init__( self, store, portal, transientSessionLifetime=TRANSIENT_SESSION_LIFETIME, persistentSessionLifetime=PERSISTENT_SESSION_LIFETIME, sessionCleanFrequency=SESSION_CLEAN_FREQUENCY, enableSubdomains=False, domains=(), **kw): """Initialize the PersistentSessionWrapper """ guard.SessionWrapper.__init__(self, portal, **kw) self.store = store self.sessions = DBPassthrough(self) self.cookieKey = 'divmod-user-cookie' self.sessionLifetime = transientSessionLifetime self.persistentSessionLifetime = persistentSessionLifetime self.sessionCleanFrequency = sessionCleanFrequency self._enableSubdomains = enableSubdomains self._domains = domains def createSessionForKey(self, key, user): PersistentSession( store=self.store, sessionKey=key, authenticatedAs=user) def authenticatedUserForKey(self, key): for session in self.store.query(PersistentSession, PersistentSession.sessionKey == key): session.renew() return session.authenticatedAs return None def removeSessionWithKey(self, key): for session in self.store.query(PersistentSession, PersistentSession.sessionKey == key): session.deleteFromStore() break # if the session doesn't exist, we ignore that fact here. def cookieDomainForRequest(self, request): """ Pick a domain to use when setting cookies. @type request: L{nevow.inevow.IRequest} @param request: Request to determine cookie domain for @rtype: C{str} or C{None} @return: Domain name to use when setting cookies, or C{None} to indicate that only the domain in the request should be used """ host = request.getHeader('host') if host is None: # This is a malformed request that we cannot possibly handle # safely, fall back to the default behaviour. return None host = host.split(':')[0] for domain in self._domains: suffix = "." + domain if host == domain: # The request is for a domain which is directly recognized. if self._enableSubdomains: # Subdomains are enabled, so the suffix is returned to # enable the cookie for this domain and all its subdomains. return suffix # Subdomains are not enabled, so None is returned to allow the # default restriction, which will enable this cookie only for # the domain in the request, to apply. return None if self._enableSubdomains and host.endswith(suffix): # The request is for a subdomain of a directly recognized # domain and subdomains are enabled. Drop the unrecognized # subdomain portion and return the suffix to enable the cookie # for this domain and all its subdomains. return suffix if self._enableSubdomains: # No directly recognized domain matched the request. If subdomains # are enabled, prefix the request domain with "." to make the # cookie valid for that domain and all its subdomains. This # probably isn't extremely useful. Perhaps it shouldn't work this # way. return "." + host # Subdomains are disabled and the domain from the request was not # recognized. Return None to get the default behavior. return None def savorSessionCookie(self, request): """ Make the session cookie last as long as the persistant session. @param request: The HTTP request object for the guard login URL. """ cookieValue = request.getSession().uid request.addCookie( self.cookieKey, cookieValue, path='/', max_age=PERSISTENT_SESSION_LIFETIME, domain=self.cookieDomainForRequest(request)) def login(self, request, session, creds, segments): """Called to check the credentials of a user. Here we extend guard's implementation to preauthenticate users if they have a valid persistant session. """ if isinstance(creds, credentials.Anonymous): preauth = self.authenticatedUserForKey(session.uid) if preauth is not None: self.savorSessionCookie(request) creds = userbase.Preauthenticated(preauth) def cbLoginSuccess(input): """ User authenticated successfully. Create the persistent session, and associate it with the username. (XXX it doesn't work like this now) """ user = request.args.get('username') if user is not None: # create a database session and associate it with this user cookieValue = session.uid if request.args.get('rememberMe'): self.createSessionForKey(cookieValue, creds.username) self.savorSessionCookie(request) return input return guard.SessionWrapper.login(self, request, session, creds, segments ).addCallback(cbLoginSuccess) def explicitLogout(self, session): """ Here we override guard's behaviour for the logout action to delete the persistent session. In this case the user has explicitly requested a logout, so the persistent session must be deleted to require the user to log in on the next request. """ guard.SessionWrapper.explicitLogout(self, session) self.removeSessionWithKey(session.uid) def getCredentials(self, request): """ Override SessionWrapper.getCredentials to add the Host: header to the credentials. This will make web-based virtual hosting work. """ username = usernameFromRequest(request) password = request.args.get('password', [''])[0] return credentials.UsernamePassword(username, password) PK�����9F:v +��+�����xmantissa/websharing.py# -*- test-case-name: xmantissa.test.test_websharing -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ This module provides web-based access to objects shared with the xmantissa.sharing module. Users' publicly shared objects are exposed at the url:: http://your-server/users/<user>@<hostname>/<share-id> Applications' publicly shared objects are exposed at the url:: http://your-server/<app-name>/<share-id> where "app-name" is the name of the offering the application was installed from. To share an item publicly, share it with the "everyone" role. To place it at the root URL of the share location, call L{addDefaultShareID} with its share ID. Example:: sharing.getEveryoneRole(yourItem.store).shareItem(yourItem, shareID=u'bob') If this is in an app store named 'foo', this object is now shared on the URL http://your-server/foo/bob. To share it at the root, as http://your-server/foo/, make it the default:: websharing.addDefaultShareID(yourItem.store, u'bob', 1) """ from zope.interface import implements from axiom import userbase, attributes from axiom.item import Item from axiom.attributes import text, integer from nevow import inevow, url, rend from xmantissa.offering import isAppStore from xmantissa import sharing class _DefaultShareID(Item): """ Item which holds a default share ID for a user's store. Default share IDs are associated with a priority, and the highest-priority ID identifies the share which will be selected if the user browsing a substore doesn't provide their own share ID. """ shareID = text(doc=""" A default share ID for the store this item lives in. """, allowNone=False) priority = integer(doc=""" The priority of this default. Higher means more important. """) def addDefaultShareID(store, shareID, priority): """ Add a default share ID to C{store}, pointing to C{shareID} with a priority C{priority}. The highest-priority share ID identifies the share that will be retrieved when a user does not explicitly provide a share ID in their URL (e.g. /host/users/username/). @param shareID: A share ID. @type shareID: C{unicode} @param priority: The priority of this default. Higher means more important. @type priority: C{int} """ _DefaultShareID(store=store, shareID=shareID, priority=priority) def getDefaultShareID(store): """ Get the highest-priority default share ID for C{store}. @return: the default share ID, or u'' if one has not been set. @rtype: C{unicode} """ defaultShareID = store.findFirst( _DefaultShareID, sort=_DefaultShareID.priority.desc) if defaultShareID is None: return u'' return defaultShareID.shareID class _ShareURL(url.URL): """ An L{url.URL} subclass which inserts share ID as a path segment in the URL just before the first call to L{child} modifies it. """ def __init__(self, shareID, *a, **k): """ @param shareID: The ID of the share we are generating a URL for. @type shareID: C{unicode}. """ self._shareID = shareID url.URL.__init__(self, *a, **k) def child(self, path): """ Override the base implementation to inject the share ID our constructor was passed. """ if self._shareID is not None: self = url.URL.child(self, self._shareID) self._shareID = None return url.URL.child(self, path) def cloneURL(self, scheme, netloc, pathsegs, querysegs, fragment): """ Override the base implementation to pass along the share ID our constructor was passed. """ return self.__class__( self._shareID, scheme, netloc, pathsegs, querysegs, fragment) # there is no point providing an implementation of any these methods which # accepts a shareID argument; they don't really mean anything in this # context. def fromString(cls, string): """ Override the base implementation to throw L{NotImplementedError}. @raises L{NotImplementedError}: always. """ raise NotImplementedError( 'fromString is not implemented on %r' % (cls.__name__,)) fromString = classmethod(fromString) def fromRequest(cls, req): """ Override the base implementation to throw L{NotImplementedError}. @raises L{NotImplementedError}: always. """ raise NotImplementedError( 'fromRequest is not implemented on %r' % (cls.__name__,)) fromRequest = classmethod(fromRequest) def fromContext(cls, ctx): """ Override the base implementation to throw L{NotImplementedError}. @raises L{NotImplementedError}: always. """ raise NotImplementedError( 'fromContext is not implemented on %r' % (cls.__name__,)) fromContext = classmethod(fromContext) def linkTo(sharedProxyOrItem): """ Generate the path part of a URL to link to a share item or its proxy. @param sharedProxy: a L{sharing.SharedProxy} or L{sharing.Share} @return: a URL object, which when converted to a string will look something like '/users/user@host/shareID'. @rtype: L{nevow.url.URL} @raise: L{RuntimeError} if the store that the C{sharedProxyOrItem} is stored in is not accessible via the web, for example due to the fact that the store has no L{LoginMethod} objects to indicate who it is owned by. """ if isinstance(sharedProxyOrItem, sharing.SharedProxy): userStore = sharing.itemFromProxy(sharedProxyOrItem).store else: userStore = sharedProxyOrItem.store appStore = isAppStore(userStore) if appStore: # This code-path should be fixed by #2703; PublicWeb is deprecated. from xmantissa.publicweb import PublicWeb substore = userStore.parent.getItemByID(userStore.idInParent) pw = userStore.parent.findUnique(PublicWeb, PublicWeb.application == substore) path = [pw.prefixURL.encode('ascii')] else: for lm in userbase.getLoginMethods(userStore): if lm.internal: path = ['users', lm.localpart.encode('ascii')] break else: raise RuntimeError( "Shared item is in a user store with no" " internal username -- can't generate a link.") if (sharedProxyOrItem.shareID == getDefaultShareID(userStore)): shareID = sharedProxyOrItem.shareID path.append('') else: shareID = None path.append(sharedProxyOrItem.shareID) return _ShareURL(shareID, scheme='', netloc='', pathsegs=path) def _storeFromUsername(store, username): """ Find the user store of the user with username C{store} @param store: site-store @type store: L{axiom.store.Store} @param username: the name a user signed up with @type username: C{unicode} @rtype: L{axiom.store.Store} or C{None} """ lm = store.findUnique( userbase.LoginMethod, attributes.AND( userbase.LoginMethod.localpart == username, userbase.LoginMethod.internal == True), default=None) if lm is not None: return lm.account.avatars.open() class UserIndexPage(object): """ This is the resource accessible at "/users" See L{xmantissa.publicweb.AnonymousSite.child_users} for the integration point with the rest of the system. """ implements(inevow.IResource) def __init__(self, loginSystem, webViewer): """ Create a UserIndexPage which draws users from a given L{userbase.LoginSystem}. @param loginSystem: the login system to look up users in. @type loginSystem: L{userbase.LoginSystem} """ self.loginSystem = loginSystem self.webViewer = webViewer def locateChild(self, ctx, segments): """ Retrieve a L{SharingIndex} for a particular user, or rend.NotFound. """ store = _storeFromUsername( self.loginSystem.store, segments[0].decode('utf-8')) if store is None: return rend.NotFound return (SharingIndex(store, self.webViewer), segments[1:]) def renderHTTP(self, ctx): """ Return a sarcastic string to the user when they try to list the index of users by hitting '/users' by itself. (This should probably do something more helpful. There might be a very large number of users so returning a simple listing is infeasible, but one might at least present a search page or something.) """ return 'Keep trying. You are almost there.' class SharingIndex(object): """ A SharingIndex is an http resource which provides a view onto a user's store, for another user. """ implements(inevow.IResource) def __init__(self, userStore, webViewer): """ Create a SharingIndex. @param userStore: an L{axiom.store.Store} to be viewed. @param webViewer: an L{IWebViewer} which represents the viewer. """ self.userStore = userStore self.webViewer = webViewer def renderHTTP(self, ctx): """ When this resource itself is rendered, redirect to the default shared item. """ return url.URL.fromContext(ctx).child('') def locateChild(self, ctx, segments): """ Look up a shared item for the role viewing this SharingIndex and return a L{PublicAthenaLivePage} containing that shared item's fragment to the user. These semantics are UNSTABLE. This method is adequate for simple uses, but it should be expanded in the future to be more consistent with other resource lookups. In particular, it should allow share implementors to adapt their shares to L{IResource} directly rather than L{INavigableFragment}, to allow for simpler child dispatch. @param segments: a list of strings, the first of which should be the shareID of the desired item. @param ctx: unused. @return: a L{PublicAthenaLivePage} wrapping a customized fragment. """ shareID = segments[0].decode('utf-8') role = self.webViewer.roleIn(self.userStore) # if there is an empty segment if shareID == u'': # then we want to return the default share. if we find one, then # let's use that defaultShareID = getDefaultShareID(self.userStore) try: sharedItem = role.getShare(defaultShareID) except sharing.NoSuchShare: return rend.NotFound # otherwise the user is trying to access some other share else: # let's see if it's a real share try: sharedItem = role.getShare(shareID) # oops it's not except sharing.NoSuchShare: return rend.NotFound return (self.webViewer.wrapModel(sharedItem), segments[1:]) PK�����9F*9w4��4�����xmantissa/webtheme.py# -*- test-case-name: xmantissa.test.test_theme -*- import sys, weakref from zope.interface import implements from twisted.python import reflect from twisted.python.util import sibpath from twisted.python.filepath import FilePath from twisted.python.components import registerAdapter from epsilon.structlike import record from nevow.loaders import xmlfile from nevow import inevow, tags, athena, page from axiom.store import Store from xmantissa import ixmantissa from xmantissa.ixmantissa import ITemplateNameResolver from xmantissa.offering import getInstalledOfferings, getOfferings class ThemeCache(object): """ Collects theme information from the filesystem and caches it. @ivar _getAllThemesCache: a list of all themes available on the filesystem, or None. @ivar _getInstalledThemesCache: a weak-key dictionary of site stores to lists of themes from all installed offerings on them. """ def __init__(self): self.emptyCache() def emptyCache(self): """ Remove cached themes. """ self._getAllThemesCache = None self._getInstalledThemesCache = weakref.WeakKeyDictionary() def _realGetAllThemes(self): """ Collect themes from all available offerings. """ l = [] for offering in getOfferings(): l.extend(offering.themes) l.sort(key=lambda o: o.priority) l.reverse() return l def getAllThemes(self): """ Collect themes from all available offerings, or (if called multiple times) return the previously collected list. """ if self._getAllThemesCache is None: self._getAllThemesCache = self._realGetAllThemes() return self._getAllThemesCache def _realGetInstalledThemes(self, store): """ Collect themes from all offerings installed on this store. """ l = [] for offering in getInstalledOfferings(store).itervalues(): l.extend(offering.themes) l.sort(key=lambda o: o.priority) l.reverse() return l def getInstalledThemes(self, store): """ Collect themes from all offerings installed on this store, or (if called multiple times) return the previously collected list. """ if not store in self._getInstalledThemesCache: self._getInstalledThemesCache[store] = (self. _realGetInstalledThemes(store)) return self._getInstalledThemesCache[store] #XXX this should be local to something, not process-global. theThemeCache = ThemeCache() getAllThemes = theThemeCache.getAllThemes getInstalledThemes = theThemeCache.getInstalledThemes class SiteTemplateResolver(object): """ L{SiteTemplateResolver} implements L{ITemplateNameResolver} according to that site's policy for loading themes. Use this class if you are displaying a public page at the top level of a site. Currently the only available policy is to load all installed themes in priority order. However, in the future, this class may provide more sophisticated ways of loading the preferred theme, including interacting with administrative tools for interactively ordering theme preference. @ivar siteStore: a site store (preferably one with some offerings installed, if you want to actually get a template back) """ implements(ITemplateNameResolver) def __init__(self, siteStore): self.siteStore = siteStore def getDocFactory(self, name, default=None): """ Locate a L{nevow.inevow.IDocFactory} object with the given name from the themes installed on the site store and return it. """ loader = None for theme in getInstalledThemes(self.siteStore): loader = theme.getDocFactory(name) if loader is not None: return loader return default registerAdapter(SiteTemplateResolver, Store, ITemplateNameResolver) _marker = object() def _realGetLoader(n, default=_marker): """ Search all themes for a template named C{n}, returning a loader for it if found. If not found and a default is passed, the default will be returned. Otherwise C{RuntimeError} will be raised. This function is deprecated in favor of using a L{ThemedElement} for your view code, or calling ITemplateNameResolver(userStore).getDocFactory. """ for t in getAllThemes(): fact = t.getDocFactory(n, None) if fact is not None: return fact if default is _marker: raise RuntimeError("No loader for %r anywhere" % (n,)) return default _loaderCache = {} def _memoizedGetLoader(n, default=_marker): """ Find a loader for a template named C{n}, returning C{default} if it is provided. Otherwise raise an error. This function caches loaders it finds. This function is deprecated in favor of using a L{ThemedElement} for your view code, or calling ITemplateNameResolver(userStore).getDocFactory. """ if n not in _loaderCache: _loaderCache[n] = _realGetLoader(n, default) return _loaderCache[n] getLoader = _memoizedGetLoader class XHTMLDirectoryTheme(object): """ I am a theme made up of a directory full of XHTML templates. The suggested use for this class is to make a subclass, C{YourThemeSubclass}, in a module in your Mantissa package, create a directory in your package called 'yourpackage/themes/<your theme name>', and then pass <your theme name> as the constructor to C{YourThemeSubclass} when passing it to the constructor of L{xmantissa.offering.Offering}. You can then avoid calculating the path name in the constructor, since it will be calculated based on where your subclass was defined. @ivar directoryName: the name of the directory containing the appropriate template files. @ivar themeName: the name of the theme that this directory represents. This will be displayed to the user. @ivar stylesheetLocation: A C{list} of C{str} giving the path segments beneath the root at which the stylesheet for this theme is available, or C{None} if there is no applicable stylesheet. """ implements(ixmantissa.ITemplateNameResolver) stylesheetLocation = None def __init__(self, themeName, priority=0, directoryName=None): """ Create a theme based off of a directory full of XHTML templates. @param themeName: sets my themeName @param priority: an integer that affects the ordering of themes returned from L{getAllThemes}. @param directoryName: If None, calculates the directory name based on the module the class is defined in and the given theme name. For a subclass C{bar.baz.FooTheme} defined in C{bar/baz.py} the instance C{FooTheme('qux')}, regardless of where it is created, will have a default directoryName of {bar/themes/qux/}. """ self.themeName = themeName self.priority = priority self.cachedLoaders = {} if directoryName is None: self.directory = FilePath( sibpath(sys.modules[self.__class__.__module__].__file__, 'themes') + '/' + self.themeName) else: self.directory = FilePath(directoryName) self.directoryName = self.directory.path def head(self, request, website): """ Provide content to include in the head of the document. If you only need to provide a stylesheet, see L{stylesheetLocation}. Otherwise, override this. @type request: L{inevow.IRequest} provider @param request: The request object for which this is a response. @param website: The site-wide L{xmantissa.website.WebSite} instance. Primarily of interest for its C{rootURL} method. @return: Anything providing or adaptable to L{nevow.inevow.IRenderer}, or C{None} to include nothing. """ stylesheet = self.stylesheetLocation if stylesheet is not None: root = website.rootURL(request) for segment in stylesheet: root = root.child(segment) return tags.link(rel='stylesheet', type='text/css', href=root) # ITemplateNameResolver def getDocFactory(self, fragmentName, default=None): """ For a given fragment, return a loaded Nevow template. @param fragmentName: the name of the template (can include relative paths). @param default: a default loader; only used if provided and the given fragment name cannot be resolved. @return: A loaded Nevow template. @type return: L{nevow.loaders.xmlfile} """ if fragmentName in self.cachedLoaders: return self.cachedLoaders[fragmentName] segments = fragmentName.split('/') segments[-1] += '.html' file = self.directory for segment in segments: file = file.child(segment) if file.exists(): loader = xmlfile(file.path) self.cachedLoaders[fragmentName] = loader return loader return default class MantissaTheme(XHTMLDirectoryTheme): """ Basic Mantissa-provided theme. Most templates in the I{base} theme will usually be satisfied from this theme provider. """ stylesheetLocation = ['static', 'mantissa-base', 'mantissa.css'] class ThemedDocumentFactory(record('fragmentName resolverAttribute')): """ A descriptor which finds a template loader based on the Mantissa theme system. Use this to have the appropriate template loaded automatically at the time of first access:: class YourElement(Element): ''' An element for showing something. ''' docFactory = ThemedDocumentFactory('template-name', 'userStore') def __init__(self, store): self.userStore = store When C{docFactory} is looked up on an instance of YourElement, the ITemplateNameResolver powerup on its C{'userStore'} attribute will be loaded and asked for the C{'template-name'} template loader. The C{docFactory} attribute access will result in the object in which this results. @ivar fragmentName: A C{str} giving the name of the template which should be loaded. @ivar resolverAttribute: A C{str} giving the name of the attribute on instances which is adaptable to L{ITemplateNameResolver}. """ def __get__(self, oself, type): """ Get the document loader for C{self.fragmentName}. """ resolver = ITemplateNameResolver( getattr(oself, self.resolverAttribute)) return resolver.getDocFactory(self.fragmentName) class _ThemedMixin(object): """ Mixin for L{nevow.inevow.IRenderer} implementations which want to use the theme system. """ implements(ixmantissa.ITemplateNameResolver) def __init__(self, fragmentParent=None): """ Create a themed fragment with the given parent. @param fragmentParent: An object to pass to C{setFragmentParent}. If not None, C{self.setFragmentParent} is called immediately. It is suggested but not required that you set this here; if not, the resulting fragment will be initialized in an inconsistent state. You must call setFragmentParent to correct this before this fragment is rendered. """ super(_ThemedMixin, self).__init__() if fragmentParent is not None: self.setFragmentParent(fragmentParent) def head(self): """ Don't do anything. """ def rend(self, context, data): """ Automatically retrieve my C{docFactory} based on C{self.fragmentName} before invoking L{athena.LiveElement.rend}. """ if self.docFactory is None: self.docFactory = self.getDocFactory(self.fragmentName) return super(_ThemedMixin, self).rend(context, data) def pythonClass(self, request, tag): """ This renderer is available on all themed fragments. It returns the fully qualified python name of the class of the fragment being rendered. """ return reflect.qual(self.__class__) page.renderer(pythonClass) def render_pythonClass(self, ctx, data): return self.pythonClass(inevow.IRequest(ctx), ctx.tag) # ITemplateNameResolver def getDocFactory(self, fragmentName, default=None): f = getattr(self.page, "getDocFactory", getLoader) return f(fragmentName, default) class ThemedFragment(_ThemedMixin, athena.LiveFragment): """ Subclass me to create a LiveFragment which supports automatic theming. (Deprecated) @ivar fragmentName: A short string naming the template from which the docFactory for this fragment should be loaded. @see ThemedElement """ fragmentName = 'fragment-no-fragment-name-specified' class ThemedElement(_ThemedMixin, athena.LiveElement): """ Subclass me to create a LiveElement which supports automatic theming. @ivar fragmentName: A short string naming the template from which the docFactory for this fragment should be loaded. """ fragmentName = 'element-no-fragment-name-specified' PK�����dFXl70��0�����xmantissa/__init__.py# -*- test-case-name: xmantissa.test -*- from xmantissa._version import __version__ from twisted.python import versions def asTwistedVersion(packageName, versionString): return versions.Version(packageName, *map(int, versionString.split("."))) version = asTwistedVersion("xmantissa", __version__) PK�����KdF5ֻzs��zs�����xmantissa/website.py# -*- test-case-name: xmantissa.test.test_website -*- """ This module defines the basic engine for web sites and applications using Mantissa. It defines the basic in-database web server, and an authentication binding using nevow.guard. To interact with the code defined here, create a web site using the command-line 'axiomatic' program using the 'web' subcommand. """ import warnings from zope.interface import implements try: from cssutils import CSSParser, replaceUrls CSSParser except ImportError: CSSParser = None from epsilon.structlike import record from nevow.inevow import IRequest, IResource from nevow.rend import Page, Fragment from nevow import inevow from nevow.static import File from nevow.url import URL from nevow import url from nevow import athena from axiom.iaxiom import IPowerupIndirector from axiom import upgrade from axiom.item import Item, _PowerupConnector, declareLegacyItem from axiom.attributes import AND, integer, text, reference, bytes, boolean from axiom.userbase import LoginSystem, getAccountNames from axiom.dependency import installOn, uninstallFrom, installedOn from xmantissa.ixmantissa import ( ISiteRootPlugin, ISessionlessSiteRootPlugin, ISiteURLGenerator) from xmantissa.port import TCPPort, SSLPort from xmantissa.web import SiteConfiguration from xmantissa.cachejs import theHashModuleProvider from xmantissa._webutil import SiteRootMixin class MantissaLivePage(athena.LivePage): """ An L{athena.LivePage} which supports the global JavaScript modules collection that Mantissa provides as a root resource. All L{athena.LivePage} usages within and derived from Mantissa should subclass this. @ivar webSite: a L{WebSite} instance which provides site configuration information for generating links. @ivar hashCache: a cache which maps JS module names to L{xmantissa.cachejs.CachedJSModule} objects. @type hashCache: L{xmantissa.cachejs.HashedJSModuleProvider} @type _moduleRoot: L{URL} @ivar _moduleRoot: The base location for script tags which load Athena modules required by this page and widgets on this page. This is set based on the I{Host} header in the request, so it is C{None} until the instance is actually rendered. """ hashCache = theHashModuleProvider _moduleRoot = None def __init__(self, webSite, *a, **k): """ Create a L{MantissaLivePage}. @param webSite: a L{WebSite} with a usable secure port implementation. """ self.webSite = webSite athena.LivePage.__init__(self, transportRoot=url.root.child('live'), *a, **k) self._jsDepsMemo = self.hashCache.depsMemo def beforeRender(self, ctx): """ Before rendering, retrieve the hostname from the request being responded to and generate an URL which will serve as the root for all JavaScript modules to be loaded. """ request = IRequest(ctx) root = self.webSite.rootURL(request) self._moduleRoot = root.child('__jsmodule__') def getJSModuleURL(self, moduleName): """ Retrieve an L{URL} object which references the given module name. This makes a 'best effort' guess as to an fully qualified HTTPS URL based on the hostname provided during rendering and the configuration of the site. This is to avoid unnecessary duplicate retrieval of the same scripts from two different URLs by the browser. If such configuration does not exist, however, it will simply return an absolute path URL with no hostname or port. @raise NotImplementedError: if rendering has not begun yet and therefore beforeRender has not provided us with a usable hostname. """ if self._moduleRoot is None: raise NotImplementedError( "JS module URLs cannot be requested before rendering.") moduleHash = self.hashCache.getModule(moduleName).hashValue return self._moduleRoot.child(moduleHash).child(moduleName) JUST_SLASH = ('',) class PrefixURLMixin(object): """ Mixin for use by I[Sessionless]SiteRootPlugin implementors; provides a resourceFactory method which looks for an C{prefixURL} string on self, and calls and returns self.createResource(). C{prefixURL} is a '/'-separated unicode string; it must be set before calling installOn. To respond to the url C{http://example.com/foo/bar}, use the prefixURL attribute u'foo/bar'. @ivar sessioned: Boolean indicating whether this object should powerup for L{ISiteRootPlugin}. Note: this is only tested when L{installOn} is called. If you change it later, it will have no impact. @ivar sessionless: Boolean indicating whether this object should powerup for ISessionlessSiteRootPlugin. This is tested at the same time as L{sessioned}. """ sessioned = False sessionless = False def __str__(self): return '/%s => item(%s)' % (self.prefixURL, self.__class__.__name__) def createResourceWith(self, webViewer): """ Create and return an IResource. This will only be invoked if the request matches the prefixURL specified on this object. May also return None to indicate that this object does not actually want to handle this request. Note that this will only be invoked for L{ISiteRootPlugin} powerups; L{ISessionlessSiteRootPlugin} powerups will only have C{createResource} invoked. """ raise NotImplementedError( "PrefixURLMixin.createResourceWith(webViewer) should be " "implemented by subclasses (%r didn't)" % ( self.__class__.__name__,)) # ISiteRootPlugin def produceResource(self, request, segments, webViewer): """ Return a C{(resource, subsegments)} tuple or None, depending on whether I wish to return an L{IResource} provider for the given set of segments or not. """ def thunk(): cr = getattr(self, 'createResource', None) if cr is not None: return cr() else: return self.createResourceWith(webViewer) return self._produceIt(segments, thunk) # ISessionlessSiteRootPlugin def sessionlessProduceResource(self, request, segments): """ Return a C{(resource, subsegments)} tuple or None, depending on whether I wish to return an L{IResource} provider for the given set of segments or not. """ return self._produceIt(segments, self.createResource) def _produceIt(self, segments, thunk): """ Underlying implmeentation of L{PrefixURLMixin.produceResource} and L{PrefixURLMixin.sessionlessProduceResource}. @param segments: the URL segments to dispatch. @param thunk: a 0-argument callable which returns an L{IResource} provider, or None. @return: a 2-tuple of C{(resource, remainingSegments)}, or L{None}. """ if not self.prefixURL: needle = () else: needle = tuple(self.prefixURL.split('/')) S = len(needle) if segments[:S] == needle: if segments == JUST_SLASH: # I *HATE* THE WEB subsegments = segments else: subsegments = segments[S:] res = thunk() # Even though the URL matched up, sometimes we might still # decide to not handle this request (eg, some prerequisite # for our function is not met by the store). Allow None # to be returned by createResource to indicate this case. if res is not None: return res, subsegments def __getPowerupInterfaces__(self, powerups): """ Install me on something (probably a Store) that will be queried for ISiteRootPlugin providers. """ #First, all the other powerups for x in powerups: yield x # Only 256 segments are allowed in URL paths. We want to make sure # that static powerups always lose priority ordering to dynamic # powerups, since dynamic powerups will have information pURL = self.prefixURL priority = (pURL.count('/') - 256) if pURL == '': # Did I mention I hate the web? Plugins at / are special in 2 # ways. Their segment length is kinda-sorta like 0 most of the # time, except when it isn't. We subtract from the priority here # to make sure that [''] is lower-priority than ['foo'] even though # they are technically the same number of segments; the reason for # this is that / is special in that it pretends to be the parent of # everything and will score a hit for *any* URL in the hierarchy. # Above, we special-case JUST_SLASH to make sure that the other # half of this special-casing holds true. priority -= 1 if not self.sessioned and not self.sessionless: warnings.warn( "Set either sessioned or sessionless on %r! Falling back to " "deprecated providedBy() behavior" % (self.__class__.__name__,), DeprecationWarning, stacklevel=2) for iface in ISessionlessSiteRootPlugin, ISiteRootPlugin: if iface.providedBy(self): yield (iface, priority) else: if self.sessioned: yield (ISiteRootPlugin, priority) if self.sessionless: yield (ISessionlessSiteRootPlugin, priority) class StaticSite(PrefixURLMixin, Item): implements(ISessionlessSiteRootPlugin, # implements both so that it ISiteRootPlugin) # works in both super and sub # stores. typeName = 'static_web_site' schemaVersion = 2 prefixURL = text() staticContentPath = text() sessioned = boolean(default=False) sessionless = boolean(default=True) def __str__(self): return '/%s => file(%s)' % (self.prefixURL, self.staticContentPath) def createResource(self): return File(self.staticContentPath) def installSite(self): """ Not using the dependency system for this class because it's only installed via the command line, and multiple instances can be installed. """ for iface, priority in self.__getPowerupInterfaces__([]): self.store.powerUp(self, iface, priority) def upgradeStaticSite1To2(oldSite): newSite = oldSite.upgradeVersion( 'static_web_site', 1, 2, staticContentPath=oldSite.staticContentPath, prefixURL=oldSite.prefixURL, sessionless=True) for pc in newSite.store.query(_PowerupConnector, AND(_PowerupConnector.powerup == newSite, _PowerupConnector.interface == u'xmantissa.ixmantissa.ISiteRootPlugin')): pc.item.powerDown(newSite, ISiteRootPlugin) return newSite upgrade.registerUpgrader(upgradeStaticSite1To2, 'static_web_site', 1, 2) class StaticRedirect(Item, PrefixURLMixin): implements(inevow.IResource, ISessionlessSiteRootPlugin, ISiteRootPlugin) schemaVersion = 2 typeName = 'web_static_redirect' targetURL = text(allowNone=False) prefixURL = text(allowNone=False) sessioned = boolean(default=True) sessionless = boolean(default=True) def __str__(self): return '/%s => url(%s)' % (self.prefixURL, self.targetURL) def locateChild(self, ctx, segments): return self, () def renderHTTP(self, ctx): return URL.fromContext(ctx).click(self.targetURL) def createResource(self): return self def upgradeStaticRedirect1To2(oldRedirect): newRedirect = oldRedirect.upgradeVersion( 'web_static_redirect', 1, 2, targetURL=oldRedirect.targetURL, prefixURL=oldRedirect.prefixURL) if newRedirect.prefixURL == u'': newRedirect.sessionless = False for pc in newRedirect.store.query(_PowerupConnector, AND(_PowerupConnector.powerup == newRedirect, _PowerupConnector.interface == u'xmantissa.ixmantissa.ISessionlessSiteRootPlugin')): pc.item.powerDown(newRedirect, ISessionlessSiteRootPlugin) return newRedirect upgrade.registerUpgrader(upgradeStaticRedirect1To2, 'web_static_redirect', 1, 2) class AxiomPage(Page): def renderHTTP(self, ctx): return self.store.transact(Page.renderHTTP, self, ctx) class AxiomFragment(Fragment): def rend(self, ctx, data): return self.store.transact(Fragment.rend, self, ctx, data) class StylesheetFactory(record('installedOfferingNames rootURL')): """ Factory which creates resources for stylesheets which will rewrite URLs in them to be rooted at a particular location. @ivar installedOfferingNames: A C{list} of C{unicode} giving the names of the offerings which are installed and have a static content path. These are the offerings for which C{StaticContent} will find children, so these are the only offerings URLs pointed at which should be rewritten. @ivar rootURL: A one-argument callable which takes a request and returns an L{URL} which is to be used as the root of all URLs served by resources this factory creates. """ def makeStylesheetResource(self, path, registry): """ Return a resource for the css at the given path with its urls rewritten based on self.rootURL. """ return StylesheetRewritingResourceWrapper( File(path), self.installedOfferingNames, self.rootURL) class StylesheetRewritingResourceWrapper( record('resource installedOfferingNames rootURL')): """ Resource which renders another resource using a request which rewrites CSS URLs. @ivar resource: Another L{IResource} which will be used to generate the response. @ivar installedOfferingNames: See L{StylesheetFactory.installedOfferingNames} @ivar rootURL: See L{StylesheetFactory.rootURL} """ implements(IResource) def renderHTTP(self, context): """ Render C{self.resource} through a L{StylesheetRewritingRequestWrapper}. """ request = IRequest(context) request = StylesheetRewritingRequestWrapper( request, self.installedOfferingNames, self.rootURL) context.remember(request, IRequest) return self.resource.renderHTTP(context) class StylesheetRewritingRequestWrapper(object): """ Request which intercepts the response body, parses it as CSS, rewrites its URLs, and sends the serialized result. @ivar request: Another L{IRequest} object, methods of which will be used to implement this request. @ivar _buffer: A list of C{str} which have been passed to the write method. @ivar installedOfferingNames: See L{StylesheetFactory.installedOfferingNames} @ivar rootURL: See L{StylesheetFactory.rootURL}. """ def __init__(self, request, installedOfferingNames, rootURL): self.request = request self._buffer = [] self.installedOfferingNames = installedOfferingNames self.rootURL = rootURL def __getattr__(self, name): """ Pass attribute lookups on to the wrapped request object. """ return getattr(self.request, name) def write(self, bytes): """ Buffer the given bytes for later processing. """ self._buffer.append(bytes) def _replace(self, url): """ Change URLs with absolute paths so they are rooted at the correct location. """ segments = url.split('/') if segments[0] == '': root = self.rootURL(self.request) if segments[1] == 'Mantissa': root = root.child('static').child('mantissa-base') segments = segments[2:] elif segments[1] in self.installedOfferingNames: root = root.child('static').child(segments[1]) segments = segments[2:] for seg in segments: root = root.child(seg) return str(root) return url def finish(self): """ Parse the buffered response body, rewrite its URLs, write the result to the wrapped request, and finish the wrapped request. """ stylesheet = ''.join(self._buffer) parser = CSSParser() css = parser.parseString(stylesheet) replaceUrls(css, self._replace) self.request.write(css.cssText) return self.request.finish() class WebSite(Item, SiteRootMixin): """ An IResource avatar which supports L{ISiteRootPlugin}s on user stores and a limited number of other statically defined children. """ powerupInterfaces = (IResource,) implements(*powerupInterfaces + (IPowerupIndirector,)) typeName = 'mantissa_web_powerup' schemaVersion = 6 hitCount = integer(default=0) def cleartextRoot(self, hostname=None): """ Return a string representing the HTTP URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of the C{hostname} attribute of this item. """ warnings.warn( "Use ISiteURLGenerator.rootURL instead of WebSite.cleartextRoot.", category=DeprecationWarning, stacklevel=2) if self.store.parent is not None: generator = ISiteURLGenerator(self.store.parent) else: generator = ISiteURLGenerator(self.store) return generator.cleartextRoot(hostname) def encryptedRoot(self, hostname=None): """ Return a string representing the HTTPS URL which is at the root of this site. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of the C{hostname} attribute of this item. """ warnings.warn( "Use ISiteURLGenerator.rootURL instead of WebSite.encryptedRoot.", category=DeprecationWarning, stacklevel=2) if self.store.parent is not None: generator = ISiteURLGenerator(self.store.parent) else: generator = ISiteURLGenerator(self.store) return generator.encryptedRoot(hostname) def maybeEncryptedRoot(self, hostname=None): """ Returning a string representing the HTTPS URL which is at the root of this site, falling back to HTTP if HTTPS service is not available. @param hostname: An optional unicode string which, if specified, will be used as the hostname in the resulting URL, regardless of the C{hostname} attribute of this item. """ warnings.warn( "Use ISiteURLGenerator.rootURL instead of " "WebSite.maybeEncryptedRoot", category=DeprecationWarning, stacklevel=2) if self.store.parent is not None: generator = ISiteURLGenerator(self.store.parent) else: generator = ISiteURLGenerator(self.store) root = generator.encryptedRoot(hostname) if root is None: root = generator.cleartextRoot(hostname) return root def rootURL(self, request): """ Simple utility function to provide a root URL for this website which is appropriate to use in links generated in response to the given request. @type request: L{twisted.web.http.Request} @param request: The request which is being responded to. @rtype: L{URL} @return: The location at which the root of the resource hierarchy for this website is available. """ warnings.warn( "Use ISiteURLGenerator.rootURL instead of WebSite.rootURL.", category=DeprecationWarning, stacklevel=2) if self.store.parent is not None: generator = ISiteURLGenerator(self.store.parent) else: generator = ISiteURLGenerator(self.store) return generator.rootURL(request) def rootChild_resetPassword(self, req, webViewer): """ Redirect authenticated users to their settings page (hopefully they have one) when they try to reset their password. This is the wrong way for this functionality to be implemented. See #2524. """ from xmantissa.ixmantissa import IWebTranslator, IPreferenceAggregator return URL.fromString( IWebTranslator(self.store).linkTo( IPreferenceAggregator(self.store).storeID)) def setServiceParent(self, parent): """ Compatibility hack necessary to prevent the Axiom service startup mechanism from barfing. Even though this Item is no longer an IService powerup, it will still be found as one one more time and this method will be called on it. """ def getFactory(self): """ @see L{setServiceParent}. """ return self.store.findUnique(SiteConfiguration).getFactory() def _getUsername(self): """ Return a username, suitable for creating a L{VirtualHostWrapper} with. """ return u'@'.join(getAccountNames(self.store).next()) class APIKey(Item): """ Persistent record of a key used for accessing an external API. @cvar URCHIN: Constant name for the "Google Analytics" API (http://code.google.com/apis/maps/) @type URCHIN: C{unicode} """ URCHIN = u'Google Analytics' apiName = text( doc=""" L{APIKey} constant naming the API this key is for. """, allowNone=False) apiKey = text( doc=""" The key. """, allowNone=False) def getKeyForAPI(cls, siteStore, apiName): """ Get the API key for the named API, if one exists. @param siteStore: The site store. @type siteStore: L{axiom.store.Store} @param apiName: The name of the API. @type apiName: C{unicode} (L{APIKey} constant) @rtype: L{APIKey} or C{NoneType} """ return siteStore.findUnique( cls, cls.apiName == apiName, default=None) getKeyForAPI = classmethod(getKeyForAPI) def setKeyForAPI(cls, siteStore, apiName, apiKey): """ Set the API key for the named API, overwriting any existing key. @param siteStore: The site store to install the key in. @type siteStore: L{axiom.store.Store} @param apiName: The name of the API. @type apiName: C{unicode} (L{APIKey} constant) @param apiKey: The key for accessing the API. @type apiKey: C{unicode} @rtype: L{APIKey} """ existingKey = cls.getKeyForAPI(siteStore, apiName) if existingKey is None: return cls(store=siteStore, apiName=apiName, apiKey=apiKey) existingKey.apiKey = apiKey return existingKey setKeyForAPI = classmethod(setKeyForAPI) def _makeSiteConfiguration(currentVersion, oldSite, couldHavePorts): from xmantissa.publicweb import AnonymousSite newSite = oldSite.upgradeVersion( 'mantissa_web_powerup', currentVersion, 6, hitCount=oldSite.hitCount) if newSite.store.parent is not None: return newSite # SiteConfiguration dependsOn LoginSystem. LoginSystem was probably # installed by the mantissa axiomatic command. During the dependency # system conversion, that command was changed to use installOn on the # LoginSystem. However, no upgrader was supplied to create the new # dependency state. Consequently, there may be none. Consequently, a new # LoginSystem will be created if an item which dependsOn LoginSystem is # installed. This would be bad. So, set up the necessary dependency state # here, before instantiating SiteConfiguration. -exarkun # Addendum: it is almost certainly the case that there are not legitimate # configurations which lack a LoginSystem. However, a large number of # database upgrade tests construct unrealistic databases. One aspect of # the unrealism is that they lack a LoginSystem. Therefore, rather than # changing all the bogus stubs and regenerating the stubs, I will just # support the case where LoginSystem is missing. However, note that a # LoginSystem upgrader may invalidate this check and result in a duplicate # being created anyway. -exarkun loginSystem = oldSite.store.findUnique(LoginSystem, default=None) if loginSystem is not None and installedOn(loginSystem) is None: installOn(loginSystem, oldSite.store) uninstallFrom(oldSite.store, oldSite) site = SiteConfiguration( store=oldSite.store, httpLog=oldSite.store.filesdir.child('httpd.log'), hostname=getattr(oldSite, "hostname", None) or u"localhost") installOn(site, site.store) anonymousAvatar = AnonymousSite(store=oldSite.store) installOn(anonymousAvatar, oldSite.store) if couldHavePorts: for tcp in site.store.query(TCPPort, TCPPort.factory == oldSite): tcp.factory = site for ssl in site.store.query(SSLPort, SSLPort.factory == oldSite): ssl.factory = site else: if oldSite.portNumber is not None: port = TCPPort( store=oldSite.store, portNumber=oldSite.portNumber, factory=site) installOn(port, oldSite.store) securePortNumber = oldSite.securePortNumber certificateFile = oldSite.certificateFile if securePortNumber is not None and certificateFile: oldCertPath = site.store.dbdir.preauthChild(certificateFile) newCertPath = site.store.newFilePath('server.pem') oldCertPath.moveTo(newCertPath) port = SSLPort( store=site.store, portNumber=oldSite.securePortNumber, certificatePath=newCertPath, factory=site) installOn(port, site.store) newSite.deleteFromStore() declareLegacyItem( WebSite.typeName, 1, dict( hitCount = integer(default=0), installedOn = reference(), portNumber = integer(default=0), securePortNumber = integer(default=0), certificateFile = bytes(default=None))) def upgradeWebSite1To6(oldSite): return _makeSiteConfiguration(1, oldSite, False) upgrade.registerUpgrader(upgradeWebSite1To6, 'mantissa_web_powerup', 1, 6) declareLegacyItem( WebSite.typeName, 2, dict( hitCount = integer(default=0), installedOn = reference(), portNumber = integer(default=0), securePortNumber = integer(default=0), certificateFile = bytes(default=None), httpLog = bytes(default=None))) def upgradeWebSite2to6(oldSite): # This is dumb and we should have a way to run procedural upgraders. newSite = _makeSiteConfiguration(2, oldSite, False) staticMistake = newSite.store.findUnique(StaticSite, StaticSite.prefixURL == u'static/mantissa', default=None) if staticMistake is not None: # Ugh, need cascading deletes staticMistake.store.powerDown(staticMistake, ISessionlessSiteRootPlugin) staticMistake.deleteFromStore() return newSite upgrade.registerUpgrader(upgradeWebSite2to6, 'mantissa_web_powerup', 2, 6) declareLegacyItem( WebSite.typeName, 3, dict( hitCount = integer(default=0), installedOn = reference(), portNumber = integer(default=0), securePortNumber = integer(default=0), certificateFile = bytes(default=None), httpLog = bytes(default=None))) def upgradeWebsite3to6(oldSite): return _makeSiteConfiguration(3, oldSite, False) upgrade.registerUpgrader(upgradeWebsite3to6, 'mantissa_web_powerup', 3, 6) declareLegacyItem( WebSite.typeName, 4, dict( hitCount=integer(default=0), installedOn=reference(), hostname=text(default=None), portNumber=integer(default=0), securePortNumber=integer(default=0), certificateFile=bytes(default=0), httpLog=bytes(default=None))) def upgradeWebsite4to6(oldSite): return _makeSiteConfiguration(4, oldSite, False) upgrade.registerUpgrader(upgradeWebsite4to6, 'mantissa_web_powerup', 4, 6) declareLegacyItem( WebSite.typeName, 5, dict( hitCount=integer(default=0), installedOn=reference(), hostname=text(default=None), httpLog=bytes(default=None))) def upgradeWebsite5to6(oldSite): """ Create a L{SiteConfiguration} if this is a site store's L{WebSite}. """ return _makeSiteConfiguration(5, oldSite, True) upgrade.registerUpgrader(upgradeWebsite5to6, WebSite.typeName, 5, 6) PK�����FHXC��C�����xmantissa/port.py# -*- test-case-name: xmantissa.test.test_port -*- """ Network port features for Mantissa services. Provided herein are L{IService} L{Item} classes which can be used to take care of most of the work required to run a network server within a Mantissa server. Framework code should define an L{Item} subclass which implements L{xmantissa.ixmantissa.IProtocolFactoryFactory} as desired. No direct interaction with the reactor nor specification of port or other network configuration is necessary in that subclass. Port types from this module can be directly instantiated or configuration can be left up to another tool which operates on arbitrary ports and L{IProtocolFactoryFactory} powerups (for example, the administrative powerup L{xmantissa.webadmin.PortConfiguration}). For example, a finger service might be defined in this way:: from fingerproject import FingerFactory from axiom.item import Item from axiom.attributes import integer from xmantissa.ixmantissa import IProtocolFactoryFactory class Finger(Item): ''' A finger (RFC 1288) server. ''' implements(IProtocolFactoryFactory) powerupInterfaces = (IProtocolFactoryFactory,) requestCount = integer(doc=''' The number of finger requests which have been responded to, ever. ''') def getFactory(self): return FingerFactory(self) All concerns related to binding ports can be disregarded. Once this item has been added to a site store, an administrator will have access to it and may configure it to listen on one or more ports. """ from zope.interface import implements try: from OpenSSL import SSL except ImportError: SSL = None from twisted.application.service import IService, IServiceCollection from twisted.application.strports import service from twisted.internet import reactor from twisted.internet.endpoints import serverFromString from twisted.internet.ssl import PrivateCertificate, CertificateOptions from twisted.python.reflect import qual from twisted.python.usage import Options from axiom.item import Item, declareLegacyItem, normalize from axiom.attributes import inmemory, integer, reference, path, text from axiom.upgrade import registerAttributeCopyingUpgrader from axiom.dependency import installOn from axiom.scripts.axiomatic import AxiomaticCommand, AxiomaticSubCommand from xmantissa.ixmantissa import IProtocolFactoryFactory class PortMixin: """ Mixin implementing most of L{IService} as would be appropriate for an Axiom L{Item} subclass in order to manage the lifetime of an L{twisted.internet.interfaces.IListeningPort}. """ implements(IService) powerupInterfaces = (IService,) # Required by IService but unused by this code. name = None def activate(self): self.parent = None self._listen = None self.listeningPort = None def installed(self): """ Callback invoked after this item has been installed on a store. This is used to set the service parent to the store's service object. """ self.setServiceParent(self.store) def deleted(self): """ Callback invoked after a transaction in which this item has been deleted is committed. This is used to remove this item from its service parent, if it has one. """ if self.parent is not None: self.disownServiceParent() # IService def setServiceParent(self, parent): IServiceCollection(parent).addService(self) self.parent = parent def disownServiceParent(self): IServiceCollection(self.parent).removeService(self) self.parent = None def privilegedStartService(self): if self.portNumber < 1024: self.listeningPort = self.listen() def startService(self): if self.listeningPort is None: self.listeningPort = self.listen() def stopService(self): d = self.listeningPort.stopListening() self.listeningPort = None return d class TCPPort(PortMixin, Item): """ An Axiom Service Item which will bind a TCP port to a protocol factory when it is started. """ schemaVersion = 2 portNumber = integer(doc=""" The TCP port number on which to listen. """) interface = text(doc=""" The hostname to bind to. """, default=u'') factory = reference(doc=""" An Item with a C{getFactory} method which returns a Twisted protocol factory. """, whenDeleted=reference.CASCADE) parent = inmemory(doc=""" A reference to the parent service of this service, whenever there is a parent. """) _listen = inmemory(doc=""" An optional reference to a callable implementing the same interface as L{IReactorTCP.listenTCP}. If set, this will be used to bind a network port. If not set, the reactor will be imported and its C{listenTCP} method will be used. """) listeningPort = inmemory(doc=""" A reference to the L{IListeningPort} returned by C{self.listen} which is set whenever there there is one listening. """) def listen(self): if self._listen is not None: _listen = self._listen else: from twisted.internet import reactor _listen = reactor.listenTCP return _listen(self.portNumber, self.factory.getFactory(), interface=self.interface.encode('ascii')) declareLegacyItem( typeName=normalize(qual(TCPPort)), schemaVersion=1, attributes=dict( portNumber=integer(), factory=reference(), parent=inmemory(), _listen=inmemory(), listeningPort=inmemory())) registerAttributeCopyingUpgrader(TCPPort, 1, 2) class SSLPort(PortMixin, Item): """ An Axiom Service Item which will bind a TCP port to a protocol factory when it is started. """ schemaVersion = 2 portNumber = integer(doc=""" The TCP port number on which to listen. """) interface = text(doc=""" The hostname to bind to. """, default=u'') certificatePath = path(doc=""" Name of the file containing the SSL certificate to use for this server. """) factory = reference(doc=""" An Item with a C{getFactory} method which returns a Twisted protocol factory. """, whenDeleted=reference.CASCADE) parent = inmemory(doc=""" A reference to the parent service of this service, whenever there is a parent. """) _listen = inmemory(doc=""" An optional reference to a callable implementing the same interface as L{IReactorTCP.listenTCP}. If set, this will be used to bind a network port. If not set, the reactor will be imported and its C{listenTCP} method will be used. """) listeningPort = inmemory(doc=""" A reference to the L{IListeningPort} returned by C{self.listen} which is set whenever there there is one listening. """) def getContextFactory(self): if SSL is None: raise RuntimeError("No SSL support: you need to install OpenSSL.") cert = PrivateCertificate.loadPEM( self.certificatePath.open().read()) certOpts = CertificateOptions( cert.privateKey.original, cert.original, requireCertificate=False, method=SSL.SSLv23_METHOD) return certOpts def listen(self): if self._listen is not None: _listen = self._listen else: from twisted.internet import reactor _listen = reactor.listenSSL return _listen( self.portNumber, self.factory.getFactory(), self.getContextFactory(), interface=self.interface.encode('ascii')) declareLegacyItem( typeName=normalize(qual(SSLPort)), schemaVersion=1, attributes=dict( portNumber=integer(), certificatePath=path(), factory=reference(), parent=inmemory(), _listen=inmemory(), listeningPort=inmemory())) registerAttributeCopyingUpgrader(SSLPort, 1, 2) class StringEndpointPort(PortMixin, Item): """ An Axiom Service Item which will listen on an endpoint described by a string when started. """ description = text(doc=""" String description of the endpoint to listen on. """, allowNone=False) factory = reference(doc=""" An Item with a C{getFactory} method which returns a Twisted protocol factory. """, whenDeleted=reference.CASCADE) parent = inmemory(doc=""" A reference to the parent service of this service, whenever there is a parent. """) _service = inmemory(doc=""" A reference to the real endpoint L{IService}. """) _endpointService = inmemory(doc=""" A callable implementing the same API as L{twisted.application.strports.service}, or C{None}. """) def activate(self): self.parent = None self._service = None self._endpointService = None def _makeService(self): """ Construct a service for the endpoint as described. """ if self._endpointService is None: _service = service else: _service = self._endpointService return _service(self.description.encode('ascii'), self.factory) def privilegedStartService(self): if self._service is None: self._service = self._makeService() self._service.privilegedStartService() def startService(self): if self._service is None: self._service = self._makeService() self._service.startService() def stopService(self): self._service.stopService() class ListOptions(Options): """ I{axiomatic port} subcommand for displaying the ports which are currently set up in a store. """ longdesc = "Show the port/factory bindings in an Axiom store." def postOptions(self): """ Display details about the ports which already exist. """ store = self.parent.parent.getStore() port = None factories = {} for portType in [TCPPort, SSLPort, StringEndpointPort]: for port in store.query(portType): key = port.factory.storeID if key not in factories: factories[key] = (port.factory, []) factories[key][1].append(port) for factory in store.powerupsFor(IProtocolFactoryFactory): key = factory.storeID if key not in factories: factories[key] = (factory, []) def key((factory, ports)): return factory.storeID for factory, ports in sorted(factories.values(), key=key): if ports: print '%d) %r listening on:' % (factory.storeID, factory) for port in ports: if getattr(port, 'interface', None): interface = "interface " + port.interface else: interface = "any interface" if isinstance(port, TCPPort): print ' %d) TCP, %s, port %d' % ( port.storeID, interface, port.portNumber) elif isinstance(port, SSLPort): if port.certificatePath is not None: pathPart = 'certificate %s' % ( port.certificatePath.path,) else: pathPart = 'NO CERTIFICATE' if port.portNumber is not None: portPart = 'port %d' % (port.portNumber,) else: portPart = 'NO PORT' print ' %d) SSL, %s, %s, %s' % ( port.storeID, interface, portPart, pathPart) elif isinstance(port, StringEndpointPort): print ' {:d}) Endpoint {!r}'.format( port.storeID, port.description) else: print '%d) %r is not listening.' % (factory.storeID, factory) if not factories: print "There are no ports configured." raise SystemExit(0) class DeleteOptions(Options): """ I{axiomatic port} subcommand for removing existing ports. @type portIdentifiers: C{list} of C{int} @ivar portIdentifiers: The store IDs of the ports to be deleted, built up by the I{port-identifier} parameter. """ longdesc = ( "Delete an existing port binding from a factory. If a server is " "currently running using the database from which the port is deleted, " "the factory will *not* stop listening on that port until the server " "is restarted.") def __init__(self): Options.__init__(self) self.portIdentifiers = [] def opt_port_identifier(self, storeID): """ Identify a port for deletion. """ self.portIdentifiers.append(int(storeID)) def _delete(self, store, portIDs): """ Try to delete the ports with the given store IDs. @param store: The Axiom store from which to delete items. @param portIDs: A list of Axiom store IDs for TCPPort or SSLPort items. @raise L{SystemExit}: If one of the store IDs does not identify a port item. """ for portID in portIDs: try: port = store.getItemByID(portID) except KeyError: print "%d does not identify an item." % (portID,) raise SystemExit(1) if isinstance(port, (TCPPort, SSLPort)): port.deleteFromStore() else: print "%d does not identify a port." % (portID,) raise SystemExit(1) def postOptions(self): """ Delete the ports specified with the port-identifier option. """ if self.portIdentifiers: store = self.parent.parent.getStore() store.transact(self._delete, store, self.portIdentifiers) print "Deleted." raise SystemExit(0) else: self.opt_help() class CreateOptions(AxiomaticSubCommand): """ I{axiomatic port} subcommand for creating new ports. """ name = "create" longdesc = ( "Create a new port binding for an existing factory. If a server is " "currently running using the database in which the port is created, " "the factory will *not* be started on that port until the server is " "restarted.") _pemFormatError = ('PEM routines', 'PEM_read_bio', 'no start line') _noSuchFileError = ('system library', 'fopen', 'No such file or directory') _certFileError = ('SSL routines', 'SSL_CTX_use_certificate_file', 'system lib') _keyFileError = ('SSL routines', 'SSL_CTX_use_PrivateKey_file', 'system lib') optParameters = [ ("strport", None, None, "A Twisted strports description of a port to add."), ("factory-identifier", None, None, "Identifier for a protocol factory to associate with the new port.")] def postOptions(self): strport = self['strport'] factoryIdentifier = self['factory-identifier'] if strport is None or factoryIdentifier is None: self.opt_help() store = self.parent.parent.getStore() storeID = int(factoryIdentifier) try: factory = store.getItemByID(storeID) except KeyError: print "%d does not identify an item." % (storeID,) raise SystemExit(1) else: if not IProtocolFactoryFactory.providedBy(factory): print "%d does not identify a factory." % (storeID,) raise SystemExit(1) else: description = self.decodeCommandLine(strport) try: serverFromString(reactor, description.encode('ascii')) except ValueError: print "%r is not a valid port description." % (strport,) raise SystemExit(1) port = StringEndpointPort( store=store, description=description, factory=factory) installOn(port, store) print "Created." raise SystemExit(0) class PortConfiguration(AxiomaticCommand): """ Axiomatic subcommand plugin for inspecting and modifying port configuration. """ subCommands = [("list", None, ListOptions, "Show existing ports and factories."), ("delete", None, DeleteOptions, "Delete existing ports."), ("create", None, CreateOptions, "Create new ports.")] name = "port" description = "Examine, create, and destroy network servers." longdesc = ( "This command allows for the inspection and modification of the " "configuration of network services in an Axiom store.") def postOptions(self): """ If nothing else happens, display usage information. """ self.opt_help() __all__ = ['TCPPort', 'SSLPort', 'PortConfiguration'] PK�����N5$GFr���������xmantissa/_version.py__version__ = "0.8.2" PK�����9F8.��.�����xmantissa/reset.rfc2822From: %(from)s To: %(to)s Subject: Password reset request Date: %(date)s Message-ID: %(message-id)s Content-Type: text/html Reply-To: %(from)s MIME-Version: 1.0 <html> <head> <title>Password reset request Click here to reset your password PK9F/. xmantissa/signup.rfc2822From: %%(from)s To: %%(to)s Subject: %(subject)s Date: %%(date)s Message-ID: %%(message-id)s Content-Type: text/html Reply-To: %%(from)s MIME-Version: 1.0 %(subject)s
%(blurb)s
%(linktext)s PK9F@@xmantissa/statcollector.tacfrom xmantissa import stats from twisted.application import internet, service from vertex import juice f = juice.JuiceServerFactory() f.log = open("stats.log",'a') f.protocol = stats.SimpleRemoteStatsCollector application = service.Application("statcollector") internet.TCPServer(8787, f).setServiceParent(application) PK9FBAxmantissa/plugins/__init__.py import os, sys __path__ = [os.path.abspath(os.path.join(x, *__name__.split('.'))) for x in sys.path] __all__ = [] # nothing to see here, move along, move along PK9Fsxmantissa/plugins/adminoff.py from xmantissa import offering, stats from xmantissa.webadmin import (TracebackViewer, LocalUserBrowser, DeveloperApplication, PortConfiguration) from xmantissa.signup import SignupConfiguration adminOffering = offering.Offering( name = u'mantissa', description = u'Powerups for administrative control of a Mantissa server.', siteRequirements = [], appPowerups = [stats.StatsService], installablePowerups = [("Signup Configuration", "Allows configuration of signup mechanisms", SignupConfiguration), ("Traceback Viewer", "Allows viewing unhandled exceptions which occur on the server", TracebackViewer), ("Port Configuration", "Allows manipulation of network service configuration.", PortConfiguration), ("Local User Browser", "A page listing all users existing in this site's store.", LocalUserBrowser), ("Admin REPL", "An interactive python prompt.", DeveloperApplication), ("Offering Configuration", "Allows installation of Offerings on this site", offering.OfferingConfiguration), ("Stats Observation", "Allows remote observation via AMP of gathered performance-related stats", stats.RemoteStatsCollectorFactory), ], loginInterfaces=(), themes = ()) PK9Ftxmantissa/plugins/baseoff.py from twisted.python.filepath import FilePath from twisted.protocols.amp import IBoxReceiver from twisted.conch.interfaces import IConchUser from nevow.inevow import IResource from xmantissa.ixmantissa import ISiteURLGenerator from xmantissa import offering from xmantissa.web import SiteConfiguration from xmantissa.webtheme import MantissaTheme from xmantissa.publicweb import AnonymousSite from xmantissa.ampserver import AMPConfiguration, AMPAvatar, EchoFactory from xmantissa.terminal import SecureShellConfiguration import xmantissa baseOffering = offering.Offering( name=u'mantissa-base', description=u'Basic Mantissa functionality', siteRequirements=[ (ISiteURLGenerator, SiteConfiguration), (IResource, AnonymousSite), (None, SecureShellConfiguration)], appPowerups=(), installablePowerups=(), loginInterfaces = [ (IResource, "HTTP logins"), (IConchUser, "SSH logins")], # priority should be 0 for pretty much any other theme. 'base' is the theme # that all other themes should use as a reference for what elements are # required. themes=(MantissaTheme('base', 1),), staticContentPath=FilePath(xmantissa.__file__).sibling('static'), version=xmantissa.version) # XXX This should be part of baseOffering, but because there is no # functionality for upgrading installed offering state, doing so would create a # class of databases which thought they had amp installed but didn't really. # See #2723. ampOffering = offering.Offering( name=u'mantissa-amp', description=u'Extra AMP-related Mantissa functionality', siteRequirements=[ (None, AMPConfiguration), ], appPowerups=[], installablePowerups=[ ("AMP Access", "Allows logins over AMP", AMPAvatar), ("AMP Echo Protocol", "Dead-simple AMP echoer, potentially useful for testing connections " "to a Mantissa AMP server or otherwise providing an example of the " "AMP functionality.", EchoFactory), ], loginInterfaces=[ (IBoxReceiver, "AMP logins"), ], themes=[], staticContentPath=None, version=xmantissa.version) PK9F,d## xmantissa/plugins/free_signup.py from xmantissa import signup freeTicket = signup.SignupMechanism( name = 'Free Ticket', description = ''' Create a page which will allow anyone with a verified email address to sign up for the system. When the user enters their email address, a confirmation email is sent to it containing a link which will allow signup to proceed. When the link is followed, an account will be created and endowed by the benefactors associated with this instance. ''', itemClass = signup.FreeTicketSignup, configuration = signup.freeTicketSignupConfiguration) userInfo = signup.SignupMechanism( name = 'Required User Information', description = ''' Create a signup mechanism with several self-validating fields. This will also require the user to select a local username before the account is created, and it will create the account immediately rather than waiting for the ticket to be claimed. ''', itemClass = signup.UserInfoSignup, configuration = signup.freeTicketSignupConfiguration) PK9F';llxmantissa/plugins/offerings.pyfrom xmantissa import people, offering, website peopleOffering = offering.Offering( name=u'People', description=u'Basic organizer and addressbook support.', siteRequirements=((None, website.WebSite),), appPowerups=(), installablePowerups = [("People", "Organizer and Address Book", people.AddPerson)], loginInterfaces=(), themes=()) PKrQF)>xmantissa/plugins/dropin.cache(dp1 S'offerings' p2 ccopy_reg _reconstructor p3 (ctwisted.plugin CachedDropin p4 c__builtin__ object p5 NtRp6 (dp7 S'moduleName' p8 S'xmantissa.plugins.offerings' p9 sS'description' p10 NsS'plugins' p11 (lp12 g3 (ctwisted.plugin CachedPlugin p13 g5 NtRp14 (dp15 S'provided' p16 (lp17 ctwisted.plugin IPlugin p18 acxmantissa.ixmantissa IOffering p19 asS'dropin' p20 g6 sS'name' p21 S'peopleOffering' p22 sg10 S'\n A set of functionality which can be added to a Mantissa server.\n\n @see L{ixmantissa.IOffering}\n ' p23 sbasbsS'baseoff' p24 g3 (g4 g5 NtRp25 (dp26 g8 S'xmantissa.plugins.baseoff' p27 sg10 Nsg11 (lp28 g3 (g13 g5 NtRp29 (dp30 g16 (lp31 g18 ag19 asg20 g25 sg21 S'ampOffering' p32 sg10 g23 sbag3 (g13 g5 NtRp33 (dp34 g16 (lp35 g18 ag19 asg20 g25 sg21 S'baseOffering' p36 sg10 g23 sbasbsS'free_signup' p37 g3 (g4 g5 NtRp38 (dp39 g8 S'xmantissa.plugins.free_signup' p40 sg10 Nsg11 (lp41 g3 (g13 g5 NtRp42 (dp43 g16 (lp44 cxmantissa.ixmantissa ISignupMechanism p45 ag18 asg20 g38 sg21 S'freeTicket' p46 sg10 S'\n I am a Twisted plugin helper.\n\n Instantiate me at module scope in a xmantissa.plugins submodule, including\n a name and description for the administrator.\n ' p47 sbag3 (g13 g5 NtRp48 (dp49 g16 (lp50 g45 ag18 asg20 g38 sg21 S'userInfo' p51 sg10 g47 sbasbsS'adminoff' p52 g3 (g4 g5 NtRp53 (dp54 g8 S'xmantissa.plugins.adminoff' p55 sg10 Nsg11 (lp56 g3 (g13 g5 NtRp57 (dp58 g16 (lp59 g18 ag19 asg20 g53 sg21 S'adminOffering' p60 sg10 g23 sbasbs.PK9F1` h))xmantissa/test/__init__.py# -*- test-case-name: xmantissa.test -*- PK9FN|w$$xmantissa/test/fakes.py# Copyright 2008 Divmod, Inc. See LICENSE file for details # -*- test-case-name: xmantissa.test.test_webapp.AuthenticatedWebViewerTests,xmantissa.test.test_publicweb.AnonymousWebViewerTests -*- """ A collection of fake versions of various objects used in tests. There are a lot of classes here because many of them have model/view interactions that are expressed through adapter registrations, so having additional types is helpful. """ from zope.interface import implements from twisted.python.components import registerAdapter from epsilon.structlike import record from nevow.athena import LiveElement, LiveFragment from nevow.page import Element from nevow import rend, loaders from nevow.inevow import IResource from xmantissa.ixmantissa import INavigableFragment class FakeLoader(record('name')): """ A fake Nevow loader object. """ class FakeTheme(record('themeName loaders')): """ A placeholder for theme lookup. @ivar loaders: A dict of strings to loader objects. @ivar themeName: A name that describes this theme. """ def getDocFactory(self, name, default=None): """ @param name: A loader name. """ return self.loaders.get(name, default) class FakeModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ class ResourceViewForFakeModel(rend.Page): """ Implementor of L{IResource} for L{FakeModel}. """ registerAdapter(ResourceViewForFakeModel, FakeModel, IResource) class _HasModel(object): """ A mixin that provides a 'model' attribute for Element subclasses. This is a simple hack that attempts to cooperatively invoke __init__ so that its numerous subclasses don't have to define a constructor. If you want to use it you should read its implementation. """ def __init__(self, model): """ Set the model attribute and delegate to the other subclass. """ self.model = model self.__class__.__bases__[1].__init__(self) class FakeElementModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ class ElementViewForFakeModel(_HasModel, Element): """ L{Element} implementor of L{INavigableFragment} for L{FakeElementModel}. """ implements(INavigableFragment) docFactory = loaders.stan('') registerAdapter(ElementViewForFakeModel, FakeElementModel, INavigableFragment) class FakeElementModelWithTheme(object): """ A simple 'model' object that does nothing, for the purposes of adaptation to L{ElementViewForFakeModelWithTheme}. """ class ElementViewForFakeModelWithTheme(_HasModel, Element): """ L{Element} implementor of L{INavigableFragment} for L{FakeElementModel}. """ implements(INavigableFragment) fragmentName = 'awesome_page' registerAdapter(ElementViewForFakeModelWithTheme, FakeElementModelWithTheme, INavigableFragment) class FakeElementModelWithDocFactory(record('loader')): """ A simple 'model' object that does nothing, for the purposes of adaptation to L{ElementViewForFakeModelWithDocFactory}. @ivar loader: A loader object (to be used as the view's docFactory). """ class ElementViewForFakeModelWithDocFactory(_HasModel, Element): """ L{Element} implementor of L{INavigableFragment} for L{FakeElementModel}. """ implements(INavigableFragment) def __init__(self, original): """ Set docFactory and proceed as usual. """ _HasModel.__init__(self, original) self.docFactory = original.loader registerAdapter(ElementViewForFakeModelWithDocFactory, FakeElementModelWithDocFactory, INavigableFragment) class FakeElementModelWithThemeAndDocFactory(record('fragmentName loader')): """ A simple 'model' object that does nothing, for the purposes of adaptation to L{ElementViewForFakeModelWithThemeAndDocFactory}. """ class ElementViewForFakeModelWithThemeAndDocFactory(_HasModel, Element): """ L{Element} implementor of L{INavigableFragment} for L{FakeElementModel}. """ implements(INavigableFragment) def __init__(self, original): """ Set docFactory and proceed as usual. """ _HasModel.__init__(self, original) self.docFactory = original.loader self.fragmentName = original.fragmentName registerAdapter(ElementViewForFakeModelWithThemeAndDocFactory, FakeElementModelWithThemeAndDocFactory, INavigableFragment) class FakeFragmentModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ class FragmentViewForFakeModel(rend.Fragment): """ L{Fragment} implementor of L{INavigableFragment} for L{FakeFragmentModel}. """ implements(INavigableFragment) docFactory = loaders.stan('') registerAdapter(FragmentViewForFakeModel, FakeFragmentModel, INavigableFragment) class FakeLiveElementModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ class LiveElementViewForFakeModel(_HasModel, LiveElement): """ L{LiveElement} Implementor of L{INavigableFragment} for L{FakeLiveElementModel}. """ implements(INavigableFragment) docFactory = loaders.stan('') registerAdapter(LiveElementViewForFakeModel, FakeLiveElementModel, INavigableFragment) class FakeLiveFragmentModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ class LiveFragmentViewForFakeModel(LiveFragment): """ L{LiveFragment} Implementor of L{INavigableFragment} for L{FakeLiveFragmentModel}. """ implements(INavigableFragment) docFactory = loaders.stan('') @classmethod def wrap(cls, model): """ Wrap the given model in this class. Implement this as a method in this file so that the warning filename will match up... """ return cls(model) registerAdapter(LiveFragmentViewForFakeModel.wrap, FakeLiveFragmentModel, INavigableFragment) class FakeElementModelWithLocateChildView(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ def __init__(self, children, beLive): """ @param children: an iterable of children to be returned from the view's locateChild. """ self.childs = iter(children) # implemented this way because we want to # see an error if locateChild is called # too many times; often this will be a # sequence of length 1 self.beLive = beLive def __conform__(self, interface): """ @param interface: IResource (for which there is no adapter) or INavigableFragment (for which there is one, depending on this model's liveness). """ if interface is not INavigableFragment: return None if self.beLive: return LiveElementViewForModelWithLocateChild(self) else: return ElementViewForFakeModelWithLocateChild(self) class _HasLocateChild(_HasModel): """ Has a locateChild that delegates to its model. """ implements(INavigableFragment) docFactory = loaders.stan('') def locateChild(self, ctx, segments): """ Stub implementation that merely records whether it was called. """ return self.model.childs.next() class LiveElementViewForModelWithLocateChild(_HasLocateChild, LiveElement): """ Live element with a locateChild. """ class ElementViewForFakeModelWithLocateChild(_HasLocateChild, Element): """ Non-live element with a locateChild. """ class FakeCustomizableElementModel(object): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ username = None def custom(self, username): """ Record the username our view was customized with. """ self.username = username class ElementViewForFakeCustomizableElementModel(_HasModel, Element): """ An L{Element} that delegates C{customizeFor} calls to its model. """ implements(INavigableFragment) docFactory = loaders.stan('') def customizeFor(self, username): """ Delegate to model. """ self.model.custom(username) return self registerAdapter(ElementViewForFakeCustomizableElementModel, FakeCustomizableElementModel, INavigableFragment) class FakeElementModelWithHead(record('head')): """ A simple 'model' object that does nothing, for the purposes of adaptation. """ def _head(self): return self.head class ElementViewForFakeModelWithHead(_HasModel, Element): """ L{Element} implementor of L{INavigableFragment} for L{FakeElementModel}. """ implements(INavigableFragment) docFactory = loaders.stan('') def head(self): return self.model._head() registerAdapter(ElementViewForFakeModelWithHead, FakeElementModelWithHead, INavigableFragment) PK9Fh xmantissa/test/livetest_forms.pyimport textwrap from nevow import loaders, tags from nevow.livetrial import testcase from xmantissa import liveform class TextInput(testcase.TestCase): jsClass = u'Mantissa.Test.Text' def getWidgetDocument(self): f = liveform.LiveForm( self.submit, [liveform.Parameter('argument', liveform.TEXT_INPUT, unicode, 'A text input field: ', default=u'hello world')]) f.setFragmentParent(self) return f def submit(self, argument): self.assertEquals(argument, u'hello world') class MultiTextInput(testcase.TestCase): jsClass = u'Mantissa.Test.MultiText' def submit(self, sequence): self.assertEquals(sequence, [1, 2, 3, 4]) def getWidgetDocument(self): f = liveform.LiveForm( self.submit, (liveform.ListParameter('sequence', int, 4, 'A bunch of text inputs: ', defaults=(1, 2, 3, 4)),)) f.setFragmentParent(self) return f class TextArea(testcase.TestCase): jsClass = u'Mantissa.Test.TextArea' defaultText = textwrap.dedent(u""" Come hither, sir. Though it be honest, it is never good To bring bad news. Give to a gracious message An host of tongues; but let ill tidings tell Themselves when they be felt. """).strip() def submit(self, argument): self.assertEquals(argument, self.defaultText) def getWidgetDocument(self): f = liveform.LiveForm( self.submit, [liveform.Parameter('argument', liveform.TEXTAREA_INPUT, unicode, 'A text area: ', default=self.defaultText)]) f.setFragmentParent(self) return f class Select(testcase.TestCase): jsClass = u'Mantissa.Test.Select' def submit(self, argument): self.assertEquals(argument, u"apples") def getWidgetDocument(self): # XXX No support for rendering these yet! f = liveform.LiveForm( self.submit, [liveform.Parameter('argument', None, unicode)]) f.docFactory = loaders.stan(tags.form(render=tags.directive('liveElement'))[ tags.select(name="argument")[ tags.option(value="apples")["apples"], tags.option(value="oranges")["oranges"]], tags.input(type='submit', render=tags.directive('submitbutton'))]) f.setFragmentParent(self) return f class Choice(testcase.TestCase): jsClass = u'Mantissa.Test.Choice' def submit(self, argument): self.assertEquals(argument, 2) def getWidgetDocument(self): f = liveform.LiveForm( self.submit, [liveform.ChoiceParameter('argument', [('One', 1, False), ('Two', 2, True), ('Three', 3, False)])]) f.setFragmentParent(self) return f class ChoiceMultiple(testcase.TestCase): jsClass = u'Mantissa.Test.ChoiceMultiple' def submit(self, argument): self.assertIn(1, argument) self.assertIn(3, argument) def getWidgetDocument(self): f = liveform.LiveForm( self.submit, [liveform.ChoiceParameter('argument', [('One', 1, True), ('Two', 2, False), ('Three', 3, True)], "Choosing multiples from a list.", multiple=True)]) f.setFragmentParent(self) return f SPECIAL = object() # guaranteed to fuck up JSON if it ever gets there by # accident. class Traverse(testcase.TestCase): jsClass = u'Mantissa.Test.Traverse' def submit(self, argument, group): self.assertEquals(argument, u'hello world') self.assertEquals(group, SPECIAL) def paramfilter(self, param1): self.assertEquals(param1, u'goodbye world') return SPECIAL def getWidgetDocument(self): f = liveform.LiveForm( self.submit, [liveform.Parameter('argument', liveform.TEXT_INPUT, unicode, 'A text input field: ', default=u'hello world'), liveform.Parameter('group', liveform.FORM_INPUT, liveform.LiveForm(self.paramfilter, [liveform.Parameter ('param1', liveform.TEXT_INPUT, unicode, 'Another input field: ', default=u'goodbye world')]), 'A form input group: ', )]) f.setFragmentParent(self) return f class SetInputValues(testcase.TestCase): jsClass = u'Mantissa.Test.SetInputValues' def submit(self, choice, choiceMult, text, passwd, textArea, checkbox): """ Assert that all input values have been reversed/inverted """ self.assertEqual(choice, 1) self.assertEqual(choiceMult, (2, 3)) self.assertEqual(text, 'dlrow olleh') self.assertEqual(passwd, 'yek terces') self.assertEqual(textArea, '2 dlrow olleh') self.failIf(checkbox) def getWidgetDocument(self): """ Make a LiveForm with one of each kind of input, except for radio buttons, since with the current liveform support for them it's difficult to use them with a single form, and it's not so important to do anything else right now """ f = liveform.LiveForm( self.submit, (liveform.ChoiceParameter( 'choice', (('0', 0, True), ('1', 1, False))), liveform.ChoiceParameter( 'choiceMult', (('0', 0, True), ('1', 1, True), ('2', 2, False), ('3', 3, False)), multiple=True), liveform.Parameter( 'text', liveform.TEXT_INPUT, unicode, default=u'hello world'), liveform.Parameter( 'passwd', liveform.PASSWORD_INPUT, unicode, default=u'secret key'), liveform.Parameter( 'textArea', liveform.TEXTAREA_INPUT, unicode, default=u'hello world 2'), liveform.Parameter( 'checkbox', liveform.CHECKBOX_INPUT, bool, default=True))) f.setFragmentParent(self) return f class FormName(testcase.TestCase): """ Test that the form name is correctly set client-side """ jsClass = u'Mantissa.Test.FormName' def getWidgetDocument(self): """ Make a nested form """ f = liveform.LiveForm( lambda **k: None, (liveform.Parameter( 'inner-form', liveform.FORM_INPUT, liveform.LiveForm( lambda **k: None, (liveform.Parameter( 'inner-parameter', liveform.TEXT_INPUT, unicode, ''),), ())),)) f.setFragmentParent(self) return f PK9F߄22!xmantissa/test/livetest_people.pyimport sys from nevow import tags from nevow.livetrial.testcase import TestCase from axiom.store import Store from axiom.dependency import installOn from xmantissa import people, ixmantissa from xmantissa.liveform import FORM_INPUT from xmantissa.webtheme import getLoader from xmantissa.webapp import PrivateApplication class AddPersonTestBase(people.AddPersonFragment): jsClass = None def __init__(self): self.store = Store() organizer = people.Organizer(store=self.store) installOn(organizer, self.store) people.AddPersonFragment.__init__(self, organizer) def getWidgetDocument(self): return tags.invisible(render=tags.directive('addPersonForm')) def mangleDefaults(self, params): """ Called before rendering the form to give tests an opportunity to modify the defaults for the parameters being used. @type params: L{list} of liveform parameters @param params: The parameters which will be used by the liveform being rendered. """ def checkResult(self, positional, keyword): """ Verify that the given arguments are the ones which were expected by the form submission. Override this in a subclass. @type positional: L{tuple} @param positional: The positional arguments submitted by the form. @type keyword: L{dict} @param keyword: The keyword arguments submitted by the form. """ raise NotImplementedError() def addPerson(self, *a, **k): """ Override form handler to just check the arguments given without trying to modify any database state. """ self.checkResult(a, k) def render_addPersonForm(self, ctx, data): liveform = super(AddPersonTestBase, self).render_addPersonForm(ctx, data) # XXX This is a pretty terrible hack. The client-side of these tests # just submit the form. In order for the assertions to succeed, that # means the form needs to be rendered with some values in it already. # There's no actual API for putting values into the form here, though. # So instead, we'll grovel over all the parameters and try to change # them to reflect what we want. Since this relies on there being no # conflictingly named parameters anywhere in the form and since it # relies on the parameters being traversable in order to find them all, # this is rather fragile. The tests should most likely just put values # in on the client or something along those lines (it's not really # clear what the intent of these tests are, anyway, so it's not clear # what alternate approach would satisfy that intent). params = [] remaining = liveform.parameters[:] while remaining: p = remaining.pop() if p.type == FORM_INPUT: remaining.extend(p.coercer.parameters) else: params.append((p.name, p)) self.mangleDefaults(dict(params)) return liveform class OnlyNick(AddPersonTestBase, TestCase): jsClass = u'Mantissa.Test.OnlyNick' def mangleDefaults(self, params): """ Set the nickname in the form to a particular value for L{checkResult} to verify when the form is submitted. """ params['nickname'].default = u'everybody' def checkResult(self, positional, keyword): """ There should be no positional arguments but there should be keyword arguments for each of the two attributes of L{Person} and three more for the basic contact items. Only the nickname should have a value. """ self.assertEqual(positional, ()) self.assertEqual( keyword, {'nickname': u'everybody', 'vip': False, 'xmantissa.people.PostalContactType': [{'address': u''}], 'xmantissa.people.EmailContactType': [{'email': u''}]}) class NickNameAndEmailAddress(AddPersonTestBase, TestCase): jsClass = u'Mantissa.Test.NickNameAndEmailAddress' def mangleDefaults(self, params): """ Set the nickname and email address to values which L{checkResult} can verify. """ params['nickname'].default = u'NICK!!!' params['xmantissa.people.EmailContactType'].parameters[0].default = u'a@b.c' def checkResult(self, positional, keyword): """ Verify that the nickname and email address set in L{mangleDefaults} are submitted. """ self.assertEqual(positional, ()) self.assertEqual( keyword, {'nickname': u'NICK!!!', 'vip': False, 'xmantissa.people.PostalContactType': [{'address': u''}], 'xmantissa.people.EmailContactType': [{'email': u'a@b.c'}]}) PK9F( xmantissa/test/livetest_prefs.pyfrom axiom.store import Store from axiom.dependency import installOn from nevow.livetrial.testcase import TestCase from nevow.athena import expose from xmantissa import prefs class _PrefMixin(object): def getWidgetDocument(self): s = Store() self.dpc = prefs.DefaultPreferenceCollection(store=s) installOn(s, self.dpc) f = prefs.PreferenceCollectionFragment(self.dpc) class Tab: name = '' children = () f.tab = Tab f.setFragmentParent(self) return f class GeneralPrefs(_PrefMixin, TestCase): """ Test case which renders L{xmantissa.ixmantissa.DefaultPreferenceCollection} and ensures that values changed client-side are correctly persisted """ jsClass = u'Mantissa.Test.GeneralPrefs' def checkPersisted(self, itemsPerPage, timezone): """ Assert that our preference collection has had its C{itemsPerPage} and C{timezone} attributes set to C{itemsPerPage} and C{timezone}. Called after the deferred returned by the liveform controller's C{submit} method has fired """ self.assertEquals(self.dpc.itemsPerPage, itemsPerPage) self.assertEquals(self.dpc.timezone, timezone) expose(checkPersisted) class PrefCollection(_PrefMixin, TestCase): """ Test case which renders L{xmantissa.ixmantissa.DefaultPreferenceCollection} and makes sure that the form appears after submit. """ jsClass = u'Mantissa.Test.PrefCollectionTestCase' PK9F;O"xmantissa/test/livetest_regions.py""" Nits for scrolltable region widget. """ from zope.interface import implements from axiom.item import Item from axiom import attributes from axiom.store import Store from nevow.livetrial.testcase import TestCase from nevow.athena import expose from xmantissa.ixmantissa import IWebTranslator from xmantissa.scrolltable import ScrollingElement from xmantissa.webtheme import getLoader class FakeTranslator(object): """ Translate webIDs deterministically for ease of testing. @ivar store: the axiom store to retrieve items from. """ implements(IWebTranslator) def __init__(self, store): """ Create a FakeTranslator from a given axiom store. """ self.store = store def fromWebID(self, webID): """ Load an item from a hashed storeID. """ return self.store.getItemByID(int(webID.split('-')[-1])) def toWebID(self, item): """ Convert an item into a string identifier by hashing its storeID. """ return 'webID-' + str(item.storeID) class SampleRowItem(Item): """ A sample item to be used as rows in the tests. """ value = attributes.integer() class ScrollingElementTestCase(TestCase): """ Nits for L{ScrollingElement} """ jsClass = u'Mantissa.Test.TestRegionLive.ScrollingElementTestCase' def getScrollingElement(self, rowCount): """ Get a L{ScrollingElement} """ s = Store() for x in xrange(rowCount): SampleRowItem(value=(x + 1) * 50, store=s) scrollingElement = ScrollingElement( s, SampleRowItem, None, (SampleRowItem.value,), None, True, FakeTranslator(s)) scrollingElement.setFragmentParent(self) scrollingElement.docFactory = getLoader( scrollingElement.fragmentName) return scrollingElement expose(getScrollingElement) PK9FK֡X &xmantissa/test/livetest_scrolltable.pyfrom nevow.athena import expose from nevow.livetrial.testcase import TestCase from axiom.item import Item from axiom.store import Store from axiom.attributes import integer from axiom.dependency import installOn from xmantissa.scrolltable import SequenceScrollingFragment from xmantissa.webtheme import getLoader from xmantissa.webapp import PrivateApplication class ScrollElement(Item): """ Dummy item used to populate scrolltables for the scrolltable tests. """ column = integer() class ScrollTableModelTestCase(TestCase): """ Tests for the scrolltable's model class. """ jsClass = u'Mantissa.Test.ScrollTableModelTestCase' class ScrollTableWidgetTestCase(TestCase): """ Tests for the scrolltable's view class. """ jsClass = u'Mantissa.Test.ScrollTableViewTestCase' def __init__(self): TestCase.__init__(self) self.perTestData = {} def getScrollingWidget(self, key, rowCount=10): store = Store() installOn(PrivateApplication(store=store), store) elements = [ScrollElement(store=store, column=i) for i in range(rowCount)] columns = [ScrollElement.column] f = SequenceScrollingFragment(store, elements, columns) f.docFactory = getLoader(f.fragmentName) f.setFragmentParent(self) self.perTestData[key] = (store, elements, f) return f expose(getScrollingWidget) def changeRowCount(self, key, n): store, elements, fragment = self.perTestData[key] elements[:] = [ScrollElement(store=store, column=i) for i in range(n)] expose(changeRowCount) class ScrollTableActionsTestCase(ScrollTableWidgetTestCase): """ Tests for scrolltable actions """ jsClass = u'Mantissa.Test.ScrollTableActionsTestCase' def getScrollingWidget(self, key, *a, **kw): f = ScrollTableWidgetTestCase.getScrollingWidget(self, key, *a, **kw) f.jsClass = u'Mantissa.Test.ScrollTableWithActions' # close over "key" because actions can't supply additional # arguments, and there isn't a use case outside of this test def action_delete(scrollElement): elements = self.perTestData[key][1] elements.remove(scrollElement) f.action_delete = action_delete return f expose(getScrollingWidget) class ScrollTablePlaceholderRowsTestCase(ScrollTableWidgetTestCase): """ Tests for the scrolltable's placeholder rows """ jsClass = u'Mantissa.Test.ScrollTablePlaceholderRowsTestCase' PK9FCzz!xmantissa/test/livetest_signup.pyfrom nevow.livetrial import testcase from nevow.athena import expose from xmantissa.signup import ValidatingSignupForm class FakeUserInfoSignup: def createUser(self, firstName, lastName, username, domain, password, emailAddress): assert False, "Form shouldn't be submitted" def usernameAvailable(self, username, domain): if username == 'bad': return [False, u'bad username'] return [True, u"no reason"] class TestUserInfoSignup(testcase.TestCase): jsClass = u'Mantissa.Test.UserInfoSignup' def getWidgetDocument(self): uis = FakeUserInfoSignup() vsf = ValidatingSignupForm(uis) vsf.setFragmentParent(self) return vsf class TestSignupLocalpartValidation(TestUserInfoSignup): jsClass = u'Mantissa.Test.SignupLocalpartValidation' class TestSignupValidationInformation(testcase.TestCase): jsClass = u'Mantissa.Test.SignupValidationInformation' def makeWidget(self): uis = FakeUserInfoSignup() vsf = ValidatingSignupForm(uis) vsf.setFragmentParent(self) return vsf expose(makeWidget) PK9F  xmantissa/test/livetest_stats.pyimport datetime from nevow.livetrial import testcase from nevow import loaders, tags, athena from epsilon import extime from xmantissa import webadmin class AdminStatsTestBase(webadmin.AdminStatsFragment): docFactory = loaders.stan(tags.div(render=tags.directive('liveElement'))) def _initializeObserver(self): pass def getGraphNames(self): return [(u"graph1", u"graph 1"), (u"graph2", u"graph 2")] athena.expose(getGraphNames) def fetchLastHour(self, name): t = extime.Time() return ([unicode((t + datetime.timedelta(minutes=i)).asHumanly()) for i in range(60)], [24, 28, 41, 37, 39, 25, 44, 32, 41, 45, 44, 47, 24, 28, 29, 49, 43, 56, 28, 35, 66, 43, 72, 65, 62, 56, 84, 52, 74, 73, 74, 77, 71, 46, 70, 55, 65, 51, 42, 55, 19, 30, 25, 24, 20, 16, 39, 22, 39, 29, 29, 18, 39, 19, 21, 12, 25, 25, 25, 29]) def buildPie(self): self.queryStats = {u'beans': 10, u'enchiladas': 27, u'salsa': 3, u'fajitas': 48} self.pieSlices() athena.expose(buildPie) class StatsTestCase(testcase.TestCase): jsClass = u'Mantissa.Test.StatsTest' docFactory = loaders.stan( tags.div(render=tags.directive('liveTest'))[ tags.invisible(render=tags.directive('start'))]) def render_start(self, ctx, data): self.testfragment = AdminStatsTestBase() self.testfragment.setFragmentParent(self) return self.testfragment def run(self): self.testfragment.statUpdate(extime.Time(), [(u'graph1', 43)]) self.testfragment.queryStatUpdate(extime.Time(), [(u'beans', 2)]) athena.expose(run) PK9F!!̳::xmantissa/test/peopleutil.py""" Helpful utilities for code which tests functionality related to L{xmantissa.people}. """ from zope.interface import implements from twisted.python.reflect import qual from axiom.store import Store from axiom.item import Item from axiom.attributes import inmemory, text from xmantissa.ixmantissa import IPeopleFilter, IContactType, IWebTranslator from xmantissa.people import Organizer from epsilon.descriptor import requiredAttribute class PeopleFilterTestMixin: """ Mixin for testing L{IPeopleFilter} providers. Requires the following attributes: @ivar peopleFilterClass: The L{IPeopleFilter} being tested. @type peopleFilterClass: L{IPeopleFilter} provider. @ivar peopleFilterName: The expected name of L{peopleFilterClass}. @type peopleFilterName: C{str} """ peopleFilterClass = requiredAttribute('peopleFilterClass') peopleFilterName = requiredAttribute('peopleFilterName') def assertComparisonEquals(self, comparison): """ Instantiate L{peopleFilterClass}, call L{IPeopleFilter.getPeopleQueryComparison} on it and assert that its result is equal to C{comparison}. @type comparison: L{axiom.iaxiom.IComparison} """ peopleFilter = self.peopleFilterClass() actualComparison = peopleFilter.getPeopleQueryComparison(Store()) # none of the Axiom query objects have meaningful equality # comparisons, but their string representations do. # this assertion should be addressed along with #2464 self.assertEqual(str(actualComparison), str(comparison)) def makeOrganizer(self): """ Return an L{Organizer}. """ return Organizer(store=Store()) def test_implementsInterface(self): """ Our people filter should provide L{IPeopleFilter}. """ self.assertTrue(IPeopleFilter.providedBy(self.peopleFilterClass())) def test_organizerIncludesIt(self): """ L{Organizer.getPeopleFilters} should include an instance of our L{IPeopleFilter}. """ organizer = self.makeOrganizer() self.assertIn( self.peopleFilterClass, [filter.__class__ for filter in organizer.getPeopleFilters()]) def test_filterName(self): """ Our L{IPeopleFilter}'s I{filterName} should match L{peopleFilterName}. """ self.assertEqual( self.peopleFilterClass().filterName, self.peopleFilterName) class StubPerson(object): """ Stub implementation of L{Person} used for testing. @ivar contactItems: A list of three-tuples of the arguments passed to createContactInfoItem. """ name = u'person' def __init__(self, contactItems): self.contactItems = contactItems self.store = object() def createContactInfoItem(self, cls, attr, value): """ Record the creation of a new contact item. """ self.contactItems.append((cls, attr, value)) def getContactInfoItems(self, itemType, valueColumn): """ Return an empty list. """ return [] def getMugshot(self): """ Return C{None} since there is no mugshot. """ return None def getDisplayName(self): """ Return a name of some sort. """ return u"Alice" def getEmailAddress(self): """ Return an email address. """ return u"alice@example.com" class StubTranslator(object): """ Translate between a dummy row identifier and a dummy object. """ implements(IWebTranslator) def __init__(self, rowIdentifier, item): self.fromWebID = {rowIdentifier: item}.__getitem__ self.toWebID = {item: rowIdentifier}.__getitem__ class StubOrganizer(object): """ Mimic some of the API presented by L{Organizer}. @ivar people: A C{dict} mapping C{unicode} strings giving person names to person objects. These person objects will be returned from appropriate calls to L{personByName}. @ivar contactTypes: a C{list} of L{IContactType}s. @ivar groupedReadOnlyViews: The C{dict} to be returned from L{groupReadOnlyViews} @ivar editedPeople: A list of the arguments which have been passed to the C{editPerson} method. @ivar deletedPeople: A list of the arguments which have been passed to the C{deletePerson} method. @ivar contactEditorialParameters: A mapping of people to lists. When passed a person, L{getContactEditorialParameters} will return the corresponding list. @ivar groupedReadOnlyViewPeople: A list of the arguments passed to L{groupReadOnlyViews}. @ivar peopleTags: The value to return from L{getPeopleTags}. @type peopleTags: C{list} @ivar peopleFilters: The sequence to return from L{getPeopleFilters}. @type peopleFilters: C{list} @ivar organizerPlugins: The sequence of return from L{getOrganizerPlugins}. @type organizerPlugins: C{list} """ _webTranslator = StubTranslator(None, None) def __init__(self, store=None, contactTypes=None, deletedPeople=None, editedPeople=None, contactEditorialParameters=None, groupedReadOnlyViews=None, peopleTags=None, peopleFilters=None, organizerPlugins=None): self.store = store self.people = {} if contactTypes is None: contactTypes = [] if deletedPeople is None: deletedPeople = [] if editedPeople is None: editedPeople = [] if contactEditorialParameters is None: contactEditorialParameters = [] if groupedReadOnlyViews is None: groupedReadOnlyViews = {} if peopleTags is None: peopleTags = [] if peopleFilters is None: peopleFilters = [] if organizerPlugins is None: organizerPlugins = [] self.contactTypes = contactTypes self.deletedPeople = deletedPeople self.editedPeople = editedPeople self.contactEditorialParameters = contactEditorialParameters self.groupedReadOnlyViews = groupedReadOnlyViews self.groupedReadOnlyViewPeople = [] self.peopleTags = peopleTags self.peopleFilters = peopleFilters self.organizerPlugins = organizerPlugins def personByName(self, name): return self.people[name] def lastNameOrder(self): return None def deletePerson(self, person): self.deletedPeople.append(person) def editPerson(self, person, name, edits): self.editedPeople.append((person, name, edits)) def toContactEditorialParameter(self, contactType, person): for (_contactType, param) in self.contactEditorialParameters[person]: if _contactType == contactType: return param def getContactEditorialParameters(self, person): return self.contactEditorialParameters[person] def getContactTypes(self): return self.contactTypes def getPeopleFilters(self): """ Return L{peopleFilters}. """ return self.peopleFilters def groupReadOnlyViews(self, person): """ Return L{groupedReadOnlyViews}. """ self.groupedReadOnlyViewPeople.append(person) return self.groupedReadOnlyViews def linkToPerson(self, person): return "/person/" + person.getDisplayName() def getPeopleTags(self): """ Return L{peopleTags}. """ return self.peopleTags def getOrganizerPlugins(self): """ Return L{organizerPlugins}. """ return self.organizerPlugins class StubOrganizerPlugin(Item): """ Organizer powerup which records which people are created and gives back canned responses to method calls. """ name = text( doc=""" @see IOrganizerPlugin.name """) createdPeople = inmemory( doc=""" A list of all L{Person} items created since this item was last loaded from the database. """) contactTypes = inmemory( doc=""" A list of L{IContactType} implementors to return from L{getContactTypes}. """) peopleFilters = inmemory( doc=""" A list of L{IPeopleFilter} imlpementors to return from L{getPeopleFilters}. """) renamedPeople = inmemory( doc=""" A list of two-tuples of C{unicode} with the first element giving the name of each L{Person} item whose name changed at the time of the change and the second element giving the value passed for the old name parameter. """) createdContactItems = inmemory( doc=""" A list of contact items created since this item was last loaded from the database. """) editedContactItems = inmemory( doc=""" A list of contact items edited since this item was last loaded from the database. """) personalization = inmemory( doc=""" An objects to be returned by L{personalize}. """) personalizedPeople = inmemory( doc=""" A list of people passed to L{personalize}. """) def activate(self): """ Initialize in-memory state tracking attributes to default values. """ self.createdPeople = [] self.renamedPeople = [] self.createdContactItems = [] self.editedContactItems = [] self.personalization = None self.personalizedPeople = [] def personCreated(self, person): """ Record the creation of a L{Person}. """ self.createdPeople.append(person) def personNameChanged(self, person, oldName): """ Record the change of a L{Person}'s name. """ self.renamedPeople.append((person.name, oldName)) def contactItemCreated(self, contactItem): """ Record the creation of a contact item. """ self.createdContactItems.append(contactItem) def contactItemEdited(self, contactItem): """ Record the editing of a contact item. """ self.editedContactItems.append(contactItem) def getContactTypes(self): """ Return the contact types list this item was constructed with. """ return self.contactTypes def getPeopleFilters(self): """ Return L{peopleFilters}. """ return self.peopleFilters def personalize(self, person): """ Record a personalization attempt and return C{self.personalization}. """ self.personalizedPeople.append(person) return self.personalization class StubReadOnlyView(object): """ Test double for the objects returned by L{IContactType.getReadOnlyView}. @ivar item: The contact item the view is for. @ivar type: The contact type the contact item comes from. """ def __init__(self, contactItem, contactType): self.item = contactItem self.type = contactType class StubContactType(object): """ Behaviorless contact type implementation used for tests. @ivar parameters: A list of L{xmantissa.liveform.Parameter} instances which will become the return value of L{getParameters}. @ivar createdContacts: A list of tuples of the arguments passed to C{createContactItem}. @ivar editorialForm: The object which will be returned from L{getEditFormForPerson}. @ivar editedContacts: A list of the contact items passed to L{getEditFormForPerson}. @ivar contactItems: The list of objects which will be returned from L{getContactItems}. @ivar queriedPeople: A list of the person items passed to L{getContactItems}. @ivar editedContacts: A list of two-tuples of the arguments passed to L{editContactItem}. @ivar createContactItems: A boolean indicating whether C{createContactItem} will return an object pretending to be a new contact item (C{True}) or C{None} to indicate no contact item was created (C{False}). @ivar theDescriptiveIdentifier: The object to return from L{descriptiveIdentifier}. @ivar contactGroup: The object to return from L{getContactGroup}. """ implements(IContactType) def __init__(self, parameters, editorialForm, contactItems, createContactItems=True, allowMultipleContactItems=True, theDescriptiveIdentifier=u'', contactGroup=None): self.parameters = parameters self.createdContacts = [] self.editorialForm = editorialForm self.editedContacts = [] self.contactItems = contactItems self.queriedPeople = [] self.editedContacts = [] self.createContactItems = createContactItems self.allowMultipleContactItems = allowMultipleContactItems self.theDescriptiveIdentifier = theDescriptiveIdentifier self.contactGroup = contactGroup def getParameters(self, ignore): """ Return L{parameters}. """ return self.parameters def uniqueIdentifier(self): """ Return the L{qual} of this class. """ return qual(self.__class__).decode('ascii') def descriptiveIdentifier(self): """ Return L{theDescriptiveIdentifier}. """ return self.theDescriptiveIdentifier def getEditFormForPerson(self, contact): """ Return an object which is supposed to be a form for editing an existing instance of this contact type and record the contact object which was passed in. """ self.editedContacts.append(contact) return self.editorialForm def createContactItem(self, person, **parameters): """ Record an attempt to create a new contact item of this type for the given person. """ contactItem = (person, parameters) self.createdContacts.append(contactItem) if self.createContactItems: return contactItem return None def getContactItems(self, person): """ Return C{self.contactItems} and record the person item passed in. """ self.queriedPeople.append(person) return self.contactItems def editContactItem(self, contact, **changes): """ Record an attempt to edit the details of a contact item. """ self.editedContacts.append((contact, changes)) def getContactGroup(self, contactItem): """ Return L{contactGroup}. """ return self.contactGroup def getReadOnlyView(self, contact): """ Return a stub view object for the given contact. @rtype: L{StubReadOnlyView} """ return StubReadOnlyView(contact, self) PK9Fxmantissa/test/rendertools.py# -*- test-case-name: xmantissa.test.test_rendertools -*- """ Simple Nevow-related rendering helpers for use in view tests only. """ from nevow.rend import Page from nevow.athena import LivePage from nevow.loaders import stan from nevow.testutil import FakeRequest from nevow.context import WovenContext from nevow.inevow import IRequest class TagTestingMixin: """ Mixin defining various useful tag-related testing functionality. """ def assertTag(self, tag, name, attributes, children): """ Assert that the given tag has the given name, attributes, and children. """ self.assertEqual(tag.tagName, name) self.assertEqual(tag.attributes, attributes) self.assertEqual(tag.children, children) def _makeContext(): """ Create the request and context objects necessary for rendering a page. @return: A two-tuple of the created L{FakeRequest} and L{WovenContext}, with the former remembered in the latter. """ request = FakeRequest() context = WovenContext() context.remember(request, IRequest) return (request, context) def renderLiveFragment(fragment): """ Render the given fragment in a LivePage. This can only work for fragments which can be rendered synchronously. Fragments which involve Deferreds will be silently rendered incompletely. @type fragment: L{nevow.athena.LiveFragment} or L{nevow.athena.LiveElement} @param fragment: The page component to render. @rtype: C{str} @return: The result of rendering the fragment. """ page = LivePage(docFactory=stan(fragment)) fragment.setFragmentParent(page) (request, context) = _makeContext() page.renderHTTP(context) page.action_close(context) return request.v def renderPlainFragment(fragment): """ same as L{render}, but expects an L{nevow.rend.Fragment} or any other L{nevow.inevow.IRenderer} """ page = Page(docFactory=stan(fragment)) (request, context) = _makeContext() page.renderHTTP(context) return request.v PK9Fܪ  xmantissa/test/test_admin.py""" Test cases for the L{xmantissa.webadmin} module. """ from twisted.trial.unittest import TestCase from nevow.athena import LivePage from nevow.context import WovenContext from nevow.testutil import FakeRequest from nevow.loaders import stan from nevow.tags import html, head, body, directive from nevow.inevow import IRequest from axiom.store import Store from axiom.userbase import LoginSystem, LoginMethod from axiom.dependency import installOn from axiom.plugins.mantissacmd import Mantissa from xmantissa.webadmin import ( LocalUserBrowser, LocalUserBrowserFragment, UserInteractionFragment, EndowFragment, DepriveFragment, SuspendFragment, UnsuspendFragment) from xmantissa.product import Product class UserInteractionFragmentTestCase(TestCase): def setUp(self): """ Create a site store and a user store with a L{LocalUserBrowser} installed on it. """ self.siteStore = Store() self.loginSystem = LoginSystem(store=self.siteStore) installOn(self.loginSystem, self.siteStore) self.userStore = Store() self.userStore.parent = self.siteStore self.browser = LocalUserBrowser(store=self.userStore) def test_createUser(self): """ Test that L{webadmin.UserInteractionFragment.createUser} method actually creates a user. """ userInteractionFragment = UserInteractionFragment(self.browser) userInteractionFragment.createUser( u'testuser', u'localhost', u'password') account = self.loginSystem.accountByAddress(u'testuser', u'localhost') self.assertEquals(account.password, u'password') def test_rendering(self): """ Test that L{webadmin.UserInteractionFragment} renders without raising any exceptions. """ f = UserInteractionFragment(self.browser) p = LivePage( docFactory=stan( html[ head(render=directive('liveglue')), body(render=lambda ctx, data: f)])) f.setFragmentParent(p) ctx = WovenContext() req = FakeRequest() ctx.remember(req, IRequest) d = p.renderHTTP(ctx) def rendered(ign): p.action_close(None) d.addCallback(rendered) return d class ActionsTestCase(TestCase): """ Tests to verify that actions behave as expected. @ivar siteStore: A site store containing an administrative user's account. @ivar siteAccount: The L{axiom.userbase.LoginAccount} for the administrator, in the site store. @ivar siteMethod: The single L{axiom.userbase.LoginMethod} for the administrator, in the site store. @ivar localUserBrowserFragment: A L{LocalUserBrowserFragment} examining the administrator's L{LocalUserBrowser} powerup. """ def setUp(self): """ Construct a site and user store with an administrator that can invoke the web administrative tools, setting the instance variables described in this class's docstring. """ self.siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(self.siteStore, u"localhost", u"", False) Mantissa().installAdmin(self.siteStore, u'admin', u'localhost', u'asdf') self.siteMethod = self.siteStore.findUnique( LoginMethod, LoginMethod.localpart == u'admin') self.siteAccount = self.siteMethod.account userStore = self.siteAccount.avatars.open() lub = userStore.findUnique(LocalUserBrowser) self.localUserBrowserFragment = LocalUserBrowserFragment(lub) def test_actionTypes(self): """ Verify that all the action methods expose the appropriate fragment objects, with their attributes set to indicate the correct objects to manipulate. """ myRowID = self.localUserBrowserFragment.linkToItem(self.siteMethod) actionMap = [('installOn', EndowFragment), ('uninstallFrom', DepriveFragment), ('suspend', SuspendFragment), ('unsuspend', UnsuspendFragment)] for action, fragmentType in actionMap: resultFragment = self.localUserBrowserFragment.performAction( action, myRowID) self.failUnless(isinstance(resultFragment, fragmentType), "%s does not return a %s" % (action, fragmentType)) self.assertEquals(resultFragment.fragmentParent, self.localUserBrowserFragment) self.assertEquals(resultFragment.account, self.siteAccount) class RenderingTestCase(TestCase): """ Test cases for HTML rendering of various fragments. """ def doRendering(self, fragmentClass): """ Verify that the given fragment class will render without raising an exception. """ siteStore = Store() loginSystem = LoginSystem(store=siteStore) installOn(loginSystem, siteStore) p = Product(store=siteStore, types=["xmantissa.webadmin.LocalUserBrowser", "xmantissa.signup.SignupConfiguration"]) account = loginSystem.addAccount(u'testuser', u'localhost', None) p.installProductOn(account.avatars.open()) f = fragmentClass(None, u'testuser', account) p = LivePage( docFactory=stan( html[ head(render=directive('liveglue')), body(render=lambda ctx, data: f)])) f.setFragmentParent(p) ctx = WovenContext() req = FakeRequest() ctx.remember(req, IRequest) d = p.renderHTTP(ctx) def rendered(ign): p.action_close(None) d.addCallback(rendered) return d def test_endowRendering(self): """ Verify that L{EndowFragment} can render without raising an exception. """ return self.doRendering(EndowFragment) def test_depriveRendering(self): """ Verify that L{DepriveFragment} can render without raising an exception. """ return self.doRendering(DepriveFragment) def test_suspendRendering(self): """ Verify that L{SuspendFragment} can render without raising an exception. """ return self.doRendering(SuspendFragment) def test_unsuspendRendering(self): """ Verify that L{UnsuspendFragment} can render without raising an exception. """ return self.doRendering(UnsuspendFragment) PK9F"a99 xmantissa/test/test_ampserver.py# Copyright (c) 2008 Divmod. See LICENSE for details. """ Tests for L{xmantissa.ampserver}. """ from zope.interface import Interface, implements from zope.interface.verify import verifyObject from twisted.python.failure import Failure from twisted.internet.defer import Deferred from twisted.internet.protocol import ServerFactory from twisted.internet.task import Clock from twisted.internet import reactor from twisted.cred.credentials import UsernamePassword from twisted.protocols.amp import ASK, COMMAND, IBoxReceiver from twisted.trial.unittest import TestCase from epsilon.ampauth import CredReceiver from epsilon.amprouter import _ROUTE from epsilon.test.test_amprouter import SomeReceiver, CollectingSender from epsilon.iepsilon import IOneTimePad from axiom.item import Item from axiom.store import Store from axiom.attributes import text, inmemory from axiom.dependency import installOn from axiom.userbase import LoginSystem from xmantissa.ixmantissa import IProtocolFactoryFactory, IBoxReceiverFactory from xmantissa.ampserver import ( _RouteConnector, AMPConfiguration, AMPAvatar, ProtocolUnknown, Router, Connect, connectRoute, EchoFactory, EchoReceiver) __metaclass__ = type class AMPConfigurationTests(TestCase): """ Tests for L{xmantissa.ampserver.AMPConfiguration} which defines how to create an L{AMP} server. """ def setUp(self): """ Create an in-memory L{Store} with an L{AMPConfiguration} in it. """ self.store = Store() self.conf = AMPConfiguration(store=self.store) installOn(self.conf, self.store) def test_interfaces(self): """ L{AMPConfiguration} implements L{IProtocolFactoryFactory}. """ self.assertTrue(verifyObject(IProtocolFactoryFactory, self.conf)) def test_powerup(self): """ L{ionstallOn} powers up the target for L{IProtocolFactoryFactory} with L{AMPConfiguration}. """ self.assertIn( self.conf, list(self.store.powerupsFor(IProtocolFactoryFactory))) def test_getFactory(self): """ L{AMPConfiguration.getFactory} returns a L{ServerFactory} instance which returns L{CredReceiver} instances from its C{buildProtocol} method. """ factory = self.conf.getFactory() self.assertTrue(isinstance(factory, ServerFactory)) protocol = factory.buildProtocol(None) self.assertTrue(isinstance(protocol, CredReceiver)) def test_generateOneTimePad(self): """ L{AMPConfiguration.generateOneTimePad} returns a one-time pad. """ object.__setattr__(self.conf, 'callLater', lambda x, y: None) pad = self.conf.generateOneTimePad(self.store) self.assertNotEqual( pad, self.conf.generateOneTimePad(self.store)) def test_oneTimePadExpires(self): """ L{AMPConfiguration.generateOneTimePad} should expire its pad. """ def callLater(seconds, f): self.assertEqual( seconds, self.conf.ONE_TIME_PAD_DURATION) f() object.__setattr__(self.conf, 'callLater', callLater) pad = self.conf.generateOneTimePad(self.store) self.assertFalse(pad in self.conf._oneTimePads) class AMPConfigurationSubStoreTests(TestCase): """ Tests for L{AMPConfiguration} which require a substore. """ def setUp(self): """ Create an in-memory L{Store} with an L{AMPConfiguration} in it, and a substore. """ self.store = Store() self.conf = AMPConfiguration(store=self.store) installOn(self.conf, self.store) self.localpart = u'alice' self.domain = u'example.org' self.password = u'foobar' loginSystem = self.store.findUnique(LoginSystem) account = loginSystem.addAccount( self.localpart, self.domain, self.password, internal=True) self.subStore = account.avatars.open() def _testPortalLogin(self, credentials): factory = self.conf.getFactory() protocol = factory.buildProtocol(None) portal = protocol.portal class IDummy(Interface): pass avatar = object() self.subStore.inMemoryPowerUp(avatar, IDummy) login = portal.login(credentials, None, IDummy) def cbLoggedIn(result): self.assertIdentical(IDummy, result[0]) self.assertIdentical(avatar, result[1]) login.addCallback(cbLoggedIn) return login def test_portal(self): """ L{AMPConfiguration.getFactory} returns a factory which creates protocols which have a C{portal} attribute which is a L{Portal} which authenticates and authorizes using L{axiom.userbase}. """ return self._testPortalLogin( UsernamePassword( '%s@%s' % (self.localpart.encode('ascii'), self.domain.encode('ascii')), self.password),) def test_portalOneTimePad(self): """ L{AMPConfiguration.getFactory} returns a factory which creates protocols which have a C{portal} attribute which is a L{Portal} which can authenticate using one-time pads. """ object.__setattr__(self.conf, 'callLater', lambda x, y: None) PAD = self.conf.generateOneTimePad(self.subStore) class OTP: implements(IOneTimePad) padValue = PAD return self._testPortalLogin(OTP()) class StubBoxReceiverFactory(Item): """ L{IBoxReceiverFactory} """ protocol = text() receivers = inmemory() receiverFactory = SomeReceiver def activate(self): self.receivers = [] def getBoxReceiver(self): receiver = self.receiverFactory() self.receivers.append(receiver) return receiver class AMPAvatarTests(TestCase): """ Tests for L{AMPAvatar} which provides an L{IBoxReceiver} implementation that supports routing messages to other L{IBoxReceiver} implementations. """ def setUp(self): """ Create a L{Store} with an L{AMPAvatar} installed on it. """ self.store = Store() self.avatar = AMPAvatar(store=self.store) installOn(self.avatar, self.store) self.factory = StubBoxReceiverFactory( store=self.store, protocol=u"bar") self.store.powerUp(self.factory, IBoxReceiverFactory) def test_interface(self): """ L{AMPAvatar} powers up the item on which it is installed for L{IBoxReceiver} and indirects that interface to the real router implementation of L{IBoxReceiver}. """ router = IBoxReceiver(self.store) self.assertTrue(verifyObject(IBoxReceiver, router)) self.assertNotIdentical(self.avatar, router) def test_connectorStarted(self): """ L{AMPAvatar.indirect} returns a L{Router} with a started route connector as its default receiver. """ receiver = SomeReceiver() self.avatar.__dict__['connectorFactory'] = lambda router: receiver router = self.avatar.indirect(IBoxReceiver) router.startReceivingBoxes(object()) self.assertTrue(receiver.started) def test_reactor(self): """ L{AMPAvatar.connectorFactory} returns a L{_RouteConnector} constructed using the global reactor. """ connector = self.avatar.connectorFactory(object()) self.assertIdentical(connector.reactor, reactor) class RouteConnectorTests(TestCase): """ Tests for L{_RouteConnector}. """ def setUp(self): """ Create a L{Store} with an L{AMPAvatar} installed on it. """ self.clock = Clock() self.store = Store() self.factory = StubBoxReceiverFactory( store=self.store, protocol=u"bar") self.store.powerUp(self.factory, IBoxReceiverFactory) self.router = Router() self.sender = CollectingSender() self.connector = _RouteConnector(self.clock, self.store, self.router) self.router.startReceivingBoxes(self.sender) self.router.bindRoute(self.connector, None).connectTo(None) def test_accept(self): """ L{_RouteConnector.accept} returns a C{dict} with a C{'route'} key associated with a new route identifier which may be used to send AMP boxes to a new instance of the L{IBoxReceiver} indicated by the C{protocol} argument passed to C{connect}. """ firstIdentifier = self.connector.accept( "first origin", u"bar")['route'] firstReceiver = self.factory.receivers.pop() secondIdentifier = self.connector.accept( "second origin", u"bar")['route'] secondReceiver = self.factory.receivers.pop() self.clock.advance(0) self.router.ampBoxReceived( {_ROUTE: firstIdentifier, 'foo': 'bar'}) self.router.ampBoxReceived( {_ROUTE: secondIdentifier, 'baz': 'quux'}) self.assertEqual(firstReceiver.boxes, [{'foo': 'bar'}]) self.assertEqual(secondReceiver.boxes, [{'baz': 'quux'}]) def test_acceptResponseBeforeApplicationBox(self): """ If the protocol L{_RouteConnector.accept} binds to a new route sends a box in its C{startReceivingBoxes} method, that box is sent to the network after the I{Connect} response is sent. """ earlyBox = {'foo': 'bar'} class EarlyReceiver: def startReceivingBoxes(self, sender): sender.sendBox(earlyBox) object.__setattr__(self.factory, 'receiverFactory', EarlyReceiver) self.connector.ampBoxReceived({ COMMAND: Connect.commandName, ASK: 'unique-identifier', 'origin': 'an origin', 'protocol': 'bar'}) self.clock.advance(0) self.assertEqual(len(self.sender.boxes), 2) route, app = self.sender.boxes expectedBox = earlyBox.copy() expectedBox[_ROUTE] = 'an origin' self.assertEqual(app, expectedBox) def test_unknownProtocol(self): """ L{_RouteConnector.accept} raises L{ProtocolUnknown} if passed the name of a protocol for which no factory can be found. """ self.assertRaises( ProtocolUnknown, self.connector.accept, "origin", u"foo") def test_originRoute(self): """ The L{IBoxReceiver}s created by L{_RouteConnector.accept} are started with L{IBoxSender}s which are associated with the origin route specified to C{accept}. """ origin = u'origin route' self.connector.accept(origin, u'bar') self.clock.advance(0) [bar] = self.factory.receivers self.assertTrue(bar.started) bar.sender.sendBox({'foo': 'bar'}) self.assertEqual(self.sender.boxes, [{_ROUTE: origin, 'foo': 'bar'}]) bar.sender.unhandledError(Failure(RuntimeError("test failure"))) self.assertEqual(self.sender.errors, ["test failure"]) class ConnectRouteTests(TestCase): """ Tests for L{connectRoute} which implements Mantissa-specific route creation logic. """ def test_connectRoute(self): """ L{connectRoute} takes an L{AMP}, a L{Router}, an L{IBoxReceiver} and a protocol name and issues a L{Connect} command for that protocol and for a newly created route associated with the given receiver over the L{AMP}. """ commands = [] results = [] class FakeAMP: def callRemote(self, cmd, **kw): commands.append((cmd, kw)) results.append(Deferred()) return results[-1] amp = FakeAMP() sender = CollectingSender() router = Router() router.startReceivingBoxes(sender) receiver = SomeReceiver() protocol = u"proto name" d = connectRoute(amp, router, receiver, protocol) self.assertEqual( commands, [(Connect, {'origin': u'0', 'protocol': u'proto name'})]) results[0].callback({'route': u'remote route'}) def cbConnected(receiverAgain): self.assertIdentical(receiver, receiverAgain) self.assertTrue(receiver.started) receiver.sender.sendBox({'foo': 'bar'}) self.assertEqual( sender.boxes, [{_ROUTE: 'remote route', 'foo': 'bar'}]) router.ampBoxReceived({_ROUTE: '0', 'baz': 'quux'}) self.assertEqual(receiver.boxes, [{'baz': 'quux'}]) d.addCallback(cbConnected) return d class BoxReceiverFactoryPowerupTestMixin: """ Common tests for implementors of L{IBoxReceiverFactory}. @ivar factoryClass: the L{IBoxReceiverFactory} implementor. @ivar protocolClass: An L{IBoxReceiver} implementor. """ def test_factoryInterfaces(self): """ C{self.factoryClass} instances provide L{IBoxReceiverFactory}. """ self.assertTrue(verifyObject(IBoxReceiverFactory, self.factoryClass())) def test_factoryPowerup(self): """ When installed, C{self.factoryClass} is a powerup for L{IBoxReceiverFactory}. """ store = Store() factory = self.factoryClass(store=store) installOn(factory, store) self.assertEqual( list(store.powerupsFor(IBoxReceiverFactory)), [factory]) def test_getBoxReceiver(self): """ C{self.factoryClass.getBoxReceiver} returns an instance of C{self.protocolClass}. """ receiver = self.factoryClass().getBoxReceiver() self.assertTrue(isinstance(receiver, self.protocolClass)) def test_receiverInterfaces(self): """ C{self.protocolClass} instances provide L{IBoxReceiver}. """ self.assertTrue(verifyObject(IBoxReceiver, self.protocolClass())) class EchoTests(BoxReceiverFactoryPowerupTestMixin, TestCase): """ Tests for L{EchoFactory} and L{EchoReceiver}, classes which provide a simple AMP echo protocol for a Mantissa AMP server. """ factoryClass = EchoFactory protocolClass = EchoReceiver def test_ampBoxReceived(self): """ L{EchoReceiver.ampBoxReceived} sends the received box back to the sender. """ sender = CollectingSender() receiver = EchoReceiver() receiver.startReceivingBoxes(sender) receiver.ampBoxReceived({'foo': 'bar'}) self.assertEqual(sender.boxes, [{'foo': 'bar'}]) receiver.stopReceivingBoxes(Failure(Exception("test exception"))) PK9F[Sf" " xmantissa/test/test_cachejs.pyfrom hashlib import sha1 from twisted.trial.unittest import TestCase from twisted.python.filepath import FilePath from nevow.inevow import IRequest from nevow.context import WovenContext from nevow.testutil import FakeRequest from xmantissa.cachejs import HashedJSModuleProvider, CachedJSModule class JSCachingTestCase(TestCase): """ Tests for L{xmantissa.cachejs}. """ hostname = 'test-mantissa-js-caching.example.com' def setUp(self): """ Create a L{HashedJSModuleProvider} and a dummy module. """ self.MODULE_NAME = 'Dummy.Module' self.MODULE_CONTENT = '/* Hello, world. /*\n' self.moduleFile = self.mktemp() fObj = file(self.moduleFile, 'w') fObj.write(self.MODULE_CONTENT) fObj.close() m = HashedJSModuleProvider() self.moduleProvider = m self._wasModified = CachedJSModule.wasModified.im_func self.callsToWasModified = 0 def countCalls(other): self.callsToWasModified += 1 return self._wasModified(other) CachedJSModule.wasModified = countCalls def tearDown(self): """ put L{CachedJSModule} back the way we found it """ CachedJSModule.wasModified = self._wasModified def _render(self, resource): """ Test helper which tries to render the given resource. """ ctx = WovenContext() req = FakeRequest(headers={'host': self.hostname}) ctx.remember(req, IRequest) return req, resource.renderHTTP(ctx) def test_hashExpiry(self): """ L{HashedJSModuleProvider.resourceFactory} should return a L{static.Data} with an C{expires} value far in the future. """ self.moduleProvider.moduleCache[self.MODULE_NAME] = CachedJSModule( self.MODULE_NAME, FilePath(self.moduleFile)) d, segs = self.moduleProvider.locateChild(None, [sha1(self.MODULE_CONTENT).hexdigest(), self.MODULE_NAME]) self.assertEqual([], segs) d.time = lambda: 12345 req, result = self._render(d) self.assertEquals( req.headers['expires'], 'Tue, 31 Dec 1974 03:25:45 GMT') self.assertEquals( result, '/* Hello, world. /*\n') def test_getModule(self): """ L{HashedJSModuleProvider.getModule} should only load modules once; subsequent calls should return the cached module object. """ module = self.moduleProvider.getModule("Mantissa.Test.Dummy") self.failUnlessIdentical(module, self.moduleProvider.getModule( "Mantissa.Test.Dummy")) def test_dontStat(self): """ L{HashedJSModuleProvider.getModule} shouldn't hit the disk more than once per module. """ module1 = self.moduleProvider.getModule("Mantissa.Test.Dummy") module2 = self.moduleProvider.getModule("Mantissa.Test.Dummy") self.assertEqual(self.callsToWasModified, 1) PK9F&>wwxmantissa/test/test_fulltext.py from zope.interface import implements from twisted.trial import unittest from twisted.application.service import IService from twisted.internet.defer import gatherResults from axiom import iaxiom, store, batch, item, attributes from axiom.userbase import LoginSystem from axiom.dependency import installOn from axiom.errors import SQLError from xmantissa import ixmantissa, fulltext def identifiersFrom(hits): """ Convert iterable of hits into list of integer unique identifiers. """ return [int(h.uniqueIdentifier) for h in hits] class IndexableThing(item.Item): implements(ixmantissa.IFulltextIndexable) _uniqueIdentifier = attributes.bytes() _textParts = attributes.inmemory() _keywordParts = attributes.inmemory() _documentType = attributes.inmemory() def uniqueIdentifier(self): return self._uniqueIdentifier def textParts(self): return self._textParts def keywordParts(self): return self._keywordParts def documentType(self): return self._documentType def sortKey(self): return self.uniqueIdentifier() class FakeMessageSource(item.Item): """ Stand-in for an item type returned from L{axiom.batch.processor}. Doesn't actually act as a source of anything, just used to test that items are kept track of properly. """ anAttribute = attributes.text(doc=""" Nothing. Axiom requires at least one attribute per item-type. """) added = attributes.inmemory() removed = attributes.inmemory() def activate(self): self.added = [] self.removed = [] def addReliableListener(self, what, style): self.added.append((what, style)) def removeReliableListener(self, what): self.removed.append(what) class IndexerTestsMixin: def createIndexer(self): raise NotImplementedError() def openWriteIndex(self): try: return self.indexer.openWriteIndex() except NotImplementedError, e: raise unittest.SkipTest(str(e)) def openReadIndex(self): try: return self.indexer.openReadIndex() except NotImplementedError, e: raise unittest.SkipTest(str(e)) def setUp(self): self.path = u'index' self.store = store.Store(filesdir=self.mktemp()) self.indexer = self.createIndexer() class FulltextTestsMixin(IndexerTestsMixin): """ Tests for any IFulltextIndexer provider. """ def testSources(self): """ Test that multiple IBatchProcessors can be added to a RemoteIndexer and that an indexer can be reset, with respect to input from its sources. """ firstSource = FakeMessageSource(store=self.store) secondSource = FakeMessageSource(store=self.store) self.indexer.addSource(firstSource) self.indexer.addSource(secondSource) self.assertEquals(firstSource.added, [(self.indexer, iaxiom.REMOTE)]) self.assertEquals(secondSource.added, [(self.indexer, iaxiom.REMOTE)]) self.assertEquals( list(self.indexer.getSources()), [firstSource, secondSource]) firstSource.added = [] secondSource.added = [] self.indexer.reset() self.assertEquals(firstSource.removed, [self.indexer]) self.assertEquals(secondSource.removed, [self.indexer]) self.assertEquals(firstSource.added, [(self.indexer, iaxiom.REMOTE)]) self.assertEquals(secondSource.added, [(self.indexer, iaxiom.REMOTE)]) def test_emptySearch(self): """ Test that a search with no term and no keywords returns an empty result set. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='7', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='21', _textParts=[u'cherry', u'drosophila melanogaster'], _keywordParts={})) writer.close() reader = self.openReadIndex() results = list(reader.search(u'', {})) self.assertEquals(results, []) def testSimpleSerializedUsage(self): writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='7', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='21', _textParts=[u'cherry', u'drosophila melanogaster'], _keywordParts={})) writer.close() reader = self.openReadIndex() results = identifiersFrom(reader.search(u'apple')) self.assertEquals(results, [7]) results = identifiersFrom(reader.search(u'banana')) self.assertEquals(results, [7]) results = identifiersFrom(reader.search(u'cherry')) self.assertEquals(results, [21]) results = identifiersFrom(reader.search(u'drosophila')) self.assertEquals(results, [21]) results = identifiersFrom(reader.search(u'melanogaster')) self.assertEquals(results, [21]) reader.close() def testWriteReadWriteRead(self): writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='1', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.close() reader = self.openReadIndex() results = identifiersFrom(reader.search(u'apple')) self.assertEquals(results, [1]) results = identifiersFrom(reader.search(u'banana')) self.assertEquals(results, [1]) reader.close() writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='2', _textParts=[u'cherry', u'drosophila melanogaster'], _keywordParts={})) writer.close() reader = self.openReadIndex() results = identifiersFrom(reader.search(u'apple')) self.assertEquals(results, [1]) results = identifiersFrom(reader.search(u'banana')) self.assertEquals(results, [1]) results = identifiersFrom(reader.search(u'cherry')) self.assertEquals(results, [2]) results = identifiersFrom(reader.search(u'drosophila')) self.assertEquals(results, [2]) results = identifiersFrom(reader.search(u'melanogaster')) self.assertEquals(results, [2]) reader.close() def testReadBeforeWrite(self): reader = self.openReadIndex() results = identifiersFrom(reader.search(u'apple')) self.assertEquals(results, []) def test_remove(self): """ Test that the L{remove} method of an indexer successfully removes the item it is given from its index. """ item = IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'apple', u'banana'], _keywordParts={}) writer = self.openWriteIndex() writer.add(item) writer.close() self.indexer.remove(item) self.indexer._flush() reader = self.openReadIndex() self.assertEqual( identifiersFrom(reader.search(u'apple')), []) def test_removalFromReadIndex(self): """ Add a document to an index and then remove it, asserting that it no longer appears once it has been removed. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.close() reader = self.openReadIndex() reader.remove(u'50') reader.close() reader = self.openReadIndex() self.assertEqual( identifiersFrom(reader.search(u'apple')), []) def testKeywordIndexing(self): """ Test that an L{IFulltextIndexable}'s keyword parts can be searched for. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'apple', u'banana'], _keywordParts={u'subject': u'fruit'})) writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'airplane')), []) self.assertEquals( identifiersFrom(reader.search(u'fruit')), []) self.assertEquals( identifiersFrom(reader.search(u'apple')), [50]) self.assertEquals( identifiersFrom(reader.search(u'apple', {u'subject': u'fruit'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'subject': u'fruit'})), [50]) def test_typeRestriction(self): """ Test that the type of an IFulltextIndexable is automatically found when indexing and searching for items of a particular type limits the results appropriately. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'first', _uniqueIdentifier='1', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.add(IndexableThing( _documentType=u'second', _uniqueIdentifier='2', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'apple', {'documentType': u'first'})), [1]) self.assertEquals( identifiersFrom(reader.search(u'apple', {'documentType': u'second'})), [2]) self.assertEquals( identifiersFrom(reader.search(u'apple', {'documentType': u'three'})), []) def testKeywordTokenization(self): """ Keyword values should be tokenized just like text parts. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'apple', u'banana'], _keywordParts={u'subject': u'list of fruit things'})) writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'pear')), []) self.assertEquals( identifiersFrom(reader.search(u'fruit')), []) self.assertEquals( identifiersFrom(reader.search(u'apple')), [50]) self.assertEquals( identifiersFrom(reader.search(u'apple', {u'subject': u'fruit'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'subject': u'fruit'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'subject': u'list'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'subject': u'things'})), [50]) def testKeywordCombination(self): """ Multiple keyword searches should be AND'ed """ writer = self.openWriteIndex() def makeIndexable(uniqueIdentifier, **k): writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(uniqueIdentifier), _textParts=[], _keywordParts=dict((unicode(k), unicode(v)) for (k, v) in k.iteritems()))) makeIndexable(50, name='john', car='honda') makeIndexable(51, name='john', car='mercedes') writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'', {u'car': u'honda'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'car': u'mercedes'})), [51]) self.assertEquals( identifiersFrom(reader.search(u'', {u'name': u'john'})), [50, 51]) self.assertEquals( identifiersFrom(reader.search(u'', {u'name': u'john', u'car': u'honda'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'name': u'john', u'car': u'mercedes'})), [51]) def testKeywordValuesInPhrase(self): """ Keyword values should return results when included in the main phrase """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'my name is jack'], _keywordParts={u'car': u'honda'})) writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'honda', {})), []) self.assertEquals( identifiersFrom(reader.search(u'jack', {})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'car': u'honda'})), [50]) self.assertEquals( identifiersFrom(reader.search(u'', {u'car': u'jack'})), []) def testDigitSearch(self): """ Should get results if we search for digits that appear in indexed documents """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='50', _textParts=[u'123 456'], _keywordParts={})) writer.close() reader = self.openReadIndex() self.assertEquals( identifiersFrom(reader.search(u'123', {})), [50]) self.assertEquals( identifiersFrom(reader.search(u'456', {})), [50]) def testSorting(self): """ Index some stuff with out of order sort keys and ensure that they come back ordered by sort key. """ writer = self.openWriteIndex() keys = (5, 20, 6, 127, 2) for k in keys: writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(k), _textParts=[u'ok'], _keywordParts={})) writer.close() reader = self.openReadIndex() self.assertEquals(identifiersFrom(reader.search(u'ok')), list(sorted(keys))) def testSortAscending(self): """ Test that the C{sortAscending} parameter to C{search} is observed """ writer = self.openWriteIndex() for i in xrange(5): writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(i), _textParts=[u'ok'], _keywordParts={})) writer.close() reader = self.openReadIndex() self.assertEquals(identifiersFrom(reader.search(u'ok')), range(5)) self.assertEquals(identifiersFrom(reader.search(u'ok', sortAscending=True)), range(5)) self.assertEquals(identifiersFrom(reader.search(u'ok', sortAscending=False)), range(4, -1, -1)) class CorruptionRecoveryMixin(IndexerTestsMixin): def corruptIndex(self): raise NotImplementedError() def testRecoveryAfterFailure(self): """ Create an indexer, attach some sources to it, let it process some messages, corrupt the database, let it try to clean things up, then make sure the index is in a reasonable state. """ # Try to access the indexer directly first so that if it is # unavailable, the test will be skipped. self.openReadIndex().close() service = batch.BatchProcessingService(self.store, iaxiom.REMOTE) task = service.step() source = batch.processor(IndexableThing)(store=self.store) self.indexer.addSource(source) things = [ IndexableThing(store=self.store, _documentType=u'thing', _uniqueIdentifier='100', _textParts=[u'apple', u'banana'], _keywordParts={}), IndexableThing(store=self.store, _documentType=u'thing', _uniqueIdentifier='200', _textParts=[u'cherry'], _keywordParts={})] for i in xrange(len(things)): task.next() self.indexer.suspend() # Sanity check - make sure both items come back from a search before # going on with the real core of the test. reader = self.openReadIndex() self.assertEquals(identifiersFrom(reader.search(u'apple')), [100]) self.assertEquals(identifiersFrom(reader.search(u'cherry')), [200]) self.assertEquals(identifiersFrom(reader.search(u'drosophila')), []) reader.close() self.corruptIndex() self.indexer.resume() things.append( IndexableThing(store=self.store, _documentType=u'thing', _uniqueIdentifier='300', _textParts=[u'drosophila', u'melanogaster'], _keywordParts={})) # Step it once so that it notices the index has been corrupted. task.next() self.indexer.suspend() # At this point, the index should have been deleted, so any search # should turn up no results. reader = self.openReadIndex() self.assertEquals(identifiersFrom(reader.search(u'apple')), []) self.assertEquals(identifiersFrom(reader.search(u'cherry')), []) self.assertEquals(identifiersFrom(reader.search(u'drosophila')), []) reader.close() self.indexer.resume() # Step it another N so that each thing gets re-indexed. for i in xrange(len(things)): task.next() self.indexer.suspend() reader = self.openReadIndex() self.assertEquals(identifiersFrom(reader.search(u'apple')), [100]) self.assertEquals(identifiersFrom(reader.search(u'cherry')), [200]) self.assertEquals(identifiersFrom(reader.search(u'drosophila')), [300]) reader.close() class HypeTestsMixin: def createIndexer(self): return fulltext.HypeIndexer(store=self.store, indexDirectory=self.path) class HypeFulltextTestCase(HypeTestsMixin, FulltextTestsMixin, unittest.TestCase): skip = "These tests don't actually pass - and I don't even care." class PyLuceneTestsMixin: def createIndexer(self): return fulltext.PyLuceneIndexer(store=self.store, indexDirectory=self.path) class PyLuceneFulltextTestCase(PyLuceneTestsMixin, FulltextTestsMixin, unittest.TestCase): def testAutomaticClosing(self): """ Test that if we create a writer and call the close-helper function, the writer gets closed. """ writer = self.openWriteIndex() fulltext._closeIndexes() self.failUnless(writer.closed, "Writer should have been closed.") def testRepeatedClosing(self): """ Test that if for some reason a writer is explicitly closed after the close-helper has run, nothing untoward occurs. """ writer = self.openWriteIndex() fulltext._closeIndexes() writer.close() self.failUnless(writer.closed, "Writer should have stayed closed.") def test_resultSlicing(self): """ Test that the wrapper object return by the pylucene index correctly handles slices """ writer = self.openWriteIndex() identifiers = range(20) for i in identifiers: writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(i), _textParts=[u'e'], _keywordParts={})) writer.close() reader = self.openReadIndex() results = reader.search(u'e') self.assertEquals(identifiersFrom(results), identifiers) self.assertEquals(identifiersFrom(results[0:None:2]), identifiers[0:None:2]) self.assertEquals(identifiersFrom(results[0:5:1]), identifiers[0:5:1]) self.assertEquals(identifiersFrom(results[15:0:-1]), identifiers[15:0:-1]) self.assertEquals(identifiersFrom(results[15:None:-1]), identifiers[15:None:-1]) self.assertEquals(identifiersFrom(results[0:24:2]), identifiers[0:24:2]) self.assertEquals(identifiersFrom(results[24:None:-1]), identifiers[24:None:-1]) def test_hitWrapperAttributes(self): """ Test that L{xmantissa.fulltext._PyLuceneHitWrapper}'s attributes are set correctly """ class Indexable: implements(ixmantissa.IFulltextIndexable) def keywordParts(self): return {u'foo': u'bar', u'baz': u'quux'} def uniqueIdentifier(self): return 'indexable' def documentType(self): return 'the indexable type' def sortKey(self): return 'foo' def textParts(self): return [u'my', u'text'] indexable = Indexable() writer = self.openWriteIndex() writer.add(indexable) writer.close() reader = self.openReadIndex() (wrapper,) = reader.search(u'text') self.assertEquals(wrapper.keywordParts, indexable.keywordParts()) self.assertEquals(wrapper.uniqueIdentifier, indexable.uniqueIdentifier()) self.assertEquals(wrapper.documentType, indexable.documentType()) self.assertEquals(wrapper.sortKey, indexable.sortKey()) class PyLuceneCorruptionRecoveryTestCase(PyLuceneTestsMixin, CorruptionRecoveryMixin, unittest.TestCase): def corruptIndex(self): """ Cause a PyLucene index to appear corrupted. """ for ch in self.store.newFilePath(self.path).children(): ch.setContent('hello, world') def testFailureDetectionFromWriter(self): """ Fulltext indexes are good at two things: soaking up I/O bandwidth and corrupting themselves. For the latter case, we need to be able to detect the condition before we can make any response to it. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='10', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.close() self.corruptIndex() self.assertRaises(fulltext.IndexCorrupt, self.openWriteIndex) self.assertRaises(fulltext.IndexCorrupt, self.openReadIndex) def testFailureDetectionFromReader(self): """ Like testFailureDetectionFromWriter, but opens a reader after corrupting the index and asserts that it also raises the appropriate exception. """ writer = self.openWriteIndex() writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier='10', _textParts=[u'apple', u'banana'], _keywordParts={})) writer.close() self.corruptIndex() self.assertRaises(fulltext.IndexCorrupt, self.openReadIndex) self.assertRaises(fulltext.IndexCorrupt, self.openWriteIndex) class PyLuceneLockedRecoveryTestCase(PyLuceneTestsMixin, CorruptionRecoveryMixin, unittest.TestCase): def setUp(self): CorruptionRecoveryMixin.setUp(self) self.corruptedIndexes = [] def corruptIndex(self): """ Loosely simulate filesystem state following a SIGSEGV or power failure. """ self.corruptedIndexes.append(self.openWriteIndex()) class PyLuceneObjectLifetimeTestCase(unittest.TestCase): def test_hitsWrapperClosesIndex(self): """ Test that when L{_PyLuceneHitsWrapper} is GC'd, the index which backs its C{Hits} object gets closed. """ class TestIndex(object): closed = False def close(self): self.closed = True index = TestIndex() wrapper = fulltext._PyLuceneHitsWrapper(index, None) self.failIf(index.closed) del wrapper self.failUnless(index.closed) class IndexerAPISearchTestsMixin(IndexerTestsMixin): """ Test ISearchProvider search API on indexer objects """ def setUp(self): """ Make a store, an account/substore, an indexer, and call startService() on the superstore's IService so the batch process interactions that happen in fulltext.py work """ self.dbdir = self.mktemp() self.path = u'index' superstore = store.Store(self.dbdir) loginSystem = LoginSystem(store=superstore) installOn(loginSystem, superstore) account = loginSystem.addAccount(u'testuser', u'example.com', None) substore = account.avatars.open() self.store = substore self.indexer = self.createIndexer() self.svc = IService(superstore) self.svc.startService() # Make sure the indexer is actually available writer = self.openWriteIndex() writer.close() def tearDown(self): """ Stop the service we started in C{setUp} """ return self.svc.stopService() def _indexSomeItems(self): writer = self.openWriteIndex() for i in xrange(5): writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(i), _textParts=[u'text'], _keywordParts={})) writer.close() def testIndexerSearching(self): """ Test calling search() on the indexer item directly """ def gotResult(res): self.assertEquals(identifiersFrom(res), range(5)) self._indexSomeItems() return self.indexer.search(u'text').addCallback(gotResult) def testIndexerSearchingCount(self): """ Test calling search() on the indexer item directly, with a count arg """ def gotResult(res): self.assertEquals(identifiersFrom(res), [0]) self._indexSomeItems() return self.indexer.search(u'text', count=1).addCallback(gotResult) def testIndexerSearchingOffset(self): """ Test calling search() on the indexer item directly, with an offset arg """ def gotResult(res): self.assertEquals(identifiersFrom(res), [1, 2, 3, 4]) self._indexSomeItems() return self.indexer.search(u'text', offset=1).addCallback(gotResult) def testIndexerSearchingCountOffset(self): """ Test calling search() on the indexer item directly, with count & offset args """ def gotResult(res): self.assertEquals(identifiersFrom(res), [1, 2, 3]) self._indexSomeItems() return self.indexer.search(u'text', count=3, offset=1) def test_DifficultTokens(self): """ Test searching for fragments of phone numbers, email addresses, and urls. """ writer = self.openWriteIndex() specimens = [u"trevor 718-555-1212", u"bob rjones@moddiv.com", u"atop http://divmod.org/projects/atop"] for i, txt in enumerate(specimens): writer.add(IndexableThing( _documentType=u'thing', _uniqueIdentifier=str(i), _textParts=[txt], _keywordParts={})) writer.close() def gotResult(res): return identifiersFrom(res) def testResults(results): self.assertEqual(results, [[0], [1], [2], [0], [1], [2]]) return gatherResults( [self.indexer.search(u'718').addCallback(gotResult), self.indexer.search(u'moddiv').addCallback(gotResult), self.indexer.search(u'divmod').addCallback(gotResult), self.indexer.search(u'718-555').addCallback(gotResult), self.indexer.search(u'rjones@moddiv').addCallback(gotResult), self.indexer.search(u'divmod.org').addCallback(gotResult), ] ).addCallback(testResults) def test_unicodeSearch(self): return self.indexer.search(u'\N{WHITE SMILING FACE}') class PyLuceneIndexerAPISearchTestCase(PyLuceneTestsMixin, IndexerAPISearchTestsMixin, unittest.TestCase): pass class HypeIndexerAPISearchTestCase(HypeTestsMixin, IndexerAPISearchTestsMixin, unittest.TestCase): skip = "These tests don't actually pass - and I don't even care." def _hasFTS3(): s = store.Store() try: s.createSQL('CREATE VIRTUAL TABLE fts USING fts3') except SQLError: return False else: return True class SQLiteTestsMixin(object): """ Mixin for tests for the SQLite indexer. """ if not _hasFTS3(): skip = 'No FTS3 support' def createIndexer(self): """ Create the SQLite indexer. """ return fulltext.SQLiteIndexer(store=self.store, indexDirectory=self.path) class SQLiteFulltextTestCase(SQLiteTestsMixin, FulltextTestsMixin, unittest.TestCase): """ Tests for SQLite fulltext indexing. """ def _noKeywords(self): raise unittest.SkipTest('Keywords are not implemented') testKeywordCombination = _noKeywords testKeywordIndexing = _noKeywords testKeywordTokenization = _noKeywords testKeywordValuesInPhrase = _noKeywords test_typeRestriction = _noKeywords class SQLiteIndexerAPISearchTestCase(SQLiteTestsMixin, IndexerAPISearchTestsMixin, unittest.TestCase): """ Tests for SQLite indexing through the indexer service. """ def test_DifficultTokens(self): raise unittest.SkipTest("SQLite tokenizer can't handle all of these") PK9F!xmantissa/test/test_interstore.py """ Tests for inter-store messaging module, L{xmantissa.messaging}. This module contains tests for persistent messaging between different accounts. """ import gc from datetime import timedelta from zope.interface import implements from twisted.trial.unittest import TestCase from twisted.internet.defer import Deferred from twisted.protocols.amp import Box, Command, Integer, String from epsilon.extime import Time from axiom.iaxiom import IScheduler from axiom.store import Store from axiom.errors import UnsatisfiedRequirement from axiom.item import Item, POWERUP_BEFORE from axiom.attributes import text, bytes, integer, boolean, inmemory from axiom.userbase import LoginSystem, LoginMethod, LoginAccount from axiom.dependency import installOn from axiom.scheduler import TimedEvent from xmantissa.interstore import ( # Public Names MessageQueue, AMPMessenger, LocalMessageRouter, Value, AMPReceiver, commandMethod, answerMethod, errorMethod, SenderArgument, TargetArgument, # Constants AMP_MESSAGE_TYPE, AMP_ANSWER_TYPE, DELIVERY_ERROR, # Error Types ERROR_REMOTE_EXCEPTION, ERROR_NO_SHARE, ERROR_NO_USER, ERROR_BAD_SENDER, # Private Names _RETRANSMIT_DELAY, _QueuedMessage, _AlreadyAnswered, _FailedAnswer, _AMPExposer, _AMPErrorExposer) from xmantissa.sharing import getEveryoneRole, Identifier from xmantissa.error import ( MessageTransportError, BadSender, UnknownMessageType, RevertAndRespond, MalformedMessage) from xmantissa.ixmantissa import IMessageReceiver, IMessageRouter class SampleException(Exception): """ Something didn't happen because of a problem. """ class StubReceiver(Item): """ This is a message receiver that will store a message sent to it for inspection by tests. """ implements(IMessageReceiver) messageType = text( doc=""" The message type which C{messageReceived} should put into its return value. """) messageData = bytes( doc=""" The message data which C{messageReceived} should put into its return value. """) inconsistent = boolean( doc=""" This value is set to True during the execution of C{messageReceived}, but False afterwards. If everything is properly transactional it should never be observably false by other code. """) buggy = boolean(allowNone=False, default=False, doc=""" C{messageReceived} should raise a L{SampleException}. """) badReturn = boolean(allowNone=False, default=False, doc=""" C{messageReceived} should return L{None}. """) receivedCount = integer(default=0, doc=""" This is a counter of the number of messages received by C{messageReceived}. """) reciprocate = boolean(allowNone=False, default=False, doc=""" C{messageReceived} should respond to its C{sender} parameter with a symmetric message in addition to answering. """) revertType = text(allowNone=True, doc=""" If set, this specifies the type of the L{RevertAndRespond} exception that C{messageReceived} should raise. """) revertData = bytes(allowNone=True, doc=""" If C{revertType} is set, this specifies the data of the L{RevertAndRespond} exception that C{messageReceived} should raise. """) def messageQueue(self): """ This is a temporary workaround; see ticket #2640 for details on the way this method should be implemented in the future. """ return self.store.findUnique(MessageQueue) def messageReceived(self, value, sender, receiver): """ A message was received. Increase the message counter and store its contents. """ self.receivedCount += 1 self.messageType = value.type self.messageData = value.data self.inconsistent = True if self.buggy: raise SampleException("Sample Message") if self.revertType is not None: raise RevertAndRespond(Value(self.revertType, self.revertData)) self.inconsistent = False if self.badReturn: return None if self.reciprocate: self.messageQueue().queueMessage( receiver, sender, Value(value.type + u'.response', value.data + ' response')) return Value(u"custom.message.type", "canned response") class StubSlowRouter(Item): """ Like L{LocalMessageRouter}, but don't actually deliver the messages until the test forces them to be delivered. By way of several parameters to `flushMessages`, this stub implementation allows for all of the arbitrary ways in which a potential networked implementation is allowed to behave - dropping messages, repeating messages, and even failing in buggy ways. Note: this must be kept in memory for the duration of any test using it. @ivar messages: a list of (sender, target, value, messageID) tuples received by routeMessage. @ivar acks: a list of (deferred, (sender, target, value, messageID)) tuples, representing an answer received by routeAnswer and the deferred that was returned to indicate its delivery. """ dummy = integer( doc=""" No state on this item is persistent; this is just to satisfy Axiom's schema requirement. """) messages = inmemory() acks = inmemory() def localRouter(self): """ Return a L{LocalMessageRouter} for this slow router's store. """ return LocalMessageRouter(self.store.findUnique(LoginSystem)) def activate(self): """ Initialize temporary list to queue messages. """ self.messages = [] self.acks = [] def routeMessage(self, sender, target, value, messageID): """ Stub implementation of L{IMessageRouter.routeMessage} that just appends to a list in memory, and later delegates from that list to the local router. """ self.messages.append((sender, target, value, messageID)) def routeAnswer(self, originalSender, originalTarget, value, messageID): """ Stub implementation of L{IMessageRouter.routeAnswer} that just appends to a list in memory. """ D = Deferred() self.acks.append((D, (originalSender, originalTarget, value, messageID))) return D def flushMessages(self, dropAcks=False, dropAckErrorType=MessageTransportError, stallAcks=False, repeatAcks=False): """ Delegate all messages queued in memory with routeMessage to the specified local router. @param dropAcks: a boolean, indicating whether to drop the answers queued by routeAnswer. @param dropAckErrorType: an exception type, indicating what exception to errback the Deferreds returned by routeAnswer with. @param stallAcks: a boolean, indicating whether to keep, but not act, on the answers queued by routeAnswer. @param repeatAcks: a boolean, indicating whether to repeat all of the acks the next time flushMessages is called. """ m = self.messages[:] self.messages = [] for message in m: self.localRouter().routeMessage(*message) if dropAcks: for D, ack in self.acks: D.errback(dropAckErrorType()) self.acks = [] if not stallAcks: for D, ack in self.acks: self.localRouter().routeAnswer(*ack).chainDeferred(D) if repeatAcks: # the Deferreds are used up, so we need a fresh batch for the # next run-through (although these will be ignored) self.acks = [(Deferred(), ack) for (D, ack) in self.acks] else: self.acks = [] def spuriousDeliveries(self): """ Simulate a faulty transport, and deliver all the currently pending messages without paying attention to their results. """ for message in self.messages: self.localRouter().routeMessage(*message) class StubDeliveryConsequence(Item): """ This implements a delivery consequence. @ivar responses: a tuple of (answer-type, answer-data, message-type, message-data, sender, target), listing all the answers received by answerReceived. @ivar bucket: a list which will have this L{StubDeliveryConsequence} appended to it when a successful message is processed. """ responses = inmemory() bucket = inmemory() invocations = integer( """ Counter, keeping track of how many times this consequence has been invoked. """, default=0, allowNone=False) succeeded = boolean( """ Did the action succeed? None if it hasn't completed, True if yes, False if no. """) inconsistent = boolean( """ This should never be set to True. It's set to None by default, False when the callback fully succeeds. """) buggy = boolean( """ Set this to cause 'success' to raise an exception. """, default=False, allowNone=False) def activate(self): """ Initialize the list of received responses. """ self.responses = [] self.bucket = [] def success(self): """ A response was received to the message. This will be executed in a transaction. Raise an exception if this consequence is buggy. """ self.bucket.append(self) self.inconsistent = True self.invocations += 1 self.succeeded = True if self.buggy: raise SampleException() self.inconsistent = False def failure(self): """ The message could not be delivered for some reason. This will be executed in a transaction. Raise an exception if this consequence is buggy. @param reason: an exception. """ self.invocations += 1 self.succeeded = False def answerReceived(self, answerValue, originalValue, originalSender, originalTarget): """ An answer was received. """ if answerValue.type == DELIVERY_ERROR: self.failure() else: self.success() # It's important that this happen after the "application" logic so that # the tests will not see this set if an exception has been raised. self.responses.append((answerValue.type, answerValue.data, originalValue.type, originalValue.data, originalSender, originalTarget)) class TimeFactory(object): """ Make a fake time factory. """ def __init__(self): """ Create a time factory with some default values. """ self.currentSeconds = 0.0 def advance(self): """ Advance the current time by one second. """ self.currentSeconds += 1.0 def next(self): """ Produce the next time in the sequence, then advance. """ self.advance() return Time.fromPOSIXTimestamp(self.currentSeconds) def peek(self): """ Return the value that will come from the next call to 'next'. """ return Time.fromPOSIXTimestamp(self.currentSeconds + 1) class SingleSiteMessagingTests(TestCase): """ These are tests for messaging within a single configured site store. """ def setUp(self): """ Create a site store with two users that can send messages to each other. """ self.siteStore = Store() self.time = TimeFactory() self.loginSystem = LoginSystem(store=self.siteStore) installOn(self.loginSystem, self.siteStore) self.aliceAccount = self.loginSystem.addAccount( u"alice", u"example.com", u"asdf", internal=True) self.bobAccount = self.loginSystem.addAccount( u"bob", u"example.com", u"asdf", internal=True) self.aliceStore, self.aliceQueue = self.accountify( self.aliceAccount.avatars.open()) self.bobStore, self.bobQueue = self.accountify( self.bobAccount.avatars.open()) # I need to make a target object with a message receiver installed on # it. Then I need to share that object. self.receiver = StubReceiver(store=self.bobStore) getEveryoneRole(self.bobStore).shareItem(self.receiver, u"suitcase") self.retransmitDelta = timedelta(seconds=_RETRANSMIT_DELAY) def accountify(self, userStore): """ Add a MessageQueue to the given user store and stub out its scheduler's time function. """ queue = MessageQueue(store=userStore) installOn(queue, userStore) IScheduler(userStore).now = self.time.peek return userStore, queue def runQueue(self, queue): """ Advance the current time and run the given message queue. """ self.time.advance() return queue.run() def test_bogusConfiguration(self): """ Delivering a message on a site without a L{LoginSystem} should cause an L{UnsatisfiedRequirement} exception to be logged, and the message not to be delivered. """ self.loginSystem.deleteFromStore() self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) [err] = self.flushLoggedErrors(UnsatisfiedRequirement) # The message should still be queued. self.assertEqual(len(list(self.aliceStore.query(_QueuedMessage))), 1) def unattachedUserStore(self): """ Create a store that is structured as if it was a user-store with messaging enabled, but was opened un-attached from its parent store. @return: a 2-tuple of (store, queue). """ carolStore, carolQueue = self.accountify(Store()) acct = LoginAccount(store=carolStore, password=u'asdf') lm = LoginMethod(store=carolStore, localpart=u'carol', domain=u'example.com', internal=True, protocol=u'*', account=acct, verified=True) return carolStore, carolQueue def test_accidentallyOpenAsSite(self): """ If a message delivery is somehow attempted with a user store accidentally opened as a site store, delivery should fail. Normally this will not happen, since the current implementation (as of when this test was written) of the scheduler will not allow timed events to run with a L{SubScheduler} installed rather than a L{Scheduler}. However, eliminating this distinction is a long-term goal, so this test is a defense against both future modifications and other code which may emulate scheduler APIs. """ carolStore, carolQueue = self.unattachedUserStore() sdc = StubDeliveryConsequence(store=carolStore) carolQueue.queueMessage( Identifier(u"nothing", u"carol", u"example.com"), Identifier(u"suitcase", u"bob", u"example.com"), Value(u'custom.message.type', "Some message contents"), sdc) self.runQueue(carolQueue) [err] = self.flushLoggedErrors(UnsatisfiedRequirement) # The message should still be queued. self.assertEqual(len(list(carolStore.query(_QueuedMessage))), 1) def test_accidentallyAnswerAsSite(self): """ If an answer delivery is somehow attempted with a user store accidentally opened as a site store, the delivery should result in a transient failure. This is even less likely than the case described in L{test_accidentallyOpenAsSite}, but in the unlikely event that the scheduler is manually run, it still shouldn't result in any errors being logged or state being lost. """ carolStore, carolQueue = self.unattachedUserStore() carolReceiver = StubReceiver(store=carolStore) getEveryoneRole(carolStore).shareItem(carolReceiver, u'some-share-id') bogusID = Identifier(u'nothing', u'nobody', u'nowhere') carolID = Identifier(u'some-share-id', u'carol', u'example.com') carolQueue.routeMessage(bogusID, carolID, Value(u'no.type', 'contents'), 1) [err] = self.flushLoggedErrors(UnsatisfiedRequirement) # The answer should still be queued. self.assertEqual(len(list(carolStore.query(_AlreadyAnswered))), 1) def test_queueMessageSimple(self): """ Queuing a message should create a _QueuedMessage object and schedule it for delivery to its intended recipient. """ # Maybe I should do this by sharing an object in Alice's store and then # wrapping a SharingView-type thing around it? it seems like there # ought to be a purely model-level API for this, though. self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"suitcase", u"bob", u"example.com"), Value(u"custom.message.type", "This is an important message.")) # OK now let's find the message that was queued. qm = self.aliceStore.findUnique(_QueuedMessage) self.assertEqual(qm.senderUsername, u'alice') self.assertEqual(qm.senderDomain, u'example.com') # Can you do this? It seems like it might be inconvenient to always # determine a resolvable "return address" - the test case here is a # simulation of reasonable behavior; alice hasn't shared anything. # self.assertEqual(qm.senderShareID, None) self.assertEqual(qm.targetUsername, u"bob") self.assertEqual(qm.targetDomain, u"example.com") self.assertEqual(qm.targetShareID, u"suitcase") self.assertEqual(qm.value.type, u"custom.message.type") self.assertEqual(qm.value.data, "This is an important message.") # It should be scheduled. Is there a timed event? te = self.aliceStore.findUnique( TimedEvent, TimedEvent.runnable == self.aliceQueue) # It should be scheduled immediately. This uses the real clock, but in # a predictable way (i.e. if time does not go backwards, then this will # work). If this test ever fails intermittently there _is_ a real # problem. self.assertNotIdentical(te.time, None) self.failUnless(te.time <= Time()) runresult = self.runQueue(self.aliceQueue) # It should succeed, it should not reschedule itself; the scheduler # will delete things that return None from run(). It would be nice to # integrate with the scheduler here, but that would potentially drag in # dependencies on other systems not scheduling stuff. self.assertEqual(runresult, None) self.assertEqual(self.receiver.messageData, "This is an important message.") self.assertEqual(self.receiver.messageType, u"custom.message.type") self.assertEqual(list(self.aliceStore.query(_QueuedMessage)), []) def aliceToBobWithConsequence(self, buggy=False): """ Queue a message from Alice to Bob with a supplied L{StubDeliveryConsequence} and return it. """ sdc = StubDeliveryConsequence(store=self.aliceStore, buggy=buggy) self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"suitcase", u"bob", u"example.com"), Value(u'custom.message.type', "Some message contents"), sdc) return sdc def checkOneResponse(self, sdc, expectedType=u"custom.message.type", expectedData="canned response", originalSender= Identifier(u"nothing", u"alice", u"example.com"), originalTarget= Identifier(u"suitcase", u"bob", u"example.com"), succeeded=None): """ This checks that the response received has the expected type and data, and corresponds to the sender and target specified by L{SingleSiteMessagingTests.aliceToBobWithConsequence}. """ if succeeded is None: if expectedType == DELIVERY_ERROR: succeeded = False else: succeeded = True # First, let's make sure that transaction committed. self.assertEqual(sdc.succeeded, succeeded) self.assertEqual(sdc.responses, [(expectedType, expectedData, u'custom.message.type', # type "Some message contents", # data originalSender, originalTarget )]) def test_queueMessageSuccessNotification(self): """ Queueing a message should emit a success notification to the supplied 'consequence' object. """ sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) self.assertEqual(sdc.succeeded, True) self.checkOneResponse(sdc) def test_queueMessageSuccessErrorHandling(self): """ If the supplied 'consequence' object is buggy, the error should be logged so that the answer can be processed later, but not propagated to the network layer. """ sdc = self.aliceToBobWithConsequence(True) self.runQueue(self.aliceQueue) # It should be run in a transaction so none of the stuff set by # 'success' should be set self.assertEqual(sdc.succeeded, None) self.assertEqual(sdc.inconsistent, None) self.assertEqual(sdc.invocations, 0) [err] = self.flushLoggedErrors(SampleException) # Make sure that no messages are queued. self.assertEqual(list(self.aliceStore.query(_QueuedMessage)), []) failures = list(self.aliceStore.query(_FailedAnswer)) self.assertEqual(len(failures), 1) # Fix the bug. In normal operation this would require a code upgrade. sdc.buggy = False failures[0].redeliver() self.checkOneResponse(sdc) def test_alreadyAnsweredRemoval(self): """ L{_AlreadyAnswered} records should be removed after the deferred from L{routeAnswer} is fired. """ slowRouter = self.stubSlowRouter() sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.flushMessages(stallAcks=True) self.assertEqual(len(slowRouter.acks), 1) # sanity check self.assertEqual(self.bobStore.query(_AlreadyAnswered).count(), 1) slowRouter.flushMessages() self.assertEqual(self.bobStore.query(_AlreadyAnswered).count(), 0) def test_repeatedAnswer(self): """ If answers are repeated, they should only be processed once. """ slowRouter = self.stubSlowRouter() sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.flushMessages(repeatAcks=True) slowRouter.flushMessages() self.assertEqual(sdc.invocations, 1) def _reschedulingTest(self, errorType): """ Test for rescheduling of L{_AlreadyAnswered} results in the presence of the given error from the router. """ slowRouter = self.stubSlowRouter() sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.flushMessages(dropAcks=True, dropAckErrorType=errorType) # It should be scheduled. self.assertEqual( len(list(IScheduler(self.bobQueue.store).scheduledTimes(self.bobQueue))), 1) # Now let's run it and see if the ack gets redelivered. self.runQueue(self.bobQueue) slowRouter.flushMessages() self.assertEqual(sdc.succeeded, True) def test_alreadyAnsweredReschedule(self): """ L{_AlreadyAnswered} records should be scheduled for retransmission if the L{Deferred} from L{routeAnswer} is errbacked with a L{MessageTransportError}. No message should be logged, since this is a transient and potentially expected error. """ self._reschedulingTest(MessageTransportError) def test_alreadyAnsweredRescheduleAndLog(self): """ L{_AlreadyAnswered} records should be scheduled for retransmission if the L{Deferred} from L{routeAnswer} is errbacked with an unknown exception type, and the exception should be logged. """ self._reschedulingTest(SampleException) [err] = self.flushLoggedErrors(SampleException) def test_alreadyAnsweredRescheduleCrash(self): """ L{_AlreadyAnswered} records should be scheduled for retransmission if the L{Deferred} from L{routeAnswer} dies without being callbacked or errbacked (such as if the store were to crash). """ slowRouter = self.stubSlowRouter() sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.flushMessages(stallAcks=True) self.assertEqual(sdc.invocations, 0) slowRouter.acks = [] gc.collect() # Make sure the Deferred is well and truly gone. self.assertIdentical( self.bobStore.findUnique(_AlreadyAnswered).deliveryDeferred, None) self.runQueue(self.bobQueue) slowRouter.flushMessages() self.assertEqual(sdc.invocations, 1) def test_noRemoteUser(self): """ What if the target user we're trying to talk to doesn't actually exist in the system? The message delivery should fail. """ sdc = StubDeliveryConsequence(store=self.aliceStore) self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"suitcase", u"bohb", u"example.com"), Value(u"custom.message.type", "Some message contents"), sdc) self.runQueue(self.aliceQueue) self.assertEqual(sdc.succeeded, False) self.checkOneResponse( sdc, DELIVERY_ERROR, ERROR_NO_USER, originalTarget=Identifier(u"suitcase", u"bohb", u"example.com")) self.assertEqual(sdc.invocations, 1) def test_noRemoteShare(self): """ Similarly, if there's nothing identified by the shareID specified, the message delivery should fail. """ sdc = StubDeliveryConsequence(store=self.aliceStore) self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"nothing", u"bob", u"example.com"), Value(u"custom.message.type", "Some message contents"), sdc) self.runQueue(self.aliceQueue) self.assertEqual(sdc.succeeded, False) self.checkOneResponse( sdc, DELIVERY_ERROR, ERROR_NO_SHARE, originalTarget=Identifier(u"nothing", u"bob", u"example.com")) self.assertEqual(sdc.invocations, 1) def buggyReceiverTest(self, exceptionType): """ Run a test expecting the receiver to fail. """ sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) self.assertEqual(sdc.succeeded, False) self.checkOneResponse(sdc, DELIVERY_ERROR, ERROR_REMOTE_EXCEPTION) [err] = self.flushLoggedErrors(exceptionType) self.assertEqual(sdc.invocations, 1) self.assertEqual(self.receiver.inconsistent, None) def test_messageReceivedBadReturn(self): """ When L{messageReceived} does not properly return a 2-tuple, that resulting exception should be reported to the delivery consequence of the message. The target database should not be left in an inconsistent state. """ self.receiver.badReturn = True self.buggyReceiverTest(TypeError) def test_messageReceivedException(self): """ When L{messageReceived} raises an exception, that exception should be reported to the delivery consequence of the message. The target database should not be left in an inconsistent state. """ self.receiver.buggy = True self.buggyReceiverTest(SampleException) def test_revertAndRespond(self): """ When L{messageReceived} raises the special L{RevertAndRespond} exception, the values passed to the exception should be used to generate the response, but the transaction should be reverted. """ t = self.receiver.revertType = u'custom.reverted.type' d = self.receiver.revertData = "this is some data that I reverted" sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) self.assertEqual(sdc.succeeded, True) self.assertEqual(sdc.inconsistent, False) self.assertEqual(sdc.invocations, 1) self.assertEqual(self.receiver.inconsistent, None) self.checkOneResponse(sdc, t, d) def test_droppedException(self): """ When L{messageReceived} raises an exception, that exception should be reported to the delivery consequence of the message, even if the initial transmission of the error report is lost. """ slowRouter = self.stubSlowRouter() self.receiver.buggy = True sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.flushMessages(dropAcks=True) [err] = self.flushLoggedErrors(SampleException) self.runQueue(self.aliceQueue) slowRouter.flushMessages() self.assertEqual(sdc.invocations, 1) self.assertEqual(sdc.succeeded, False) self.checkOneResponse(sdc, DELIVERY_ERROR, ERROR_REMOTE_EXCEPTION) def test_senderNotVerified(self): """ When the sender users name or domain do not match an internal, verified login method of the originating store, sending a message via queueMessage should resport an ERROR_BAD_SENDER. """ sdc = StubDeliveryConsequence(store=self.aliceStore) self.aliceQueue.queueMessage( Identifier(u"nothing", u"fred", u"example.com"), Identifier(u"suitcase", u"bob", u"example.com"), Value(u"custom.message.type", "Some message contents"), sdc) self.assertEqual(sdc.invocations, 0) self.runQueue(self.aliceQueue) self.assertEqual(self.receiver.receivedCount, 0) self.assertEqual(sdc.succeeded, False) self.assertEqual(sdc.invocations, 1) self.checkOneResponse(sdc, DELIVERY_ERROR, ERROR_BAD_SENDER, originalSender=Identifier( u"nothing", u"fred", u"example.com")) [err] = self.flushLoggedErrors(BadSender) bs = err.value self.assertEqual(bs.attemptedSender, u'fred@example.com') self.assertEqual(bs.allowedSenders, [u'alice@example.com']) self.assertEqual(str(bs), "alice@example.com attempted to send message " "as fred@example.com") def stubSlowRouter(self): """ Replace this test's stub router with an artificially slowed-down router. """ slowRouter = StubSlowRouter(store=self.siteStore) self.siteStore.powerUp(slowRouter, IMessageRouter, POWERUP_BEFORE) return slowRouter def test_slowDelivery(self): """ If the site message-deliverer powerup returns a Deferred that takes a while to fire, the L{MessageQueue} should respond by rescheduling itself in the future. """ slowRouter = self.stubSlowRouter() self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"suitcase", u"bob", u"example.com"), Value(u"custom.message.type", "Message2")) [time1] = IScheduler(self.aliceQueue.store).scheduledTimes(self.aliceQueue) time2 = self.runQueue(self.aliceQueue) self.assertEqual(time2 - self.time.peek(), self.retransmitDelta) self.assertEqual(self.receiver.receivedCount, 0) self.assertEqual(len(slowRouter.messages), 1) slowRouter.flushMessages() self.assertEqual(len(slowRouter.messages), 0) self.assertEqual(self.receiver.receivedCount, 1) def test_reallySlowDelivery(self): """ If the Deferred takes so long to fire that another retransmission attempt takes place, the message should only be delivered once. If it does fail, the next transmission attempt should actually transmit. """ slowRouter = self.stubSlowRouter() c = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) self.runQueue(self.aliceQueue) slowRouter.flushMessages() self.assertEqual(len(slowRouter.messages), 0) self.assertEqual(self.receiver.receivedCount, 1) self.assertEqual(c.invocations, 1) self.assertEqual(c.succeeded, 1) def test_multipleMessages(self): """ Sending multiple messages at the same time should result in the messages being processed immediately, with no delay, but in order. """ slowRouter = self.stubSlowRouter() bucket = [] c = self.aliceToBobWithConsequence() c.bucket = bucket c2 = self.aliceToBobWithConsequence() c2.bucket = bucket # Sanity check; make sure the message hasn't been processed yet. self.assertEqual(bucket, []) self.runQueue(self.aliceQueue) slowRouter.flushMessages() self.assertEqual(c.invocations, 1) self.assertEqual(c.succeeded, 1) self.assertEqual(c2.invocations, 1) self.assertEqual(c2.succeeded, 1) self.assertEqual(bucket, [c, c2]) self.assertEqual(self.runQueue(self.aliceQueue), None) def test_multipleAnswers(self): """ Sending multiple messages at the same time should result in the messages being processed immediately, with no delay, but in order. """ slowRouter = self.stubSlowRouter() bucket = [] c = self.aliceToBobWithConsequence() c.bucket = bucket c2 = self.aliceToBobWithConsequence() c2.bucket = bucket # Sanity check; make sure the message hasn't been processed yet. self.assertEqual(bucket, []) self.runQueue(self.aliceQueue) slowRouter.flushMessages(dropAcks=True) [time1] = IScheduler(self.bobQueue.store).scheduledTimes(self.bobQueue) time2 = self.runQueue(self.bobQueue) self.assertEqual(time2 - self.time.peek(), self.retransmitDelta) slowRouter.flushMessages() self.assertEqual(c.invocations, 1) self.assertEqual(c.succeeded, 1) self.assertEqual(c2.invocations, 1) self.assertEqual(c2.succeeded, 1) self.assertEqual(bucket, [c, c2]) self.assertEqual(self.runQueue(self.aliceQueue), None) def test_deliveryIdempotence(self): """ Delivering the same message to a substore twice should only result in it being delivered to application code once. """ slowRouter = self.stubSlowRouter() sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) slowRouter.spuriousDeliveries() self.assertEqual(self.receiver.receivedCount, 1) self.assertEqual(sdc.invocations, 0) slowRouter.flushMessages() self.assertEqual(self.receiver.receivedCount, 1) self.assertEqual(sdc.invocations, 1) self.checkOneResponse(sdc, ) def test_reciprocate(self): """ In addition to responding to the message with a return value, the receiver should be able to remember the sender and emit reciprocal messages. """ self.receiver.reciprocate = True receiver2 = StubReceiver(store=self.aliceStore) getEveryoneRole(self.aliceStore).shareItem(receiver2, u'nothing') sdc = self.aliceToBobWithConsequence() self.runQueue(self.aliceQueue) self.runQueue(self.bobQueue) self.assertEqual(receiver2.receivedCount, 1) self.assertEqual(receiver2.messageType, u'custom.message.type.response') self.assertEqual(receiver2.messageData, 'Some message contents response') def test_unhandledDeliveryError(self): """ When a message cannot be delivered, but no consequence is supplied, the error should be logged. """ self.receiver.buggy = True self.aliceQueue.queueMessage( Identifier(u"nothing", u"alice", u"example.com"), Identifier(u"nothing", u"bob", u"example.com"), Value(u'custom.message.type', "Some message contents")) self.runQueue(self.aliceQueue) [err] = self.flushLoggedErrors(MessageTransportError) def test_messageRemoteItem(self): """ When a message is sent to a shared item using L{AMPMessenger.messageRemote}, and received by an L{AMPReceiver}, its data should be delivered to an appropriately decorated method. This is an integration test of the functionality covered by this suite and the functionality covered by L{AMPMessagingTests}. """ aliceAMP = RealAMPReceiver(store=self.aliceStore) getEveryoneRole(self.aliceStore).shareItem(aliceAMP, u'ally') bobAMP = RealAMPReceiver(store=self.bobStore) getEveryoneRole(self.bobStore).shareItem(bobAMP, u'bobby') msgr = AMPMessenger(self.aliceQueue, Identifier(u'ally', u'alice', u'example.com'), Identifier(u'bobby', u'bob', u'example.com')) msgr.messageRemote(SimpleCommand, int1=3, str2="hello") self.runQueue(self.aliceQueue) self.assertEqual(bobAMP.args, [(3, 'hello')]) class SimpleError(Exception): """ A simple error that should be detectable by L{SimpleCommand}. """ class SimpleCommand(Command): """ Sample simple command with a few arguments. """ arguments = [("int1", Integer()), ("str2", String())] response = [("int3", Integer())] errors = {SimpleError: "SIMPLE_ERROR"} class SenderCommand(Command): arguments = [("sender", SenderArgument())] response = [("sender", SenderArgument())] class TargetCommand(Command): arguments = [("target", TargetArgument())] response = [("target", TargetArgument())] class TrivialCommand(Command): """ Trivial no-argument AMP command. """ errors = {SimpleError: "SIMPLE_ERROR"} class RealAMPReceiver(Item, AMPReceiver): """ This is an integration testing item for making sure that decorated methods on items receiving messages will work. """ dummy = integer() args = inmemory() def activate(self): """ Set up test state. """ self.args = [] @commandMethod.expose(SimpleCommand) def doit(self, int1, str2): """ Simple responder for L{SimpleCommand}. """ self.args.append((int1, str2)) return dict(int3=int1+len(str2)) class MyAMPReceiver(AMPReceiver): """ A simple L{AMPReceiver} subclass with a few exposed methods. """ def __init__(self): """ Create a L{MyAMPReceiver}. """ self.commandArguments = [] self.commandAnswers = [] self.commandErrors = [] self.senders = [] self.targets = [] @commandMethod.expose(SimpleCommand) def simpleCommand(self, int1, str2): """ Responder for a simple command. """ self.commandArguments.append((int1, str2)) return dict(int3=4) @answerMethod.expose(SimpleCommand) def simpleAnswer(self, int3): """ Responder for a simple answer. """ self.commandAnswers.append(int3) @errorMethod.expose(SimpleCommand, SimpleError) def simpleError(self, failure): self.commandErrors.append(failure) @commandMethod.expose(SenderCommand) def commandWithSender(self, sender): self.senders.append(sender) return {} @commandMethod.expose(TargetCommand) def commandWithTarget(self, target): self.targets.append(target) return {} @answerMethod.expose(SenderCommand) def answerWithSender(self, sender): self.senders.append(sender) @answerMethod.expose(TargetCommand) def answerWithTarget(self, target): self.targets.append(target) class ExpectedBuggyReceiver(AMPReceiver): """ This AMP responder will raise an expected exception type. """ @commandMethod.expose(TrivialCommand) def raiseSimpleError(self): raise SimpleError("simple description") class UnexpectedBuggyReceiver(AMPReceiver): """ This AMP responder will raise an unexpected exception type. """ @commandMethod.expose(TrivialCommand) def raiseRuntimeError(self): raise RuntimeError() class AMPMessagingTests(TestCase): """ Test cases for high-level AMP message parsing and emitting API. """ def setUp(self): """ Initialize the list of messages which this can deliver as a pseudo-queue. """ self.messages = [] def queueMessage(self, sender, target, value, consequence=None): """ Emulate L{MessageQueue.queueMessage}. """ self.messages.append((sender, target, value.type, value.data, consequence)) def test_messageRemote(self): """ L{AMPMessenger.messageRemote} should queue a message with the provided queue, sender, and target, serializing its arguments according to the provided AMP command. """ sender = Identifier(u'test-sender', u'bob', u'example.com') target = Identifier(u'test-target', u'alice', u'example.com') msgr = AMPMessenger(self, sender, target) msgr.messageRemote(SimpleCommand, int1=3, str2="hello") expectedConsequence = None self.assertEqual(self.messages, [(sender, target, AMP_MESSAGE_TYPE, Box(_command="SimpleCommand", int1="3", str2="hello").serialize(), expectedConsequence)]) def test_messageReceived(self): """ L{AMPReceiver.messageReceived} should dispatch commands to methods that were appropriately decorated. """ amr = MyAMPReceiver() questionBox = Box(_command=SimpleCommand.commandName, int1="7", str2="test") data = questionBox.serialize() response = amr.messageReceived( Value(AMP_MESSAGE_TYPE, data), None, None) self.assertEqual(response.type, AMP_ANSWER_TYPE) self.assertEqual(amr.commandArguments, [(7, "test")]) self.assertEqual(response.data, Box(int3="4").serialize()) def test_messageReceivedHandledError(self): """ L{AMPReceiver.messageReceived} should emit a responseType and responseData of the appropriate type if the command in question has a translation of the raised error. """ bug = ExpectedBuggyReceiver() questionBox = Box(_command=TrivialCommand.commandName) data = questionBox.serialize() rar = self.assertRaises(RevertAndRespond, bug.messageReceived, Value(AMP_MESSAGE_TYPE, data), None, None) self.assertEqual(rar.value.type, AMP_ANSWER_TYPE) self.assertEqual(rar.value.data, Box(_error_code="SIMPLE_ERROR", _error_description="simple description") .serialize()) def test_messageReceivedUnhandledError(self): """ L{AMPReceiver.messageReceived} should allow error not defined by its command to be raised so that the normal L{ERROR_REMOTE_EXCEPTION} behavior takes over. """ bug = UnexpectedBuggyReceiver() questionBox = Box(_command=TrivialCommand.commandName) data = questionBox.serialize() self.assertRaises(RuntimeError, bug.messageReceived, Value(AMP_MESSAGE_TYPE, data), None, None) def test_answerReceived(self): """ L{AMPReceiver.answerReceived} should dispatch answers to methods that were appropriately decorated. """ originalMessageData = Box(_command=SimpleCommand.commandName).serialize() amr = MyAMPReceiver() answerBox = Box(int3="4") data = answerBox.serialize() amr.answerReceived(Value(AMP_ANSWER_TYPE, data), Value(None, originalMessageData), None, None) self.assertEqual(amr.commandAnswers, [4]) def test_errorReceived(self): """ L{AMPReceiver.answerReceived} should dispatch answers that indicate an AMP error to methods decorated by the L{errorMethod} decorator, not to the L{answerMethod} decorator. """ originalMessageData = Box(_command=SimpleCommand.commandName).serialize() amr = MyAMPReceiver() data = Box(_error="SIMPLE_ERROR").serialize() amr.answerReceived(Value(AMP_ANSWER_TYPE, data), Value(None, originalMessageData), None, None) self.assertEqual(amr.commandAnswers, []) amr.commandErrors.pop().trap(SimpleError) self.assertEqual(amr.commandErrors, []) def test_messageReceivedWrongType(self): """ An L{UnknownMessageType} should be raised when a message of the wrong type is dispatched to an L{AMPReceiver}. """ amr = MyAMPReceiver() questionBox = Box(_command=SimpleCommand.commandName, int1="7", str2="test") data = questionBox.serialize() for badType in u'some.random.type', AMP_ANSWER_TYPE: self.assertRaises(UnknownMessageType, amr.messageReceived, Value(badType, data), None, None) self.assertEqual(amr.commandArguments, []) def test_messageReceivedBadData(self): """ A L{MalformedMessage} should be raised when a message that cannot be interpreted as a single AMP box is received. """ amr = MyAMPReceiver() for badData in ["", Box().serialize() + Box().serialize()]: self.assertRaises(MalformedMessage, amr.messageReceived, Value(AMP_MESSAGE_TYPE, badData), None, None) def test_answerReceivedBadData(self): """ A L{MalformedMessage} should be raised when a message that cannot be interpreted as a single AMP box is received. """ originalMessageData = Box(_command=SimpleCommand.commandName).serialize() amr = MyAMPReceiver() for badData in ["", Box().serialize() + Box().serialize()]: self.assertRaises(MalformedMessage, amr.answerReceived, Value(AMP_ANSWER_TYPE, badData), Value(None, originalMessageData), None, None) def test_answerReceivedWrongType(self): """ An L{UnknownMessageType} exception should be raised when a answer of the wrong type is dispatched to an L{AMPReceiver}. """ originalMessageData = Box(_command=SimpleCommand.commandName).serialize() amr = MyAMPReceiver() answerBox = Box(int3="4") data = answerBox.serialize() for badType in u'some.random.type', AMP_MESSAGE_TYPE: self.assertRaises(UnknownMessageType, amr.answerReceived, Value(badType, data), Value(None, originalMessageData), None, None) self.assertEqual(amr.commandAnswers, []) def test_messageReceivedSenderArgument(self): """ The special argument L{TargetArgument} should cause the L{sender} argument to L{messageReceived} to be passed as an argument to the responder method. """ amr = MyAMPReceiver() shareident = Identifier(u'abc', u'def', u'ghi') amr.messageReceived( Value(AMP_MESSAGE_TYPE, Box(_command=SenderCommand.commandName).serialize()), shareident, None) self.assertEqual([shareident], amr.senders) def test_messageReceivedTargetArgument(self): """ The special argument L{TargetArgument} should cause the L{sender} argument to L{messageReceived} to be passed as an argument to the responder method. """ amr = MyAMPReceiver() shareident = Identifier(u'abc', u'def', u'ghi') amr.messageReceived(Value(AMP_MESSAGE_TYPE, Box(_command=TargetCommand.commandName).serialize()), None, shareident) self.assertEqual([shareident], amr.targets) def test_answerReceivedSenderArgument(self): """ The special argument L{SenderArgument} should cause the L{originalSender} argument to L{answerReceived} to be passed as an argument to the responder method. """ amr = MyAMPReceiver() shareident = Identifier(u'abc', u'def', u'ghi') amr.answerReceived( Value(AMP_ANSWER_TYPE, Box().serialize()), Value(None, Box(_command=SenderCommand.commandName).serialize()), shareident, None) self.assertEqual([shareident], amr.senders) def test_answerReceivedTargetArgument(self): """ The special argument L{TargetArgument} should cause the L{originalTarget} argument to L{answerReceived} to be passed as an argument to the responder method. """ amr = MyAMPReceiver() shareident = Identifier(u'abc', u'def', u'ghi') amr.answerReceived( Value(AMP_ANSWER_TYPE, Box().serialize()), Value(None, Box(_command=TargetCommand.commandName).serialize()), None, shareident) self.assertEqual([shareident], amr.targets) class ExpositionTests(TestCase): """ Tests for exposing methods with the L{_AMPExposer.expose} decorator, and retrieving them with the L{_AMPExposer.responderForName} lookup method. """ def setUp(self): """ Set up a local L{_AMPExposer} instance for testing. """ self.ampExposer = _AMPExposer("amp exposer for testing") self.errorExposer = _AMPErrorExposer("lulz") def test_exposeCommand(self): """ Exposing a method as a command object ought to make it accessible to L{responderForName}, and add a matching C{command} attribute to that result. """ class TestClass(object): def __init__(self, x): self.num = x @self.ampExposer.expose(TrivialCommand) def thunk(self): return 'hi', self.num + 1 tc = TestClass(3) callable = self.ampExposer.responderForName(tc, TrivialCommand.commandName) self.assertEqual(callable(), ("hi", 4)) self.assertIdentical(callable.command, TrivialCommand) def test_exposeError(self): """ A method exposed as an error handler for a particular type of error should be able to be looked up by the combination of the command and the error. """ class TestClass(object): @self.errorExposer.expose(SimpleCommand, SimpleError) def thunk(self): raise SimpleError() tc = TestClass() thunk = self.errorExposer.errbackForName(tc, SimpleCommand.commandName, "SIMPLE_ERROR") self.assertEqual(thunk.exception, SimpleError) self.assertEqual(thunk.command, SimpleCommand) self.assertRaises(SimpleError, thunk) def test_exposeOnTwoTypes(self): """ An integration test with L{epsilon.expose}; sanity checking to make sure that exposing different methods on different classes for the same command name yields the same results. """ class TestClass(object): @self.ampExposer.expose(TrivialCommand) def thunk(self): return 1 class TestClass2: @self.ampExposer.expose(TrivialCommand) def funk(self): return 2 tc2 = TestClass2() callable = self.ampExposer.responderForName(tc2, TrivialCommand.commandName) self.assertEqual(callable(), 2) tc = TestClass() callable = self.ampExposer.responderForName(tc, TrivialCommand.commandName) self.assertEqual(callable(), 1) PK9F3vv!xmantissa/test/test_javascript.py# Copyright (c) 2006 Divmod. # See LICENSE for details. """ Runs mantissa javascript tests as part of the mantissa python tests """ from nevow.testutil import JavaScriptTestCase class MantissaJavaScriptTestCase(JavaScriptTestCase): """ Run all the mantissa javascript test """ def test_scrollmodel(self): """ Test the model object which tracks most client-side state for any ScrollTable. """ return 'Mantissa.Test.TestScrollModel' def test_placeholders(self): """ Test the model objects which track placeholder nodes in the message scrolltable. """ return 'Mantissa.Test.TestPlaceholder' def test_autocomplete(self): """ Tests the model object which tracks client-side autocomplete state """ return 'Mantissa.Test.TestAutoComplete' def test_region(self): """ Test the model objects which track placeholder nodes in the message scrolltable. """ return 'Mantissa.Test.TestRegionModel' def test_people(self): """ Tests the model objects which deal with the address book and person objects. """ return 'Mantissa.Test.TestPeople' def test_validate(self): """ Test the class which validates input on the signup page and posts it to the server. """ return 'Mantissa.Test.TestValidate' def test_liveform(self): """ Test the LiveForm widgets. """ return 'Mantissa.Test.TestLiveForm' def test_domReplace(self): """ Test the stuff which replaces things in the DOM. """ return 'Mantissa.Test.TestDOMReplace' def test_offering(self): """ Tests for the offering administration interface. """ return 'Mantissa.Test.TestOffering' PK9Fu5xmantissa/test/test_liveform.py """ Tests for L{xmantissa.liveform}. """ from xml.dom.minidom import parseString from zope.interface import implements from zope.interface.verify import verifyObject from twisted.internet.defer import Deferred from twisted.trial.unittest import TestCase from epsilon.hotfix import require require('twisted', 'trial_assertwarns') from nevow.page import renderer from nevow.tags import directive, div, span from nevow.loaders import stan from nevow.flat import flatten from nevow.inevow import IQ from nevow.athena import expose from xmantissa.liveform import ( FORM_INPUT, TEXT_INPUT, PASSWORD_INPUT, CHOICE_INPUT, Parameter, TextParameterView, PasswordParameterView, ChoiceParameter, ChoiceParameterView, Option, OptionView, LiveForm, ListParameter, ListChangeParameterView, ListChangeParameter, RepeatedLiveFormWrapper, _LIVEFORM_JS_CLASS, _SUBFORM_JS_CLASS, EditObject, FormParameter, FormParameterView, FormInputParameterView) from xmantissa.webtheme import getLoader from xmantissa.test.rendertools import TagTestingMixin, renderLiveFragment from xmantissa.ixmantissa import IParameter, IParameterView class StubView(object): """ Behaviorless implementation of L{IParameterView} used where such an object is required by tests. """ implements(IParameterView) patternName = 'text' def setDefaultTemplate(self, tag): """ Ignore the default template tag. """ class ParameterTestsMixin: """ Mixin defining various tests common to different parameter view objects. """ def viewFactory(self, parameter): """ Instantiate a view object for the given parameter. """ raise NotImplementedError("%s did not implement viewFactory") def test_fromInputs(self): """ The parameter should provide a C{fromInputs} method for LiveForm to poke. """ self.assertTrue( hasattr(self.param, 'fromInputs'), "Parameter did not even have fromInputs method, let alone " "implement it correctly. Override this test method and " "assert something meaningful.") def test_comparison(self): """ Parameter view objects should compare equal to other view objects of the same type which wrap the same underlying parameter object. """ self.assertTrue(self.viewFactory(self.param) == self.viewFactory(self.param)) self.assertFalse(self.viewFactory(self.param) != self.viewFactory(self.param)) self.assertFalse(self.viewFactory(self.param) == object()) self.assertTrue(self.viewFactory(self.param) != object()) def test_name(self): """ The I{name} renderer of the view object should render the name of the L{Parameter} it wraps as a child of the tag it is given. """ tag = div() renderedName = renderer.get(self.view, 'name')(None, tag) self.assertTag(tag, 'div', {}, [self.name]) def test_label(self): """ The I{label} renderer of the view object should render the label of the L{Parameter} it wraps as a child of the tag it is given. """ tag = div() renderedLabel = renderer.get(self.view, 'label')(None, tag) self.assertTag(tag, 'div', {}, [self.label]) def test_withoutLabel(self): """ The I{label} renderer of the view object should do nothing if the wrapped L{Parameter} has no label. """ tag = div() self.param.label = None renderedOptions = renderer.get(self.view, 'label')(None, tag) self.assertTag(renderedOptions, 'div', {}, []) def _defaultRenderTest(self, fragmentName): loader = getLoader(fragmentName) document = loader.load() patternName = self.view.patternName + '-input-container' pattern = IQ(document).onePattern(patternName) self.view.setDefaultTemplate(pattern) html = flatten(self.view) # If it parses, well, that's the best we can do, given an arbitrary # template. document = parseString(html) def test_renderWithDefault(self): """ The parameter view should be renderable using the default template. """ return self._defaultRenderTest('liveform') def test_renderWithCompact(self): """ The parameter view should be renderable using the compact template. """ return self._defaultRenderTest('liveform-compact') def test_clone(self): """ The parameter should be cloneable. """ newDefault = object() param = self.param.clone(newDefault) self.assertTrue(isinstance(param, self.param.__class__)) self.assertIdentical(param.name, self.param.name) self.assertIdentical(param.label, self.param.label) self.assertIdentical(param.description, self.param.description) self.assertIdentical(param.coercer, self.param.coercer) self.assertIdentical(param.default, newDefault) self.assertIdentical(param.viewFactory, self.param.viewFactory) class TextLikeParameterViewTestsMixin: """ Mixin defining tests for parameter views which are simple text fields. """ def type(): def get(self): raise AttributeError("%s did not define the type attribute") return get, type = property(*type()) name = u'param name' label = u'param label' coercer = lambda value: value description = u'param desc' default = u'param default value' def setUp(self): """ Create a L{Parameter} and a L{TextParameterView} wrapped around it. """ self.param = Parameter( self.name, self.type, self.coercer, self.label, self.description, self.default) self.view = self.viewFactory(self.param) def test_default(self): """ L{TextParameterView.value} should render the default value of the L{Parameter} it wraps as a child of the tag it is given. """ tag = div() renderedDefault = renderer.get(self.view, 'default')(None, tag) self.assertTag(tag, 'div', {}, [self.default]) def test_withoutDefault(self): """ L{TextParameterView.value} should leave the tag it is given unchanged if the L{Parameter} it wraps has a C{None} default. """ tag = div() self.param.default = None renderedDefault = renderer.get(self.view, 'default')(None, tag) self.assertTag(tag, 'div', {}, []) def test_description(self): """ L{TextParameterView.description} should render the description of the L{Parameter} it wraps as a child of the tag it is given. """ tag = div() renderedDescription = renderer.get(self.view, 'description')(None, tag) self.assertTag(tag, 'div', {}, [self.description]) def test_withoutDescription(self): """ L{TextParameterView.description} should leave the tag it is given unchanged if the L{Parameter} it wraps has no description. """ tag = div() self.param.description = None renderedDescription = renderer.get(self.view, 'description')(None, tag) self.assertTag(tag, 'div', {}, []) def test_renderCompletely(self): """ L{TextParameterView} should be renderable in the usual Nevow manner. """ self.view.docFactory = stan(div[ div(render=directive('name')), div(render=directive('label')), div(render=directive('default')), div(render=directive('description'))]) html = flatten(self.view) self.assertEqual( html, '
param name
param label
' '
param default value
param desc
') class TextParameterViewTests(TextLikeParameterViewTestsMixin, TestCase, ParameterTestsMixin, TagTestingMixin): """ Tests for the view generation code for C{TEXT_INPUT} L{Parameter} instances. """ type = TEXT_INPUT viewFactory = TextParameterView class PasswordParameterViewTests(TextLikeParameterViewTestsMixin, TestCase, ParameterTestsMixin, TagTestingMixin): """ Tests for the view generation code for C{PASSWORD_INPUT} L{Parameter} instances. """ type = PASSWORD_INPUT viewFactory = PasswordParameterView class ChoiceParameterTests(TestCase, ParameterTestsMixin, TagTestingMixin): """ Tests for the view generation code for C{CHOICE_INPUT} L{Parameter} instances. """ viewFactory = ChoiceParameterView def setUp(self): """ Create a L{Parameter} and a L{ChoiceParameterView} wrapped around it. """ self.type = CHOICE_INPUT self.name = u'choice name' self.choices = [ Option(u'description one', u'value one', False), Option(u'description two', u'value two', False)] self.label = u'choice label' self.description = u'choice description' self.multiple = False self.param = ChoiceParameter( self.name, self.choices, self.label, self.description, self.multiple) self.view = self.viewFactory(self.param) def test_multiple(self): """ L{ChoiceParameterView.multiple} should render the multiple attribute on the tag it is passed if the wrapped L{ChoiceParameter} is a L{MULTI_CHOICE_INPUT}. """ tag = div() self.param.multiple = True renderedSelect = renderer.get(self.view, 'multiple')(None, tag) self.assertTag(tag, 'div', {'multiple': 'multiple'}, []) def test_fromInputs(self): """ L{ChoiceParameter.fromInputs} should extract the inputs directed at it and pass them on to the coerce function. """ self.assertEqual( self.param.fromInputs({self.name: ['0']}), self.choices[0].value) def test_single(self): """ L{ChoiceParameterView.multiple} should not render the multiple attribute on the tag it is passed if the wrapped L{ChoiceParameter} is a L{CHOICE_INPUT}. """ tag = div() renderedSelect = renderer.get(self.view, 'multiple')(None, tag) self.assertTag(tag, 'div', {}, []) def test_options(self): """ L{ChoiceParameterView.options} should load the I{option} pattern from the tag it is passed and add copies of it as children to the tag for all of the options passed to L{ChoiceParameterView.__init__}. """ option = span(pattern='option') tag = div[option] renderedOptions = renderer.get(self.view, 'options')(None, tag) self.assertEqual( renderedOptions.children[1:], [OptionView(index, c, None) for (index, c) in enumerate(self.choices)]) def test_description(self): """ L{ChoiceParameterView.description} should add the description of the wrapped L{ChoiceParameter} to the tag it is passed. """ tag = div() renderedOptions = renderer.get(self.view, 'description')(None, tag) self.assertTag(renderedOptions, 'div', {}, [self.description]) def test_withoutDescription(self): """ L{ChoiceParameterView.description} should do nothing if the wrapped L{ChoiceParameter} has no description. """ tag = div() self.param.description = None renderedOptions = renderer.get(self.view, 'description')(None, tag) self.assertTag(renderedOptions, 'div', {}, []) def test_clone(self): """ L{ChoiceParameter} instances should be cloneable. """ newChoices = [object()] param = self.param.clone(newChoices) self.assertTrue(isinstance(param, self.param.__class__)) self.assertIdentical(param.name, self.param.name) self.assertIdentical(param.label, self.param.label) self.assertIdentical(param.type, self.param.type) self.assertIdentical(param.description, self.param.description) self.assertIdentical(param.multiple, self.param.multiple) self.assertIdentical(param.choices, newChoices) self.assertIdentical(param.viewFactory, self.param.viewFactory) class OptionTests(TestCase, TagTestingMixin): """ Tests for the view generation code for a single choice, L{OptionView}. """ simpleOptionTag = div[ div(render=directive('description')), div(render=directive('value')), div(render=directive('index')), div(render=directive('selected'))] def setUp(self): """ Create an L{Option} and an L{OptionView} wrapped around it. """ self.description = u'option description' self.value = u'option value' self.selected = True self.option = Option(self.description, self.value, self.selected) self.index = 3 self.view = OptionView(self.index, self.option, self.simpleOptionTag) def test_description(self): """ L{OptionView.description} should add the description of the option it wraps as a child to the tag it is passed. """ tag = div() renderedDescription = renderer.get(self.view, 'description')(None, tag) self.assertTag(renderedDescription, 'div', {}, [self.description]) def test_value(self): """ L{OptionView.value} should add the value of the option it wraps as a child to the tag it is passed. """ tag = div() renderedValue = renderer.get(self.view, 'value')(None, tag) self.assertTag(renderedValue, 'div', {}, [self.value]) def test_index(self): """ L{OptionView.index} should add the index passed to L{OptionView.__init__} to the tag it is passed. """ tag = div() renderedIndex = renderer.get(self.view, 'index')(None, tag) self.assertTag(renderedIndex, 'div', {}, [self.index]) def test_selected(self): """ L{OptionView.selected} should add a I{selected} attribute to the tag it is passed if the option it wraps is selected. """ tag = div() renderedValue = renderer.get(self.view, 'selected')(None, tag) self.assertTag(renderedValue, 'div', {'selected': 'selected'}, []) def test_notSelected(self): """ L{OptionView.selected} should not add a I{selected} attribute to the tag it is passed if the option it wraps is not selected. """ self.option.selected = False tag = div() renderedValue = renderer.get(self.view, 'selected')(None, tag) self.assertTag(renderedValue, 'div', {}, []) def test_renderCompletely(self): """ L{ChoiceParameterView} should be renderable in the usual Nevow manner. """ html = flatten(self.view) self.assertEqual( html, '
option description
option value
' '
3
') class ListParameterTests(TestCase): """ Tests for L{ListParameter}. """ def test_fromInputs(self): """ L{ListParameter.fromInputs} should extract multiple values from the mapping it is passed and coerce each value, returning a Deferred which fires with a list of all of the coerced values. """ name = u'list param' param = ListParameter(name, int, 2) result = param.fromInputs({name + u'_0': [u'1'], name + u'_1': [u'3']}) result.addCallback(self.assertEqual, [1, 3]) return result class FormParameterTests(TestCase): """ Tests for L{Parameter} created with a type of L{FORM_INPUT} and for L{FormParameter}. """ def test_deprecated(self): """ Creating a L{Parameter} with a type of L{FORM_INPUT} should emit a deprecation warning referring to L{FormParameter}. """ self.assertWarns( DeprecationWarning, "Create a FormParameter, not a Parameter with type FORM_INPUT", __file__, lambda: Parameter(None, FORM_INPUT, None)) def test_viewFactory(self): """ L{FormParameter.viewFactory} should return a L{FormParameterView} wrapped around the parameter. """ parameter = FormParameter(lambda **kw: None, LiveForm(None, [])) view = parameter.viewFactory(parameter, None) self.assertTrue(isinstance(view, FormParameterView)) self.assertIdentical(view.parameter, parameter) def test_provides(self): """ L{FormParameter} should provide L{IParameter}. """ parameter = FormParameter(u'name', None) self.assertTrue(IParameter.providedBy(parameter)) self.assertTrue(verifyObject(IParameter, parameter)) def test_fromInputs(self): """ L{FormParameter.fromInputs} should extract the input value which corresponds to the parameter's name and pass it to the invoke method of the wrapped form. """ value = '-13' invoked = {} param = u'foo' form = LiveForm(invoked.update, [Parameter(param, TEXT_INPUT, int)]) name = u'name' parameter = FormParameter(name, form) parameter.fromInputs({name: [{param: [value]}]}) self.assertEqual(invoked, {param: int(value)}) def test_compact(self): """ L{FormParameter.compact} should call compact on the wrapped form. """ class FakeForm(object): isCompact = False def compact(self): self.isCompact = True form = FakeForm() parameter = FormParameter(None, form) parameter.compact() self.assertTrue(form.isCompact) class FormParameterViewTests(TestCase): """ Tests for L{FormParameterView}. """ def test_provides(self): """ L{FormParameterView} should provide L{IParameterView}. """ self.assertTrue(IParameterView.providedBy(FormParameterView(None))) def test_inputs(self): """ The I{input} renderer of L{FormParameterView} should add a subform from its wrapped form as a child to the tag it is called with. """ form = LiveForm(None, []) name = u'bar' parameter = FormParameter(name, form) view = FormParameterView(parameter) tag = div() inputRenderer = renderer.get(view, 'input') tag = inputRenderer(None, div()) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) self.assertEqual(tag.children, [form]) self.assertIdentical(form.fragmentParent, view) self.assertEqual(form.subFormName, name) class FormInputParameterViewTests(TestCase): """ Tests for deprecated L{FormInputParameterView}. """ def test_inputs(self): """ The I{input} renderer of L{FormParameterView} should add a subform from its wrapped form as a child to the tag it is called with. """ form = LiveForm(None, []) name = u'bar' type = FORM_INPUT form = LiveForm(None, []) parameter = Parameter(name, type, form) view = FormInputParameterView(parameter) tag = div() inputRenderer = renderer.get(view, 'input') tag = inputRenderer(None, div()) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) self.assertEqual(tag.children, [form]) self.assertIdentical(form.fragmentParent, view) self.assertEqual(form.subFormName, name) class LiveFormTests(TestCase, TagTestingMixin): """ Tests for the form generation code in L{LiveForm}. """ # Minimal tag which can be used with the form renderer. Classes are only # used to tell nodes apart in the tests. simpleLiveFormTag = div[ span(pattern='text-input-container'), span(pattern='password-input-container'), span(pattern='form-input-container'), span(pattern='liveform', _class='liveform-container'), span(pattern='subform', _class='subform-container')] def test_compact(self): """ L{LiveForm.compact} should replace the existing C{docFactory} with one for the I{compact} version of the live form template. """ form = LiveForm(None, []) form.compact() self.assertTrue(form.docFactory.template.endswith('/liveform-compact.html')) def test_recursiveCompact(self): """ L{LiveForm.compact} should also call C{compact} on all of its subforms. """ class StubChild(object): compacted = False def compact(self): self.compacted = True child = StubChild() form = LiveForm(None, [Parameter('foo', FORM_INPUT, child), Parameter('bar', TEXT_INPUT, int), ListParameter('baz', None, 3), ChoiceParameter('quux', [])]) form.compact() self.assertTrue(child.compacted) def test_descriptionSlot(self): """ L{LiveForm.form} should fill the I{description} slot on the tag it is passed with the description of the form. """ description = u"the form description" formFragment = LiveForm(None, [], description) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual(formTag.slotData['description'], description) def test_formSlotOuter(self): """ When it is not nested inside another form, L{LiveForm.form} should fill the I{form} slot on the tag with the tag's I{liveform} pattern. """ def submit(**kw): pass formFragment = LiveForm(submit, []) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertTag( formTag.slotData['form'], 'span', {'class': 'liveform-container'}, []) def test_formSlotInner(self): """ When it has a sub-form name, L{LiveForm.form} should fill the I{form} slot on the tag with the tag's I{subform} pattern. """ def submit(**kw): pass formFragment = LiveForm(submit, []) formFragment.subFormName = 'test-subform' formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertTag( formTag.slotData['form'], 'span', {'class': 'subform-container'}, []) def test_noParameters(self): """ When there are no parameters, L{LiveForm.form} should fill the I{inputs} slot on the tag it uses to fill the I{form} slot with an empty list. """ def submit(**kw): pass formFragment = LiveForm(submit, []) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual(formTag.slotData['form'].slotData['inputs'], []) def test_parameterViewOverride(self): """ L{LiveForm.form} should use the C{view} attribute of parameter objects, if it is not C{None}, to fill the I{inputs} slot on the tag it uses to fill the I{form} slot. """ def submit(**kw): pass name = u'param name' label = u'param label' type = TEXT_INPUT coercer = lambda value: value description = u'param desc' default = u'param default value' view = StubView() views = {} viewFactory = views.get param = Parameter( name, type, coercer, label, description, default, viewFactory) views[param] = view formFragment = LiveForm(submit, [param]) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual( formTag.slotData['form'].slotData['inputs'], [view]) def test_individualTextParameter(self): """ L{LiveForm.form} should fill the I{inputs} slot on the tag it uses to fill the I{form} slot with a list consisting of one L{TextParameterView} when the L{LiveForm} is created with one C{TEXT_INPUT} L{Parameter}. """ def submit(**kw): pass name = u'param name' label = u'param label' type = TEXT_INPUT coercer = lambda value: value description = u'param desc' default = u'param default value' param = Parameter( name, type, coercer, label, description, default) formFragment = LiveForm(submit, [param]) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual( formTag.slotData['form'].slotData['inputs'], [TextParameterView(param)]) def test_individualPasswordParameter(self): """ L{LiveForm.form} should fill the I{inputs} slot of the tag it uses to fill the I{form} slot with a list consisting of one L{TextParameterView} when the L{LiveForm} is created with one C{PASSWORD_INPUT} L{Parameter}. """ def submit(**kw): pass name = u'param name' label = u'param label' type = PASSWORD_INPUT coercer = lambda value: value description = u'param desc' default = u'param default value' param = Parameter( name, type, coercer, label, description, default) formFragment = LiveForm(submit, [param]) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual( formTag.slotData['form'].slotData['inputs'], [PasswordParameterView(param)]) def test_individualFormParameter(self): """ L{LiveForm.form} should fill the I{inputs} slot of the tag it uses to fill the I{form} slot with a list consisting of one L{FormParameterView} when the L{LiveForm} is created with one L{FormParameter}. """ parameter = FormParameter(u'form param', LiveForm(None, [])) formFragment = LiveForm(lambda **kw: None, [parameter]) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual( formTag.slotData['form'].slotData['inputs'], [FormParameterView(parameter)]) def test_individualFormInputParameter(self): """ L{LiveForm.form} should fill the I{inputs} slot of the tag it uses to fill the I{form} slot with a list consisting of one L{FormInputParameterView} when the L{LiveForm} is created with one C{FORM_INPUT} L{Parameter}. """ def submit(**kw): pass name = u'param name' type = FORM_INPUT coercer = LiveForm(None, []) param = Parameter(name, type, coercer) formFragment = LiveForm(submit, [param]) formTag = formFragment.form(None, self.simpleLiveFormTag) self.assertEqual( formTag.slotData['form'].slotData['inputs'], [FormInputParameterView(param)]) def test_liveformTemplateStructuredCorrectly(self): """ When a L{LiveForm} is rendered using the default template, the form contents should end up inside the I{form} tag. As I understand it, this is a necessary condition for the resulting html form to operate properly. However, due to the complex behavior of the HTML (or even XHTML) DOM and the inscrutability of the various specific implementations of it, it is not entirely unlikely that my understanding is, in some way, flawed. If you know better, and believe this test to be in error, supplying a superior test or simply deleting this one may not be out of the question. -exarkun """ def submit(**kw): pass name = u'param name' label = u'param label' type = PASSWORD_INPUT coercer = lambda value: value description = u'param desc' default = u'param default value' param = Parameter( name, type, coercer, label, description, default) formFragment = LiveForm(submit, [param]) html = renderLiveFragment(formFragment) document = parseString(html) forms = document.getElementsByTagName('form') self.assertEqual(len(forms), 1) inputs = forms[0].getElementsByTagName('input') self.assertTrue(len(inputs) >= 1) def test_liveFormJSClass(self): """ Verify that the C{jsClass} attribute of L{LiveForm} is L{_LIVEFORM_JS_CLASS}. """ self.assertEqual(LiveForm.jsClass, _LIVEFORM_JS_CLASS) def test_subFormJSClass(self): """ Verify that the C{jsClass} attribute of the form returned from L{LiveForm.asSubForm} is L{_SUBFORM_JS_CLASS}. """ liveForm = LiveForm(lambda **k: None, ()) subForm = liveForm.asSubForm(u'subform') self.assertEqual(subForm.jsClass, _SUBFORM_JS_CLASS) def test_invoke(self): """ L{LiveForm.invoke} should take the post dictionary it is passed, call the coercer for each of its parameters, take the output from each, whether it is available synchronously or as a Deferred result, and pass the aggregate to the callable the L{LiveForm} was instantiated with. It should return a L{Deferred} which fires with the result of the callable when it is available. """ arguments = {} submitResult = object() def submit(**args): arguments.update(args) return submitResult syncCoerces = [] syncResult = object() def syncCoercer(value): syncCoerces.append(value) return syncResult sync = Parameter(u'sync', None, syncCoercer, None, None, None) asyncCoerces = [] asyncResult = Deferred() def asyncCoercer(value): asyncCoerces.append(value) return asyncResult async = Parameter(u'async', None, asyncCoercer, None, None, None) form = LiveForm(submit, [sync, async]) invokeResult = form.invoke({sync.name: [u'sync value'], async.name: [u'async value']}) # Both of the coercers should have been called with their respective # values. self.assertEqual(syncCoerces, [u'sync value']) self.assertEqual(asyncCoerces, [u'async value']) # The overall form callable should not have been called yet, since a # Deferred is still outstanding. self.assertEqual(arguments, {}) # This will be the result of the Deferred from asyncCoercer asyncObject = object() def cbInvoke(result): self.assertEqual(result, submitResult) self.assertEqual(arguments, {sync.name: syncResult, async.name: asyncObject}) invokeResult.addCallback(cbInvoke) asyncResult.callback(asyncObject) return invokeResult def test_callingInvokes(self): """ Calling a LiveForm should be the same as calling its invoke method. This isn't a public API. """ returnValue = object() calledWith = [] def coercer(**params): calledWith.append(params) return returnValue form = LiveForm( coercer, [Parameter(u'name', TEXT_INPUT, unicode, u'label', u'descr', u'default')]) result = form({u'name': [u'value']}) self.assertEqual(calledWith, [{u'name': u'value'}]) result.addCallback(self.assertIdentical, returnValue) return result class ListChangeParameterViewTestCase(TestCase): """ Tests for L{ListChangeParameterView}. """ def setUp(self): class TestableLiveForm(LiveForm): _isCompact = False def compact(self): self._isCompact = True self.innerParameters = [Parameter('foo', TEXT_INPUT, int)] self.parameter = ListChangeParameter( u'repeatableFoo', self.innerParameters, defaults=[], modelObjects=[]) self.parameter.liveFormFactory = TestableLiveForm self.parameter.repeatedLiveFormWrapper = RepeatedLiveFormWrapper self.view = ListChangeParameterView(self.parameter) def test_patternName(self): """ L{ListChangeParameterView} should use I{repeatable-form} as its C{patternName} """ self.assertEqual(self.view.patternName, 'repeatable-form') def _doSubFormTest(self, subFormWrapper): """ C{subFormWrapper} (which we expect to be the result of L{self.parameter.formFactory}, wrapped in L{self.parameter.repeatedLiveFormWrapper) should be a render-ready liveform that knows its a subform. """ self.failUnless( isinstance(subFormWrapper, RepeatedLiveFormWrapper)) self.assertIdentical(subFormWrapper.fragmentParent, self.view) subForm = subFormWrapper.liveForm self.assertEqual(self.innerParameters, subForm.parameters) self.assertEqual(subForm.subFormName, self.parameter.name) def test_formsRendererReturnsSubForm(self): """ The C{forms} renderer of L{ListChangeParameterView} should render the liveform that was passed to the underlying parameter, as a subform. """ (form,) = renderer.get(self.view, 'forms')(None, None) self._doSubFormTest(form) def test_repeatFormReturnsSubForm(self): """ The C{repeatForm} exposed method of L{ListChangeParameterView} should return the liveform that was passed to the underlying parameter, as a subform. """ self._doSubFormTest(expose.get(self.view, 'repeatForm')()) def test_formsRendererCompact(self): """ The C{forms} renderer of L{ListChangeParameterView} should call C{compact} on the form it returns, if the parameter it is wrapping had C{compact} called on it. """ self.parameter.compact() (renderedForm,) = renderer.get(self.view, 'forms')(None, None) self.failUnless(renderedForm.liveForm._isCompact) def test_repeatFormCompact(self): """ The C{repeatForm} exposed method of of L{ListChangeParameterView} should call C{compact} on the form it returns, if the parameter it is wrapping had C{compact} called on it. """ self.parameter.compact() renderedForm = expose.get(self.view, 'repeatForm')() self.failUnless(renderedForm.liveForm._isCompact) def test_formsRendererNotCompact(self): """ The C{forms} renderer of L{ListChangeParameterView} shouldn't call C{compact} on the form it returns, unless the parameter it is wrapping had C{compact} called on it. """ (renderedForm,) = renderer.get(self.view, 'forms')(None, None) self.failIf(renderedForm.liveForm._isCompact) def test_repeatFormNotCompact(self): """ The C{repeatForm} exposed method of L{ListChangeParameterView} shouldn't call C{compact} on the form it returns, unless the parameter it is wrapping had C{compact} called on it. """ renderedForm = expose.get(self.view, 'repeatForm')() self.failIf(renderedForm.liveForm._isCompact) def test_repeaterRenderer(self): """ The C{repeater} renderer of L{ListChangeParameterView} should return an instance of the C{repeater} pattern from its docFactory. """ self.view.docFactory = stan(div(pattern='repeater', foo='bar')) renderedTag = renderer.get(self.view, 'repeater')(None, None) self.assertEqual(renderedTag.attributes['foo'], 'bar') class ListChangeParameterTestCase(TestCase): """ Tests for L{ListChangeParameter}. """ _someParameters = (Parameter('foo', TEXT_INPUT, int),) def setUp(self): self.innerParameters = [Parameter('foo', TEXT_INPUT, int)] self.defaultValues = {u'foo': -56} self.defaultObject = object() self.parameter = ListChangeParameter( u'repeatableFoo', self.innerParameters, defaults=[self.defaultValues], modelObjects=[self.defaultObject]) def getListChangeParameter(self, parameters, defaults): return ListChangeParameter( name=u'stateRepeatableFoo', parameters=parameters, defaults=defaults, modelObjects=[object() for i in range(len(defaults))]) def test_asLiveForm(self): """ L{ListChangeParameter.asLiveForm} should wrap forms in L{ListChangeParameter.liveFormWrapperFactory}. """ parameter = self.getListChangeParameter(self._someParameters, []) parameter.repeatedLiveFormWrapper = RepeatedLiveFormWrapper liveFormWrapper = parameter.asLiveForm() self.failUnless( isinstance(liveFormWrapper, RepeatedLiveFormWrapper)) self.assertTrue(liveFormWrapper.removable) liveForm = liveFormWrapper.liveForm self.failUnless(isinstance(liveForm, LiveForm)) self.assertEqual(liveForm.subFormName, parameter.name) self.assertEqual(liveForm.parameters, self._someParameters) def test_getInitialLiveForms(self): """ Same as L{test_asLiveForm}, but looks at the single liveform returned from L{ListChangeParameter.getInitialLiveForms} when the parameter was constructed with no defaults. """ parameter = self.getListChangeParameter(self._someParameters, []) parameter.repeatedLiveFormWrapper = RepeatedLiveFormWrapper liveFormWrappers = parameter.getInitialLiveForms() self.assertEqual(len(liveFormWrappers), 1) liveFormWrapper = liveFormWrappers[0] self.failUnless( isinstance(liveFormWrapper, RepeatedLiveFormWrapper)) self.assertFalse(liveFormWrapper.removable) liveForm = liveFormWrapper.liveForm self.failUnless(isinstance(liveForm, LiveForm)) self.assertEqual(liveForm.subFormName, parameter.name) self.assertEqual(liveForm.parameters, self._someParameters) def test_getInitialLiveFormsDefaults(self): """ Same as L{test_getInitialLiveForms}, but for the case where the parameter was constructed with default values. """ defaults = [{'foo': 1}, {'foo': 3}] parameter = self.getListChangeParameter(self._someParameters, defaults) parameter.repeatedLiveFormWrapper = RepeatedLiveFormWrapper liveFormWrappers = parameter.getInitialLiveForms() self.assertEqual(len(liveFormWrappers), len(defaults)) for (liveFormWrapper, default) in zip(liveFormWrappers, defaults): self.failUnless( isinstance(liveFormWrapper, RepeatedLiveFormWrapper)) self.assertTrue(liveFormWrapper.removable) liveForm = liveFormWrapper.liveForm self.failUnless(isinstance(liveForm, LiveForm)) self.assertEqual(liveForm.subFormName, parameter.name) self.assertEqual(len(liveForm.parameters), 1) # Matches up with self._someParameters, except the default should # be different. innerParameter = liveForm.parameters[0] self.assertEqual(innerParameter.name, 'foo') self.assertEqual(innerParameter.type, TEXT_INPUT) self.assertEqual(innerParameter.coercer, int) self.assertEqual(innerParameter.default, default['foo']) def test_identifierMapping(self): """ L{ListChangeParameter} should be able to freely convert between python objects and the opaque identifiers generated from them. """ defaultObject = object() identifier = self.parameter._idForObject(defaultObject) self.assertIdentical( self.parameter._objectFromID(identifier), defaultObject) def test_extractCreations(self): """ L{RepeatableFormParameter._extractCreations} should return a list of two-tuples giving the identifiers of new objects being created and their associated uncoerced arguments. """ key = u'key' (modificationIdentifier,) = self.parameter._idsToObjects.keys() modificationValue = u'edited value' creationIdentifier = self.parameter._newIdentifier() creationValue = u'created value' dataSets = [ {self.parameter._IDENTIFIER_KEY: creationIdentifier, key: creationValue}, {self.parameter._IDENTIFIER_KEY: modificationIdentifier, key: modificationValue}] creations = list(self.parameter._extractCreations(dataSets)) self.assertEqual(creations, [(creationIdentifier, {key: creationValue})]) def test_extractEdits(self): """ L{RepeatableFormParameter._extractEdits} should return a list of two-tuples giving the identifiers of existing model objects which might be about to change and their associated uncoerced arguments. """ key = u'key' creationIdentifier = self.parameter._newIdentifier() creationValue = u'created value' modificationIdentifier = self.parameter._idForObject( self.defaultObject) modificationValue = u'edited value' dataSets = [ {self.parameter._IDENTIFIER_KEY: creationIdentifier, key: creationValue}, {self.parameter._IDENTIFIER_KEY: modificationIdentifier, key: modificationValue}] edits = list(self.parameter._extractEdits(dataSets)) self.assertEqual( edits, [(modificationIdentifier, {key: modificationValue})]) def test_coerceAll(self): """ L{RepeatableFormParameter._coerceAll} should take a list of two-tuples and return a L{Deferred} which is called back with a list of tuples where the first element of each tuple is the first element of a tuple from the input and the second element of each tuple is the result of the L{Deferred} returned by a call to L{RepeatableFormParameter._coerceSingleRepetition} with the second element of the same tuple from the input. The ordering of the input and output lists should be the same. """ firstInput = object() firstValue = u'1' secondInput = object() secondValue = u'2' inputs = [(firstInput, {u'foo': firstValue}), (secondInput, {u'foo': secondValue})] coerceDeferred = self.parameter._coerceAll(inputs) coerceDeferred.addCallback( self.assertEqual, [(firstInput, {u'foo': int(firstValue)}), (secondInput, {u'foo': int(secondValue)})]) return coerceDeferred def test_coercion(self): """ L{ListChangeParameter._coerceSingleRepetition} should call the appropriate coercers from the repeatable form's parameters. """ d = self.parameter._coerceSingleRepetition({u'foo': [u'-56']}) d.addCallback(self.assertEqual, {u'foo': -56}) return d def test_coercerCreate(self): """ L{ListChangeParameter.coercer} should be able to figure out that a repetition is new if it is associated with an identifier generated by C{asLiveForm}. """ parameter = ListChangeParameter( u'repeatableFoo', self.innerParameters, defaults=[], modelObjects=[]) # get an id allocated to us liveFormWrapper = parameter.asLiveForm() coerceDeferred = parameter.coercer( [{u'foo': [u'-56'], parameter._IDENTIFIER_KEY: liveFormWrapper.identifier}]) def cbCoerced(submission): self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, []) self.assertEqual(len(submission.create), 1) self.assertEqual(submission.create[0].values, {u'foo': -56}) CREATED_OBJECT = object() submission.create[0].setter(CREATED_OBJECT) self.assertIdentical( parameter._objectFromID(liveFormWrapper.identifier), CREATED_OBJECT) coerceDeferred.addCallback(cbCoerced) return coerceDeferred def test_coercerCreateNoChange(self): """ L{ListChangeParameter.coercer} should be able to figure out when nothing has been done to a set of values created by a previous submission. """ parameter = ListChangeParameter( u'repeatableFoo', self.innerParameters, defaults=[], modelObjects=[]) # get an id allocated to us liveFormWrapper = parameter.asLiveForm() identifier = liveFormWrapper.identifier value = {u'foo': [u'-56'], parameter._IDENTIFIER_KEY: identifier} coerceDeferred = parameter.coercer([value.copy()]) def cbFirstSubmit(firstSubmission): firstSubmission.create[0].setter(None) # Resubmit the same thing return parameter.coercer([value.copy()]) def cbSecondSubmit(secondSubmission): self.assertEqual(secondSubmission.create, []) self.assertEqual(secondSubmission.edit, []) self.assertEqual(secondSubmission.delete, []) coerceDeferred.addCallback(cbFirstSubmit) coerceDeferred.addCallback(cbSecondSubmit) return coerceDeferred def test_coercerEdit(self): """ L{ListChangeParameter.coercer} should be able to figure out that a repetition is an edit if its identifier corresponds to an entry in the list of defaults. """ (identifier,) = self.parameter._idsToObjects.keys() editDeferred = self.parameter.coercer( [{u'foo': [u'-57'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbEdit(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, [EditObject(self.defaultObject, {u'foo': -57})]) self.assertEqual(submission.delete, []) editDeferred.addCallback(cbEdit) return editDeferred def test_repeatedCoercerEdit(self): """ L{ListChangeParameter.coercer} should work correctly with respect to repeated edits. """ (identifier,) = self.parameter._idsToObjects.keys() editDeferred = self.parameter.coercer( [{u'foo': [u'-57'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbEdited(ignored): # edit it back to the initial value return self.parameter.coercer( [{u'foo': [u'-56'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbRestored(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, [EditObject(self.defaultObject, {u'foo': -56})]) self.assertEqual(submission.delete, []) editDeferred.addCallback(cbEdited) editDeferred.addCallback(cbRestored) return editDeferred def test_coercerNoChange(self): """ L{ListChangeParameter.coercer} shouldn't include a repetition anywhere in its result if it corresponds to a default and wasn't edited. """ (identifier,) = self.parameter._idsToObjects.keys() unchangedDeferred = self.parameter.coercer( [{u'foo': [u'-56'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbUnchanged(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, []) unchangedDeferred.addCallback(cbUnchanged) return unchangedDeferred def test_repeatedCoercerNoChange(self): """ Same as L{test_coercerNoChange}, but with multiple submissions that don't change anything. """ (identifier,) = self.parameter._idsToObjects.keys() editDeferred = self.parameter.coercer( [{u'foo': [u'-56'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbEdited(ignored): # Same values - no edit occurs. return self.parameter.coercer( [{u'foo': [u'-56'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbUnedited(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, []) editDeferred.addCallback(cbEdited) editDeferred.addCallback(cbUnedited) return editDeferred def test_coercerDelete(self): """ L{ListChangeParameter.coercer} should be able to figure out that a default was deleted if it doesn't get a repetition with a corresponding identifier. """ deleteDeferred = self.parameter.coercer([]) def cbDeleted(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, [self.defaultObject]) deleteDeferred.addCallback(cbDeleted) return deleteDeferred def test_repeatedCoercerDelete(self): """ L{ListChangeParameter.coercer} should only report a deletion the first time that it doesn't see a particular value. """ deleteDeferred = self.parameter.coercer([]) def cbDeleted(ignored): return self.parameter.coercer([]) def cbUnchanged(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, []) deleteDeferred.addCallback(cbDeleted) deleteDeferred.addCallback(cbUnchanged) return deleteDeferred def test_coercerDeleteUnsubmitted(self): """ L{ListChangeParameter.coercer} should not report as deleted an internal marker objects when a form is repeated but the repetition is omitted from the submission. """ (identifier,) = self.parameter._idsToObjects.keys() # Creates some new state inside the parameter (yea, ick, state). repetition = self.parameter.asLiveForm() unchangedDeferred = self.parameter.coercer([ {u'foo': [u'-56'], self.parameter._IDENTIFIER_KEY: identifier}]) def cbUnchanged(submission): self.assertEqual(submission.create, []) self.assertEqual(submission.edit, []) self.assertEqual(submission.delete, []) unchangedDeferred.addCallback(cbUnchanged) return unchangedDeferred def test_makeDefaultLiveForm(self): """ L{ListChangeParameter._makeDefaultLiveForm} should make a live form that has been correctly wrapped and initialized. """ liveFormWrapper = self.parameter._makeDefaultLiveForm( (self.parameter.defaults[0], 1234)) self.failUnless(isinstance( liveFormWrapper, self.parameter.repeatedLiveFormWrapper)) liveForm = liveFormWrapper.liveForm self.failUnless(isinstance(liveForm, LiveForm)) self.assertEqual( len(liveForm.parameters), len(self.innerParameters)) for parameter in liveForm.parameters: self.assertEqual(parameter.default, self.defaultValues[parameter.name]) def test_makeADefaultLiveFormChoiceParameter(self): """ Verify that the parameter-defaulting done by L{ListChangeParameter._makeDefaultLiveForm} works for L{ChoiceParameter} instances. """ param = ListChangeParameter( u'', [ChoiceParameter( u'choice', [Option(u'opt 1', u'1', True), Option(u'opt 2', u'2', False)], u'label!', u'description!', multiple=True)]) liveFormWrapper = param._makeDefaultLiveForm( ({u'choice': [u'1', u'2']}, 1234)) liveForm = liveFormWrapper.liveForm clonedParams = liveForm.parameters self.assertEqual(len(clonedParams), 1) clonedChoiceParam = clonedParams[0] self.assertTrue( isinstance(clonedChoiceParam, ChoiceParameter)) self.assertEqual(clonedChoiceParam.name, u'choice') self.assertEqual(clonedChoiceParam.label, u'label!') self.assertEqual( clonedChoiceParam.description, u'description!') self.assertTrue(clonedChoiceParam.multiple) self.assertEqual(len(clonedChoiceParam.choices), 2) (c1, c2) = clonedChoiceParam.choices self.assertEqual(c1.description, u'opt 1') self.assertEqual(c1.value, u'1') self.assertEqual(c1.selected, True) self.assertEqual(c2.description, u'opt 2') self.assertEqual(c2.value, u'2') self.assertEqual(c2.selected, True) def test_asLiveFormIdentifier(self): """ L{ListChangeParameter.asLiveForm} should allocate an identifier for the new liveform, pass it to the liveform wrapper and put the placeholder value L{ListChangeParameter._NO_OBJECT_MARKER} into the object mapping. """ liveFormWrapper = self.parameter.asLiveForm() self.assertIn(liveFormWrapper.identifier, self.parameter._idsToObjects) self.assertIdentical( self.parameter._objectFromID(liveFormWrapper.identifier), self.parameter._NO_OBJECT_MARKER) def test_correctIdentifiersFromGetInitialLiveForms(self): """ L{ListChangeParameter.getInitialLiveForms} should return a list of L{RepeatedLiveFormWrapper} instances with C{identifier} attributes which correspond to the identifiers associated with corresponding model objects in the L{ListChangeParameter}. """ # XXX This should really have more than one model object to make sure # ordering is tested properly. forms = self.parameter.getInitialLiveForms() self.assertEqual(len(forms), 1) self.assertIdentical( self.parameter._objectFromID(forms[0].identifier), self.defaultObject) def test_fromInputs(self): """ L{RepeatableFormParameter.fromInputs} should call the appropriate coercers from the repeatable form's parameters. """ (modifyIdentifier,) = self.parameter._idsToObjects.keys() # Make a new object to be deleted deleteObject = object() deleteIdentifier = self.parameter._idForObject(deleteObject) createIdentifier = self.parameter._newIdentifier() result = self.parameter.fromInputs({ self.parameter.name: [[ {self.parameter._IDENTIFIER_KEY: modifyIdentifier, u'foo': [u'-57']}, {self.parameter._IDENTIFIER_KEY: createIdentifier, u'foo': [u'18']}]]}) def cbCoerced(changes): self.assertEqual(len(changes.create), 1) self.assertEqual(changes.create[0].values, {u'foo': 18}) self.assertEqual(len(changes.edit), 1) self.assertIdentical(changes.edit[0].object, self.defaultObject) self.assertEqual(changes.edit[0].values, {u'foo': -57}) self.assertEqual(changes.delete, [deleteObject]) result.addCallback(cbCoerced) return result class RepeatedLiveFormWrapperTestCase(TestCase): """ Tests for L{RepeatedLiveFormWrapper}. """ def test_removeLinkRenderer(self): """ Verify that the I{removeLink} renderer of L{RepeatedLiveFormWrapper} only returns the tag if the C{removable} argument passed to the constructor is C{True}. """ fragment = RepeatedLiveFormWrapper(None, None, removable=True) removeLinkRenderer = renderer.get(fragment, 'removeLink', None) tag = div() self.assertIdentical(removeLinkRenderer(None, tag), tag) fragment.removable = False self.assertEqual(removeLinkRenderer(None, tag), '') PK9FNLL"xmantissa/test/test_mantissacmd.py# Copyright (c) 2008 Divmod. See LICENSE for details. """ Tests for I{axiomatic mantissa} and other functionality provided by L{axiom.plugins.mantissacmd}. """ from twisted.trial.unittest import TestCase from twisted.python.filepath import FilePath from twisted.internet.ssl import Certificate from axiom.store import Store from axiom.plugins.mantissacmd import genSerial, Mantissa from axiom.test.util import CommandStubMixin from xmantissa.ixmantissa import IOfferingTechnician from xmantissa.port import TCPPort, SSLPort from xmantissa.web import SiteConfiguration from xmantissa.terminal import SecureShellConfiguration from xmantissa.plugins.baseoff import baseOffering class MiscTestCase(TestCase): def test_genSerial(self): """ Test that L{genSerial} returns valid unique serials. """ s1 = genSerial() self.assertTrue(isinstance(s1, int), '%r must be an int' % (s1,)) self.assertTrue(s1 >= 0, '%r must be positive' % (s1,)) s2 = genSerial() self.assertNotEqual(s1, s2) class CertificateTestCase(CommandStubMixin, TestCase): """ Tests for the certificate generated by L{Mantissa}. """ def _getCert(self): """ Get the SSL certificate from an Axiom store directory. """ certFile = FilePath(self.dbdir).child('files').child('server.pem') return Certificate.loadPEM(certFile.open('rb').read()) def test_uniqueSerial(self): """ Test that 'axiomatic mantissa' generates SSL certificates with a different unique serial on each invocation. """ m = Mantissa() m.parent = self self.dbdir = self.mktemp() self.store = Store(self.dbdir) m.parseOptions(['--admin-password', 'foo']) cert1 = self._getCert() self.dbdir = self.mktemp() self.store = Store(self.dbdir) m.parseOptions(['--admin-password', 'foo']) cert2 = self._getCert() self.assertNotEqual(cert1.serialNumber(), cert2.serialNumber()) def test_commonName(self): """ C{axiomatic mantissa} generates an SSL certificate with the domain part of the admin username as its common name. """ m = Mantissa() m.parent = self self.dbdir = self.mktemp() self.store = Store(filesdir=FilePath(self.dbdir).child("files").path) m.parseOptions(['--admin-user', 'admin@example.com', '--admin-password', 'foo']) cert = self._getCert() self.assertEqual(cert.getSubject().commonName, "example.com") class MantissaCommandTests(TestCase, CommandStubMixin): """ Tests for L{Mantissa}. """ def setUp(self): """ Create a store to use in tests. """ self.filesdir = self.mktemp() self.siteStore = Store(filesdir=self.filesdir) def test_baseOffering(self): """ L{Mantissa.installSite} installs the Mantissa base offering. """ options = Mantissa() options.installSite(self.siteStore, u"example.com", u"", False) self.assertEqual( IOfferingTechnician(self.siteStore).getInstalledOfferingNames(), [baseOffering.name]) def test_httpPorts(self): """ L{Mantissa.installSite} creates a TCP port and an SSL port for the L{SiteConfiguration} which comes with the base offering it installs. """ options = Mantissa() options.installSite(self.siteStore, u"example.com", u"", False) site = self.siteStore.findUnique(SiteConfiguration) tcps = list(self.siteStore.query(TCPPort, TCPPort.factory == site)) ssls = list(self.siteStore.query(SSLPort, SSLPort.factory == site)) self.assertEqual(len(tcps), 1) self.assertEqual(tcps[0].portNumber, 8080) self.assertEqual(len(ssls), 1) self.assertEqual(ssls[0].portNumber, 8443) self.assertNotEqual(ssls[0].certificatePath, None) def test_hostname(self): """ L{Mantissa.installSite} sets the C{hostname} of the L{SiteConfiguration} to the domain name it is called with. """ options = Mantissa() options.installSite(self.siteStore, u"example.net", u"", False) site = self.siteStore.findUnique(SiteConfiguration) self.assertEqual(site.hostname, u"example.net") def test_sshPorts(self): """ L{Mantissa.installSite} creates a TCP port for the L{SecureShellConfiguration} which comes with the base offering it installs. """ options = Mantissa() options.installSite(self.siteStore, u"example.com", u"", False) shell = self.siteStore.findUnique(SecureShellConfiguration) tcps = list(self.siteStore.query(TCPPort, TCPPort.factory == shell)) self.assertEqual(len(tcps), 1) self.assertEqual(tcps[0].portNumber, 8022) PK9F"ˌs11xmantissa/test/test_offering.py""" Tests for xmantissa.offering. """ from zope.interface import Interface, implements from zope.interface.verify import verifyClass, verifyObject from twisted.trial import unittest from axiom.store import Store from axiom import item, attributes, userbase from axiom.plugins.mantissacmd import Mantissa from axiom.dependency import installedOn from xmantissa import ixmantissa, offering from xmantissa.web import SiteConfiguration from xmantissa.ampserver import AMPConfiguration from xmantissa.plugins.baseoff import baseOffering, ampOffering from xmantissa.plugins.offerings import peopleOffering class TestSiteRequirement(item.Item): typeName = 'test_site_requirement' schemaVersion = 1 attr = attributes.integer() class TestAppPowerup(item.Item): typeName = 'test_app_powerup' schemaVersion = 1 attr = attributes.integer() class ITestInterface(Interface): """ An interface to which no object can be adapted. Used to ensure failed adaption causes a powerup to be installed. """ class OfferingPluginTest(unittest.TestCase): """ A simple test for getOffering. """ def test_getOfferings(self): """ getOffering should use the Twisted plugin system to load the plugins provided with Mantissa. Since this is dynamic, we can't assert anything about the complete list, but we can at least verify that all the plugins that should be there, are. """ foundOfferings = list(offering.getOfferings()) allExpectedOfferings = [baseOffering, ampOffering, peopleOffering] for expected in allExpectedOfferings: self.assertIn(expected, foundOfferings) class OfferingTest(unittest.TestCase): def setUp(self): self.store = Store(filesdir=self.mktemp()) Mantissa().installSite(self.store, u"localhost", u"", False) Mantissa().installAdmin(self.store, u'admin', u'localhost', u'asdf') self.userbase = self.store.findUnique(userbase.LoginSystem) self.adminAccount = self.userbase.accountByAddress( u'admin', u'localhost') off = offering.Offering( name=u'test_offering', description=u'This is an offering which tests the offering ' 'installation mechanism', siteRequirements=[(ITestInterface, TestSiteRequirement)], appPowerups=[TestAppPowerup], installablePowerups=[], loginInterfaces=[], themes=[], ) self.offering = off # Add this somewhere that the plugin system is going to see it. self._originalGetOfferings = offering.getOfferings offering.getOfferings = self.fakeGetOfferings def fakeGetOfferings(self): """ Return standard list of offerings, plus one extra. """ return list(self._originalGetOfferings()) + [self.offering] def tearDown(self): """ Remove the temporary offering. """ offering.getOfferings = self._originalGetOfferings def test_installOffering(self): """ L{OfferingConfiguration.installOffering} should install the given offering on the Mantissa server. """ conf = self.adminAccount.avatars.open().findUnique( offering.OfferingConfiguration) io = conf.installOffering(self.offering, None) # InstalledOffering should be returned, and installed on the site store foundIO = self.store.findUnique(offering.InstalledOffering, offering.InstalledOffering.offeringName == self.offering.name) self.assertIdentical(io, foundIO) # Site store requirements should be on the site store tsr = self.store.findUnique(TestSiteRequirement) self.failUnless(installedOn(tsr), self.store) # App store should have been created appStore = self.userbase.accountByAddress(self.offering.name, None) self.assertNotEqual(appStore, None) # App store requirements should be on the app store ss = appStore.avatars.open() tap = ss.findUnique(TestAppPowerup) self.failUnless(installedOn(tap), ss) self.assertRaises(offering.OfferingAlreadyInstalled, conf.installOffering, self.offering, None) def test_getInstalledOfferingNames(self): """ L{getInstalledOfferingNames} should list the names of offerings installed on the given site store. """ self.assertEquals(offering.getInstalledOfferingNames(self.store), ['mantissa-base']) self.test_installOffering() installed = offering.getInstalledOfferingNames(self.store) installed.sort() expected = [u"mantissa-base", u"test_offering"] expected.sort() self.assertEquals(installed, expected) def test_getInstalledOfferings(self): """ getInstalledOfferings should return a mapping of offering name to L{Offering} object for each installed offering on a given site store. """ self.assertEquals(offering.getInstalledOfferings(self.store), {baseOffering.name: baseOffering}) self.test_installOffering() self.assertEquals(offering.getInstalledOfferings(self.store), {baseOffering.name: baseOffering, self.offering.name: self.offering}) def test_isAppStore(self): """ isAppStore returns True for stores with offerings installed on them, False otherwise. """ conf = self.adminAccount.avatars.open().findUnique( offering.OfferingConfiguration) conf.installOffering(self.offering, None) app = self.userbase.accountByAddress(self.offering.name, None) self.failUnless(offering.isAppStore(app.avatars.open())) self.failIf(offering.isAppStore(self.adminAccount.avatars.open())) class FakeOfferingTechnician(object): """ In-memory only implementation of the offering inspection/installation API. @ivar installedOfferings: A mapping from offering names to corresponding L{IOffering} providers which have been passed to C{installOffering}. """ implements(ixmantissa.IOfferingTechnician) def __init__(self): self.installedOfferings = {} def installOffering(self, offering): """ Add the given L{IOffering} provider to the list of installed offerings. """ self.installedOfferings[offering.name] = offering def getInstalledOfferings(self): """ Return a copy of the internal installed offerings mapping. """ return self.installedOfferings.copy() def getInstalledOfferingNames(self): """ Return the names from the internal installed offerings mapping. """ return self.installedOfferings.keys() class OfferingTechnicianTestMixin: """ L{unittest.TestCase} mixin which defines unit tests for classes which implement L{IOfferingTechnician}. @ivar offerings: A C{list} of L{Offering} instances which will be installed by the tests this mixin defines. """ offerings = [ offering.Offering(u'an offering', None, [], [], [], [], []), offering.Offering(u'another offering', None, [], [], [], [], [])] def createTechnician(self): """ @return: An L{IOfferingTechnician} provider which will be tested. """ raise NotImplementedError( "%r did not implement createTechnician" % (self.__class__,)) def test_interface(self): """ L{createTechnician} returns an instance of a type which declares that it implements L{IOfferingTechnician} and has all of the methods and attributes defined by the interface. """ technician = self.createTechnician() technicianType = type(technician) self.assertTrue( ixmantissa.IOfferingTechnician.implementedBy(technicianType)) self.assertTrue( verifyClass(ixmantissa.IOfferingTechnician, technicianType)) self.assertTrue( verifyObject(ixmantissa.IOfferingTechnician, technician)) def test_getInstalledOfferingNames(self): """ The L{ixmantissa.IOfferingTechnician.getInstalledOfferingNames} implementation returns a C{list} of C{unicode} strings, each element giving the name of an offering which has been installed. """ offer = self.createTechnician() self.assertEqual(offer.getInstalledOfferingNames(), []) expected = [] for dummyOffering in self.offerings: offer.installOffering(dummyOffering) expected.append(dummyOffering.name) expected.sort() installed = offer.getInstalledOfferingNames() installed.sort() self.assertEqual(installed, expected) def test_getInstalledOfferings(self): """ The L{ixmantissa.IOfferingTechnician.getInstalledOfferings} implementation returns a C{dict} mapping C{unicode} offering names to the corresponding L{IOffering} providers. """ offer = self.createTechnician() self.assertEqual(offer.getInstalledOfferings(), {}) expected = {} for dummyOffering in self.offerings: offer.installOffering(dummyOffering) expected[dummyOffering.name] = dummyOffering self.assertEqual(offer.getInstalledOfferings(), expected) class OfferingAdapterTests(unittest.TestCase, OfferingTechnicianTestMixin): """ Tests for L{offering.OfferingAdapter}. """ def setUp(self): """ Hook offering plugin discovery so that only the fake offerings the test wants exist. """ self.origGetOfferings = offering.getOfferings offering.getOfferings = self.getOfferings def tearDown(self): """ Restore the original L{getOfferings} function. """ offering.getOfferings = self.origGetOfferings def getOfferings(self): """ Return some dummy offerings, as defined by C{self.offerings}. """ return self.offerings def createTechnician(self): """ Create an L{offering.OfferingAdapter}. """ store = Store() technician = offering.OfferingAdapter(store) return technician class FakeOfferingTechnicianTests(unittest.TestCase, OfferingTechnicianTestMixin): """ Tests (ie, verification) for L{FakeOfferingTechnician}. """ def createTechnician(self): """ Create a L{FakeOfferingTechnician}. """ return FakeOfferingTechnician() class BaseOfferingTests(unittest.TestCase): """ Tests for the base Mantissa offering, L{xmantissa.plugins.baseoff.baseOffering}. """ def test_interface(self): """ C{baseOffering} provides L{IOffering}. """ self.assertTrue(verifyObject(ixmantissa.IOffering, baseOffering)) def test_staticContentPath(self): """ C{baseOffering.staticContentPath} gives the location of a directory which has I{mantissa.css} in it. """ self.assertTrue( baseOffering.staticContentPath.child('mantissa.css').exists()) def _siteRequirementTest(self, offering, cls): """ Verify that installing C{offering} results in an instance of the given Item subclass being installed as a powerup for IProtocolFactoryFactory. """ store = Store() ixmantissa.IOfferingTechnician(store).installOffering(offering) factories = list(store.powerupsFor(ixmantissa.IProtocolFactoryFactory)) for factory in factories: if isinstance(factory, cls): break else: self.fail("No instance of %r in %r" % (cls, factories)) def test_siteConfiguration(self): """ L{SiteConfiguration} powers up a store for L{IProtocolFactoryFactory} when L{baseOffering} is installed on that store. """ self._siteRequirementTest(baseOffering, SiteConfiguration) def test_ampConfiguration(self): """ L{AMPConfiguration} powers up a store for L{IProtocolFactoryFactory} when L{ampOffering} is installed on that store. """ self._siteRequirementTest(ampOffering, AMPConfiguration) PK9Fc-c-%xmantissa/test/test_password_reset.pyimport email from twisted.trial.unittest import TestCase from nevow.url import URL from nevow.flat import flatten from nevow.inevow import IResource from nevow.testutil import FakeRequest, renderPage from axiom.store import Store from axiom import userbase from axiom.dependency import installOn from axiom.plugins.mantissacmd import Mantissa from xmantissa import ixmantissa from xmantissa.web import SiteConfiguration from xmantissa.webapp import PrivateApplication from xmantissa.signup import PasswordResetResource, _PasswordResetAttempt class PasswordResetTestCase(TestCase): def setUp(self): """ Set up a fake objects and methods for the password reset tests. """ siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(siteStore, u"divmod.com", u"", False) self.loginSystem = siteStore.findUnique(userbase.LoginSystem) self.site = siteStore.findUnique(SiteConfiguration) la = self.loginSystem.addAccount( u'joe', u'divmod.com', u'secret', internal=True) la.addLoginMethod( u'joe', u'external.com', protocol=u'zombie dance', verified=False, internal=False) # create an account with no external mail address account = self.loginSystem.addAccount( u'jill', u'divmod.com', u'secret', internal=True) account.addLoginMethod( u'jill', u'divmod.com', protocol=u'zombie dance', verified=True, internal=True) self.siteStore = siteStore # Set up the user store to have all the elements necessary to redirect # in the case where the user is already logged in. substore = la.avatars.open() installOn(PrivateApplication(store=substore), substore) self.userStore = substore self.loginAccount = la self.nonExternalAccount = account self.reset = PasswordResetResource(self.siteStore) def test_reset(self): """ Test a password reset, as it might happen for a user """ self.reset.resetPassword( self.reset.newAttemptForUser(u'joe@divmod.com'), u'more secret') self.assertEqual(self.loginAccount.password, u'more secret') self.assertEqual( self.siteStore.query(_PasswordResetAttempt).count(), 0) def test_attemptByKey(self): """ Test that L{xmantissa.signup.PasswordResetResource.attemptByKey} knows the difference between valid and invalid keys """ self.failUnless(self.reset.attemptByKey( self.reset.newAttemptForUser(u'joe@divmod.com').key)) self.failIf(self.reset.attemptByKey(u'not really a key')) def test_accountByAddress(self): """ Test that L{xmantissa.signup.PasswordResetResource.accountByAddress} behaves similarly to L{axiom.userbase.LoginSystem.accountByAddress} """ self.assertEqual( self.reset.accountByAddress(u'joe@divmod.com'), self.loginSystem.accountByAddress(u'joe', u'divmod.com')) def test_handleRequest(self): """ Check that handling a password reset request for a user sends email appropriately. """ def myFunc(url, attempt, email): myFunc.emailsSent += 1 myFunc.url = url myFunc.attempt = attempt myFunc.email = email url = 'http://oh.no/reset.html' myFunc.emailsSent = 0 self.reset.sendEmail=myFunc # positive case. User exists. Email should be sent self.reset.handleRequestForUser(u'joe@divmod.com', url) self.assertEquals(myFunc.emailsSent, 1) self.assertEquals(myFunc.url, 'http://oh.no/reset.html') self.assertEquals(myFunc.attempt.username, u'joe@divmod.com') self.assertEquals(myFunc.email, 'joe@external.com') # Negative case. User does not exist. Email should not be sent self.reset.handleRequestForUser(u'no_joe@divmod.com', url) self.assertEquals(myFunc.emailsSent, 1) # Negative case. User exists, but has no external mail. Email should not # be sent. self.reset.handleRequestForUser(u'jill@divmod.com', url) self.assertEquals(myFunc.emailsSent, 1) def test_sendEmail(self): """ L{PasswordResetResource.sendEmail} should format a meaningful password reset email. """ resetAddress = 'reset@example.org' resetURI = URL.fromString('http://example.org/resetPassword') userAddress = 'joe@divmod.com' resetAttempt = self.reset.newAttemptForUser(userAddress.decode('ascii')) _sentEmail = [] self.reset.sendEmail(resetURI, resetAttempt, userAddress, _sendEmail=lambda *args: _sentEmail.append(args)) self.assertEquals(len(_sentEmail), 1) [(sentFrom, sentTo, sentText)] = _sentEmail self.assertEquals(sentFrom, resetAddress) self.assertEquals(sentTo, userAddress) msg = email.message_from_string(sentText) [headerFrom] = msg.get_all('from') [headerTo] = msg.get_all('to') [headerDate] = msg.get_all('date') # Python < 2.5 compatibility try: from email import utils except ImportError: from email import Utils as utils self.assertEquals(utils.parseaddr(headerFrom)[1], resetAddress) self.assertEquals(utils.parseaddr(headerTo)[1], userAddress) self.assertTrue(utils.parsedate_tz(headerDate) is not None, '%r is not a RFC 2822 date' % headerDate) self.assertTrue(not msg.is_multipart()) self.assertIn(flatten(resetURI.child(resetAttempt.key)), msg.get_payload()) def test_redirectToSettingsWhenLoggedIn(self): """ When a user is already logged in, navigating to /resetPassword should redirect to the settings page, since the user can change their password from there. """ self.assertNotIdentical(self.userStore.parent, None) # sanity check prefPage = ixmantissa.IPreferenceAggregator(self.userStore) urlPath = ixmantissa.IWebTranslator(self.userStore).linkTo(prefPage.storeID) request = FakeRequest(headers={"host": "example.com"}) app = IResource(self.userStore) rsc = IResource(app.locateChild(request, ("resetPassword",))[0]) d = renderPage(rsc, reqFactory=lambda : request) def rendered(result): self.assertEquals( 'http://example.com' + urlPath, request.redirected_to) d.addCallback(rendered) return d def test_getExternalEmail(self): """ Test that we can accurately retrieve an external email address from an attempt. """ email = self.reset.getExternalEmail(self.loginAccount) self.assertEquals(email, 'joe@external.com') def test_noExternalEmail(self): """ Test that C{getExternalEmail} returns C{None} if there is no external email address for that account. """ email = self.reset.getExternalEmail(self.nonExternalAccount) self.assertEquals(email, None) def test_nothingSpecified(self): """ Submitting an empty form should redirect the user back to the form. """ self.reset.handleRequestForUser = lambda *args: self.fail(args) _request = FakeRequest( headers={'host': 'example.org'}, uri='/resetPassword', currentSegments=['resetPassword'], args={'username': [''], 'email': ['']}) _request.method = 'POST' d = renderPage(self.reset, reqFactory=lambda: _request) def rendered(_): self.assertEquals(_request.redirected_to, 'http://example.org/resetPassword') d.addCallback(rendered) return d def test_onlyUsernameSpecified(self): """ Test that if the user only supplies the local part of their username then the password resetter will still find the correct user. """ hostname = self.site.hostname.encode('ascii') def handleRequest(username, url): handleRequest.attempt = self.reset.newAttemptForUser(username) handleRequest.username = username class Request(FakeRequest): method = 'POST' def __init__(self, *a, **kw): FakeRequest.__init__(self, *a, **kw) self.args = {'username': ['joe'], 'email': ['']} self.received_headers['host'] = hostname self.reset.handleRequestForUser = handleRequest d = renderPage(self.reset, reqFactory=Request) d.addCallback(lambda _: self.assertEquals(handleRequest.username, 'joe@' + hostname)) return d def test_emailAddressSpecified(self): """ If an email address and no username is specified, then the password resetter should still find the correct user. """ requests = [] def handleRequest(username, url): requests.append((username, url)) class Request(FakeRequest): method = 'POST' def __init__(self, *a, **k): FakeRequest.__init__(self, *a, **k) self.args = {'username': [''], 'email': ['joe@external.com']} self.reset.handleRequestForUser = handleRequest d = renderPage(self.reset, reqFactory=Request) def completedRequest(): self.assertEqual(len(requests), 1) self.assertEqual(requests[0][0], 'joe@divmod.com') d.addCallback(lambda ign: completedRequest()) return d def specifyBogusEmail(self, bogusEmail): """ If an email address (which is not present in the system) and no username is specified, then the password reset should still ask the user to check their email. No distinction is provided to discourage "oafish attempts at hacking", as duncan poetically put it. """ requests = [] def handleRequest(username, url): requests.append((username, url)) class Request(FakeRequest): method = 'POST' def __init__(self, *a, **k): FakeRequest.__init__(self, *a, **k) self.args = {'username': [''], 'email': [bogusEmail]} self.reset.handleRequestForUser = handleRequest d = renderPage(self.reset, reqFactory=Request) def completedRequest(): self.assertEqual(len(requests), 0) d.addCallback(lambda ign: completedRequest()) return d def test_notPresentEmailAddress(self): """ If an email address is not present in the system, no notification should be sent, but the user should receive the same feedback as if it worked, to discourage cracking attempts. """ return self.specifyBogusEmail('not-in-the-system@example.com') def test_malformedEmailAddress(self): """ If a malformed email address is provided, no notification should be sent, but the user should receive the same feedback as if it worked, to discourage cracking attempts. """ return self.specifyBogusEmail('hello, world!') PK9F1ܫ  xmantissa/test/test_prefs.pyfrom zope.interface import implements from twisted.trial.unittest import TestCase from axiom.item import Item from axiom.store import Store from axiom import attributes from axiom.dependency import installOn from xmantissa import prefs, ixmantissa, liveform class WidgetShopPrefCollection(Item, prefs.PreferenceCollectionMixin): """ Basic L{xmantissa.ixmantissa.IPreferenceCollection}, with a single preference, C{preferredWidget} """ implements(ixmantissa.IPreferenceCollection) installedOn = attributes.reference() preferredWidget = attributes.text() powerupInterfaces = (ixmantissa.IPreferenceCollection,) def getPreferenceParameters(self): return (liveform.Parameter('preferredWidget', liveform.TEXT_INPUT, unicode, 'Preferred Widget'),) class PreferencesTestCase(TestCase): """ Test case for basic preference functionality """ def testAggregation(self): """ Assert that L{xmantissa.prefs.PreferenceAggregator} gives us the right values for the preference attributes on L{WidgetShopPrefCollection} """ store = Store() agg = prefs.PreferenceAggregator(store=store) installOn(agg, store) coll = WidgetShopPrefCollection(store=store) installOn(coll, store) coll.preferredWidget = u'Foo' self.assertEqual(agg.getPreferenceValue('preferredWidget'), 'Foo') coll.preferredWidget = u'Bar' self.assertEqual(agg.getPreferenceValue('preferredWidget'), 'Bar') def testGetPreferences(self): """ Test that L{prefs.PreferenceCollectionMixin.getPreferences} works """ class TrivialPreferenceCollection(prefs.PreferenceCollectionMixin): foo = 'bar' def getPreferenceParameters(self): return (liveform.Parameter('foo', liveform.TEXT_INPUT, str),) self.assertEqual(TrivialPreferenceCollection().getPreferences(), {'foo': 'bar'}) def testGetPreferencesNone(self): """ Test that L{prefs.PreferenceCollectionMixin.getPreferences} does the right thing when the preference collection returns None from C{getPreferenceParameters} """ class TrivialPreferenceCollection(prefs.PreferenceCollectionMixin): def getPreferenceParameters(self): return None self.assertEqual(TrivialPreferenceCollection().getPreferences(), {}) PK9FAνsxmantissa/test/test_product.pyfrom zope.interface import implements, Interface from twisted.trial.unittest import TestCase from twisted.python.reflect import qual from axiom.store import Store from axiom.item import Item from axiom.attributes import integer from xmantissa.product import Installation, ProductConfiguration, Product, ProductFragment class IFoo(Interface): pass class Foo(Item): implements(IFoo) powerupInterfaces = (IFoo,) attr = integer() class IBaz(Interface): pass class Baz(Item): implements(IBaz) powerupInterfaces = (IBaz,) attr = integer() class ProductTest(TestCase): def setUp(self): """ Create a pseudo site store and a pseudo user store in it. """ self.siteStore = Store() self.userStore = Store() self.userStore.parent = self.siteStore def test_product(self): self.product = Product(store=self.siteStore) self.product.types = [ n.decode('ascii') for n in [qual(Foo), qual(Baz)]] self.product.installProductOn(self.userStore) i = self.userStore.findUnique(Installation) self.assertEqual(i.types, self.product.types) def test_createProduct(self): """ Verify that L{ProductConfiguration.createProduct} creates a correctly configured L{Product} and returns it. """ conf = ProductConfiguration(store=self.userStore) product = conf.createProduct([Foo, Baz]) self.assertEqual(product.types, [qual(Foo), qual(Baz)]) class InstallationTest(TestCase): def setUp(self): self.s = Store() self.product = Product() self.product.types = [n.decode('ascii') for n in [qual(Foo), qual(Baz)]] self.product.installProductOn(self.s) self.i = self.s.findUnique(Installation) def test_install(self): """ Ensure that Installation installs instances of the types it is created with. """ self.assertNotEqual(IFoo(self.s, None), None) self.assertNotEqual(IBaz(self.s, None), None) self.assertEqual(list(self.i.items), [self.s.findUnique(t) for t in [Foo, Baz]]) def test_uninstall(self): """ Ensure that Installation properly uninstalls all of the items it controls. """ self.product.removeProductFrom(self.s) self.assertEqual(IFoo(self.s, None), None) self.assertEqual(IBaz(self.s, None), None) self.assertEqual(list(self.s.query(Installation)), []) class StubProductConfiguration(object): """ Stub implementation of L{ProductConfiguration} for testing purposes. @ivar createdProducts: A list containing all powerups lists passed to C{createProduct}. """ def __init__(self, createdProducts): self.createdProducts = createdProducts def createProduct(self, powerups): self.createdProducts.append(powerups) class ViewTests(TestCase): """ Tests for L{ProductFragment}. """ def test_coerceProduct(self): """ Verify that L{ProductFragment.coerceProduct} calls C{createProduct} on the object it wraps and passes its arguments through. """ createdProducts = [] fragment = ProductFragment(StubProductConfiguration(createdProducts)) fragment.coerceProduct(foo=u'bar', baz=u'quux') self.assertEqual(createdProducts, [[u'bar', u'quux']]) def test_coerceProductReturn(self): """ Verify that L{ProductFragment.coerceProduct} returns a string indicating success. """ createdProducts = [] fragment = ProductFragment(StubProductConfiguration(createdProducts)) result = fragment.coerceProduct() self.assertEqual(result, u'Created.') PK9F{4]wwxmantissa/test/test_q2q.py from twisted.trial import unittest from axiom import store from xmantissa import ixmantissa, endpoint class MantissaQ2Q(unittest.TestCase): def testInstallation(self): d = self.mktemp() s = store.Store(unicode(d)) q = endpoint.UniversalEndpointService(store=s) q.installOn(s) self.assertIdentical(ixmantissa.IQ2QService(s), q) PK9F!!xmantissa/test/test_recordattr.py """ Tests for xmantissa._recordattr module. """ from twisted.trial.unittest import TestCase from epsilon.structlike import record from axiom.item import Item from axiom.attributes import text, integer from axiom.store import Store from xmantissa._recordattr import RecordAttribute, WithRecordAttributes class Sigma(record('left right')): """ A simple record type which composes two attributes. """ def getLeft(self): """ Return the left attribute. """ return self.left def getRight(self): """ Return the right attribute. """ return self.right class RecordAttributeTestItem(Item): """ An item for testing record attributes. """ alpha = text() beta = integer() sigma = RecordAttribute(Sigma, [alpha, beta]) class RecordAttributeRequired(Item, WithRecordAttributes): """ An item for testing record attributes with mandatory attributes. """ alpha = text(allowNone=False) beta = integer(allowNone=False) sigma = RecordAttribute(Sigma, [alpha, beta]) class ItemWithRecordAttributeTest(TestCase): """ An item with a RecordAttribute attribute ought to return and store its attributes. """ def eitherWay(self, thunk, T, **kw): """ Run the given 'thunk' with an item not inserted into a store, then one inserted into a store after the fact, then one inserted into a store in its constructor. """ ra1 = T(**kw) thunk(ra1) s = Store() ra2 = T(**kw) ra2.store = s thunk(ra2) ra3 = T(store=s, **kw) thunk(ra3) def test_getAttribute(self): """ Retrieving a L{RecordAttribute} whose component attributes are set normally should yield a record instance of the appropriate type with the values set. """ def check(rati): self.assertEqual(rati.sigma.getLeft(), u'one') self.assertEqual(rati.sigma.getRight(), 2) self.eitherWay(check, RecordAttributeTestItem, alpha=u'one', beta=2) def test_initialize(self): """ Initializing an item with a record attribute set to something should set its underlying attributes. """ def check(rati): self.assertEqual(rati.alpha, u'five') self.assertEqual(rati.beta, 6) self.eitherWay(check, RecordAttributeTestItem, sigma=Sigma(left=u'five', right=6)) def test_initializeNoNones(self): """ Initializing an item with a record attribute set to something should set its underlying attributes in the case where the underlying item does not work. """ def check(rati): self.assertEqual(rati.alpha, u'seven') self.assertEqual(rati.beta, 8) self.eitherWay(check, RecordAttributeRequired.create, sigma=Sigma(left=u'seven', right=8)) def test_setAttribute(self): """ Setting a L{RecordAttribute} should set all of its component attributes. """ def check(rati): rati.sigma = Sigma(left=u'three', right=4) self.assertEqual(rati.alpha, u'three') self.assertEqual(rati.beta, 4) self.eitherWay(check, RecordAttributeTestItem) def test_queryComparisons(self): """ Querying with an inequality on a L{RecordAttribute} should yield the same results as querying on its AND'ed component attributes. """ s = Store() RARc = RecordAttributeRequired.create x = RARc(store=s, sigma=Sigma(left=u'x', right=1)) y = RARc(store=s, sigma=Sigma(left=u'y', right=2)) z = RARc(store=s, sigma=Sigma(left=u'z', right=3)) a = RARc(store=s, sigma=Sigma(left=u'a', right=4)) self.assertEqual(list(s.query( RecordAttributeRequired, RecordAttributeRequired.sigma == Sigma(u'z', 3))), [z]) self.assertEqual(list(s.query( RecordAttributeRequired, RecordAttributeRequired.sigma == Sigma(u'z', 9))), []) self.assertEqual(list(s.query( RecordAttributeRequired, RecordAttributeRequired.sigma != Sigma(u'y', 2), sort=RecordAttributeRequired.storeID.ascending)), [x, z, a]) PK9F'k"xmantissa/test/test_rendertools.py """ Tests for L{xmantissa.test.rendertools}. """ from twisted.trial.unittest import TestCase from nevow.athena import LiveFragment, LiveElement from nevow.loaders import stan from nevow.tags import p, directive from xmantissa.test.rendertools import renderLiveFragment class LivePageRendererTestCase(TestCase): """ Test utility function L{render} to make sure it can render various kinds of fragments. """ message = 'Hello, world.' def docFactory(self, renderer, message): return stan(p(render=directive(renderer))[message]) def testRenderLiveFragment(self): """ Test that L{render} spits out the right thing for a L{LiveFragment}. """ docFactory = self.docFactory('liveFragment', self.message) self.assertIn( self.message, renderLiveFragment(LiveFragment(docFactory=docFactory))) def testRenderLiveElement(self): """ Test that L{render} spits out the right thing for a L{LiveElement}. """ docFactory = self.docFactory('liveElement', self.message) self.assertIn( self.message, renderLiveFragment(LiveElement(docFactory=docFactory))) PK9FDxmantissa/test/test_search.pyfrom zope.interface import implements from twisted.internet import defer from axiom.store import Store from axiom.item import Item from axiom import attributes from axiom.dependency import installOn from nevow.testutil import renderLivePage, FragmentWrapper, AccumulatingFakeRequest from nevow import loaders from nevow.athena import LiveFragment from twisted.trial import unittest from xmantissa import search, ixmantissa from xmantissa.webtheme import getLoader class QueryParseTestCase(unittest.TestCase): def testPlainTerm(self): """ Test that a regular boring search query with nothing special is parsed as such. """ self.assertEquals(search.parseSearchTerm(u"foo"), (u"foo", None)) self.assertEquals(search.parseSearchTerm(u"foo bar"), (u"foo bar", None)) def testKeywordTerm(self): """ Test keywords in a search query are found and returned. """ self.assertEquals( search.parseSearchTerm(u"foo:bar"), (u"", {u"foo": u"bar"})) self.assertEquals( search.parseSearchTerm(u"foo bar:baz"), (u"foo", {u"bar": u"baz"})) self.assertEquals( search.parseSearchTerm(u"foo bar baz:quux"), (u"foo bar", {u"baz": u"quux"})) self.assertEquals( search.parseSearchTerm(u"foo bar:baz quux"), (u"foo quux", {u"bar": u"baz"})) self.assertEquals( search.parseSearchTerm(u"foo bar:baz quux foobar:barbaz"), (u"foo quux", {u"bar": u"baz", u"foobar": u"barbaz"})) def testBadlyFormedKeywordTerm(self): """ Test that a keyword search that isn't quite right gets cleaned up. """ self.assertEquals( search.parseSearchTerm(u"foo:"), (u"foo", None)) self.assertEquals( search.parseSearchTerm(u":foo"), (u"foo", None)) self.assertEquals( search.parseSearchTerm(u":"), (u"", None)) class TrivialSearchProvider(Item): implements(ixmantissa.ISearchProvider) z = attributes.integer() powerupInterfaces = (ixmantissa.ISearchProvider,) def search(self, term, keywords=None, count=None, offset=0, sortAscending=True): class TrivialResultsFragment(LiveFragment): docFactory = loaders.stan('This is a search result') return defer.succeed(TrivialResultsFragment()) class ArgumentPassthroughSearchProvider(Item): implements(ixmantissa.ISearchProvider) powerupInterfaces = (ixmantissa.ISearchProvider,) z = attributes.integer() def search(self, *a, **k): return defer.succeed((a, k)) class DecodingTestCase(unittest.TestCase): """ Tests for encoding of search terms """ def _renderAggregateSearch(self, charset, term): """ Set up a store, and render an aggregate search, with charset C{charset} and search term {term} @return: deferred firing with string render result """ s = Store() installOn(TrivialSearchProvider(store=s), s) agg = search.SearchAggregator(store=s) installOn(agg, s) f = search.AggregateSearchResults(agg) f.docFactory = getLoader(f.fragmentName) page = FragmentWrapper(f) req = AccumulatingFakeRequest() req.args = dict(_charset_=[charset], term=[term]) result = renderLivePage(page, reqFactory=lambda: req) return result def testRenderingQueryOK(self): """ Check that a rendered aggregate search yields results if given a valid charset and encoded term """ def gotResult(res): self.assertIn('This is a search result', res) return self._renderAggregateSearch('utf-8', 'r\xc3\xb4').addCallback(gotResult) def testRenderingQueryBad(self): """ Check that a rendered aggregate search doesn't contain any results if the charset is unknown """ def gotResult(res): self.assertIn('Your browser sent', res) return self._renderAggregateSearch('divmod-27', 'yeah').addCallback(gotResult) class AggregatorTestCase(unittest.TestCase): """ Test that the behaviour of L{xmantissa.search.SearchAggregator} conforms to L{xmantissa.ixmantissa.ISearchAggregator} """ def test_argsForwarded(self): """ Test that the search aggregator forwards arguments given to C{search} to L{xmantissa.ixmantissa.ISearchProvider}s """ s = Store() installOn(ArgumentPassthroughSearchProvider(store=s), s) agg = search.SearchAggregator(store=s) installOn(agg, s) args = (((u'foo', {u'bar': u'baz'}), {'sortAscending': False}), ((u'bar', {u'foo': u'oof'}), {'sortAscending': True}), ((u'', {}), {'sortAscending': True, 'count': 5, 'offset': 1})) def checkArgs(gotArgs): for ((success, gotArgset), expectedArgset) in zip(gotArgs, args): self.assertEquals(gotArgset[0], expectedArgset) dl = defer.DeferredList([agg.search(*a[0], **a[1]) for a in args]) dl.addCallback(checkArgs) return dl PK9F 3}3}xmantissa/test/test_sharing.py """ Unit tests for the L{xmantissa.sharing} module. """ from zope.interface import Interface, implements from epsilon.hotfix import require require("twisted", "trial_assertwarns") from twisted.trial import unittest from twisted.python.components import registerAdapter from twisted.protocols.amp import Command, Box, parseString from axiom.store import Store from axiom.item import Item from axiom.attributes import integer, boolean from axiom.test.util import QueryCounter from axiom.userbase import LoginMethod, LoginAccount from xmantissa import sharing class IPrivateThing(Interface): def mutateSomeState(): pass class IExternal(Interface): """ This is an interface for functionality defined externally to an item. """ def doExternal(): """ Do something external. """ class IExtraExternal(IExternal): """ This is an interface for functionality defined externally to an item. """ def doExternalExtra(): """ Do something _extra_ and external. """ class IConflicting(Interface): """ This is an interface with a name that conflicts with IExternal. """ def doExternal(): """ Conflicts with IExternal.doExternal. """ class IReadOnly(Interface): def retrieveSomeState(): """ Retrieve the data. """ class PrivateThing(Item): implements(IPrivateThing, IReadOnly) publicData = integer(indexed=True) externalized = boolean() typeName = 'test_sharing_private_thing' schemaVersion = 1 def mutateSomeState(self): self.publicData += 5 def retrieveSomeState(self): """ Retrieve my public data. """ return self.publicData class ExtraPrivateThing(Item): """ Private class which supports extra operations / adaptations. """ privateData = integer(indexed=True) def doPrivateThing(self): """ A method, that does a thing. """ class ExternalThing(object): implements(IExternal) def __init__(self, priv): self.privateThing = priv def doExternal(self): """ Do an external thing. """ self.privateThing.externalized = True return "external" class ExtraExternalThing(object): """ Adapter for ExtraPrivateThing. """ implements(IExtraExternal) def __init__(self, extraPrivateThing): self.extraPrivateThing = extraPrivateThing # WARNING WARNING WARNING WARNING WARNING # The following two methods are shared methods on a shared interfaces which # return 'self' as one of the parts of their return value. IN NORMAL # APPLICATION CODE THIS IS A SECURITY RISK! One of the major features of # the sharing system is that it makes programs secure in the face of errors # such as accessing an attribute that you are not technically supposed to # access from the view. However, if you give back 'self' to some view code # that is asking externally, you are telling the view that it has FULL # ACCESS to this object and may call methods on it with impunity. DO NOT # DO THIS IN NORMAL APPLICATION CODE, IT IS ONLY FOR TESTING!!! def doExternal(self): """ Do something external and return a 2-tuple of a marker value to indicate that this adapter class was used and an identifier to confirm the adapter's identity. """ return ("external", self) def doExternalExtra(self): """ Similar to doExternal, but a different method """ return ("external-extra", self) # DANGER DANGER DANGER DANGER DANGER registerAdapter(ExternalThing, PrivateThing, IExternal) registerAdapter(ExtraExternalThing, ExtraPrivateThing, IExtraExternal) class IPublicThing(Interface): def callMethod(): pass def isMethodAvailable(self): pass class PublicFacingAdapter(object): implements(IPublicThing) def __init__(self, thunk): self.thunk = thunk def isMethodAvailable(self): return IPrivateThing.providedBy(self.thunk) def callMethod(self): return self.thunk.mutateSomeState() registerAdapter(PublicFacingAdapter, IReadOnly, IPublicThing) class InterfaceUtils(unittest.TestCase): """ Test cases for private zope interface utility functionality. """ class A(Interface): "A root interface." class B(A): "A->B" class C(B): "A->B->C" class D(C): "A->B->C->D" class Q(Interface): "Another root interface, unrelated to A" class W(B): """ A->B->... | +->W """ class X(W): "A->B->W->X" class Y(X): "A->B->W->X->Y" class Z(Y): "A->B->W->X->Y->Z" def test_commonParent(self): """ Verify the function which determines the common parent of two interfaces. """ self.assertEquals(sharing._commonParent(self.Z, self.D), self.B) def test_noCommonParent(self): """ _commonParent should return None for two classes which do not have a common parent. """ self.assertIdentical(sharing._commonParent(self.A, self.Q), None) def test_commonParentOfYourself(self): """ The common parent of the same object is itself. """ self.assertIdentical(sharing._commonParent(self.A, self.A), self.A) class SimpleSharing(unittest.TestCase): def setUp(self): self.store = Store() def test_differentUserSameID(self): """ Verify that if different facets of the same item are shared to different users with the same shareID, each user will receive the correct respective facet with only the correct methods exposed. """ t = PrivateThing(store=self.store, publicData=789) toBob = sharing.shareItem(t, toName=u'bob@example.com', interfaces=[IReadOnly]) toAlice = sharing.shareItem(t, toName=u'alice@example.com', shareID=toBob.shareID, interfaces=[IPrivateThing]) # Sanity check. self.assertEquals(toBob.shareID, toAlice.shareID) asBob = sharing.getShare(self.store, sharing.getPrimaryRole( self.store, u'bob@example.com'), toBob.shareID) asAlice = sharing.getShare(self.store, sharing.getPrimaryRole( self.store, u'alice@example.com'), toBob.shareID) self.assertEquals(asBob.retrieveSomeState(), 789) self.assertRaises(AttributeError, lambda : asBob.mutateSomeState) self.assertRaises(AttributeError, lambda : asAlice.retrieveSomeState) asAlice.mutateSomeState() # Make sure they're both seeing the same item. self.assertEquals(asBob.retrieveSomeState(), 789+5) def test_simpleShareMethods(self): """ Verify that an item which is shared with Role.shareItem can be retrieved and manipulated with Role.getShare. This is the new-style API, which isn't yet widely used, but should be preferred in new code. """ t = PrivateThing(store=self.store, publicData=456) bob = sharing.getPrimaryRole(self.store, u'bob@example.com', createIfNotFound=True) shareItemResult = bob.shareItem(t) gotShare = bob.getShare(shareItemResult.shareID) gotShare.mutateSomeState() self.assertEquals(t.publicData, 456 + 5) def test_simpleShare(self): """ Verify that an item which is shared with shareItem can be retrieved and manipulated with getShare. This is an older-style API, on its way to deprecation. """ t = PrivateThing(store=self.store, publicData=456) shareItemResult = self.assertWarns( PendingDeprecationWarning, "Use Role.shareItem() instead of sharing.shareItem().", __file__, lambda : sharing.shareItem(t, toName=u'bob@example.com')) bob = sharing.getPrimaryRole(self.store, u'bob@example.com') gotShare = self.assertWarns( PendingDeprecationWarning, "Use Role.getShare() instead of sharing.getShare().", __file__, lambda : sharing.getShare(self.store, bob, shareItemResult.shareID)) gotShare.mutateSomeState() self.assertEquals(t.publicData, 456 + 5) def test_invalidShareID(self): """ Verify that NoSuchShare is raised when getShare is called without sharing anything first. """ self.assertRaises(sharing.NoSuchShare, sharing.getShare, self.store, sharing.getPrimaryRole(self.store, u'nobody@example.com'), u"not a valid shareID") def test_unauthorizedAccessNoShare(self): """ Verify that NoSuchShare is raised when getShare is called with a user who is not allowed to access a shared item. """ t = PrivateThing(store=self.store, publicData=345) theShare = sharing.shareItem(t, toName=u'somebody@example.com') self.assertRaises(sharing.NoSuchShare, sharing.getShare, self.store, sharing.getPrimaryRole(self.store, u'nobody@example.com'), theShare.shareID) def test_deletedOriginalNoShare(self): """ NoSuchShare should be raised when getShare is called with an item who is not allowed to access a shared item. """ t = PrivateThing(store=self.store, publicData=234) theShare = sharing.shareItem(t, toName=u'somebody@example.com') t.deleteFromStore() self.assertRaises(sharing.NoSuchShare, sharing.getShare, self.store, sharing.getPrimaryRole(self.store, u'somebody@example.com'), theShare.shareID) def test_shareAndAdapt(self): """ Verify that when an item is shared to a particular user with a particular interface, retrieving it for that user results in methods on the given interface being callable and other methods being restricted. """ t = PrivateThing(store=self.store, publicData=789) # Sanity check. self.failUnless(IPublicThing(t).isMethodAvailable()) shared = sharing.shareItem(t, toName=u'testshare', interfaces=[IReadOnly]) proxy = sharing.getShare(self.store, sharing.getPrimaryRole(self.store, u'testshare'), shared.shareID) self.assertFalse(IPublicThing(proxy).isMethodAvailable()) self.assertRaises(AttributeError, IPublicThing(proxy).callMethod) def test_getShareProxyWithAdapter(self): """ When you share an item with an interface that has an adapter for that interface, the object that results from getShare should provide the interface by exposing the adapter rather than the original item. """ privateThing = PrivateThing(store=self.store) shared = sharing.shareItem(privateThing, toName=u'testshare', interfaces=[IExternal]) proxy = sharing.getShare(self.store, sharing.getPrimaryRole(self.store, u'testshare'), shared.shareID) proxy.doExternal() self.assertTrue(privateThing.externalized) def test_conflictingNamesException(self): """ When you share an item with two interfaces that contain different adapters with conflicting names, an exception should be raised alerting you to this conflict. """ extraPrivateThing = ExtraPrivateThing(store=self.store) self.assertRaises(sharing.ConflictingNames, sharing.shareItem, extraPrivateThing, toName=u'testshare', interfaces=[IExternal, IConflicting]) def test_coalesceInheritedAdapters(self): """ If multiple interfaces that are part of the same inheritance hierarchy are specified, only the leaf interfaces should be adapted to, and provided for all interfaces it inherits from. """ extraPrivateThing = ExtraPrivateThing(store=self.store) role = sharing.getPrimaryRole(self.store, u'testshare') extraProxy = sharing.getShare( self.store, role, sharing.shareItem( extraPrivateThing, toRole=role, interfaces=[IExternal, IExtraExternal]).shareID) externalTag, externalObj = extraProxy.doExternal() extraExternalTag, extraExternalObj = extraProxy.doExternalExtra() self.assertIdentical(externalObj, extraExternalObj) self.assertEquals(externalTag, 'external') self.assertEquals(extraExternalTag, 'external-extra') class AccessibilityQuery(unittest.TestCase): def setUp(self): self.i = 0 self.store = Store() self.things = [] self.bobThings = [] self.aliceThings = [] self.bob = sharing.getPrimaryRole(self.store, u'bob@example.com', createIfNotFound=True) self.alice = sharing.getPrimaryRole(self.store, u'alice@example.com', createIfNotFound=True) def test_twoInterfacesTwoGroups(self): """ Verify that when an item is shared to two roles that a user is a member of, they will have access to both interfaces when it is retrieved with getShare. """ self.addSomeThings() us = sharing.getPrimaryRole(self.store, u'us', True) them = sharing.getPrimaryRole(self.store, u'them', True) self.bob.becomeMemberOf(us) self.bob.becomeMemberOf(them) it = PrivateThing(store=self.store, publicData=1234) sharing.shareItem(it, toRole=us, shareID=u'q', interfaces=[IPrivateThing]) sharing.shareItem(it, toRole=them, shareID=u'q', interfaces=[IReadOnly]) that = sharing.getShare(self.store, self.bob, u'q') self.assertEquals(that.retrieveSomeState(), 1234) that.mutateSomeState() self.assertEquals(that.retrieveSomeState(), 1239) def test_twoInterfacesTwoGroupsQuery(self): """ Verify that when an item is shared to two roles that a user is a member of, and then retrieved by an asAccessibleTo query, both interfaces will be accessible on each object in the query result, and the same number of items will be accessible in the query as were shared. """ us = sharing.getPrimaryRole(self.store, u'us', True) them = sharing.getPrimaryRole(self.store, u'them', True) self.bob.becomeMemberOf(us) self.bob.becomeMemberOf(them) for x in range(3): it = PrivateThing(store=self.store, publicData=x) sharing.shareItem(it, toRole=us, shareID=u'q', interfaces=[IPrivateThing]) sharing.shareItem(it, toRole=them, shareID=u'q', interfaces=[IReadOnly]) # sanity check self.assertEquals(self.store.query(PrivateThing).count(), 3) aat = list(sharing.asAccessibleTo(self.bob, self.store.query( PrivateThing, sort=PrivateThing.publicData.descending))) aat2 = list(sharing.asAccessibleTo(self.bob, self.store.query( PrivateThing, sort=PrivateThing.publicData.ascending))) # sanity check x2 for acc in aat: acc.mutateSomeState() expectedData = [x + 5 for x in reversed(range(3))] self.assertEquals([acc.retrieveSomeState() for acc in aat], expectedData) self.assertEquals([acc.retrieveSomeState() for acc in aat2], list(reversed(expectedData))) def test_twoInterfacesTwoGroupsUnsortedQuery(self): """ Verify that when duplicate shares exist for the same item and an asAccessibleTo query is made with no specified sort, the roles are still deduplicated properly. """ us = sharing.getPrimaryRole(self.store, u'us', True) them = sharing.getPrimaryRole(self.store, u'them', True) self.bob.becomeMemberOf(us) self.bob.becomeMemberOf(them) for x in range(3): it = PrivateThing(store=self.store, publicData=x) sharing.shareItem(it, toRole=us, shareID=u'q', interfaces=[IPrivateThing]) sharing.shareItem(it, toRole=them, shareID=u'q', interfaces=[IReadOnly]) # sanity check self.assertEquals(self.store.query(PrivateThing).count(), 3) aat = list(sharing.asAccessibleTo(self.bob, self.store.query( PrivateThing))) # sanity check x2 for acc in aat: acc.mutateSomeState() expectedData = [x + 5 for x in range(3)] aat.sort(key=lambda i: i.retrieveSomeState()) self.assertEquals([acc.retrieveSomeState() for acc in aat], expectedData) def addSomeThings(self): privateThing = PrivateThing(store=self.store, publicData=-self.i) self.i += 1 self.things.append(privateThing) self.bobThings.append(sharing.shareItem( privateThing, toName=u'bob@example.com', interfaces=[IReadOnly])) self.aliceThings.append(sharing.shareItem( privateThing, toName=u'alice@example.com', interfaces=[IPrivateThing])) def test_asAccessibleTo(self): """ Ensure that L{Role.asAccessibleTo} returns only items actually accessible to the given role. """ for i in range(10): self.addSomeThings() query = self.store.query(PrivateThing) aliceQuery = list(self.alice.asAccessibleTo(query)) bobQuery = list(self.bob.asAccessibleTo(query)) self.assertEqual(map(sharing.itemFromProxy, bobQuery), map(lambda x: x.sharedItem, self.bobThings)) self.assertEqual(map(sharing.itemFromProxy, aliceQuery), map(lambda x: x.sharedItem, self.aliceThings)) self.assertEqual([p.sharedInterfaces for p in aliceQuery], [[IPrivateThing]] * 10) self.assertEqual([p.sharedInterfaces for p in bobQuery], [[IReadOnly]] * 10) def test_accessibilityQuery(self): """ Ensure that asAccessibleTo returns only items actually accessible to the given role. """ for i in range(10): self.addSomeThings() query = self.store.query(PrivateThing) aliceQuery = self.assertWarns( PendingDeprecationWarning, "Use Role.asAccessibleTo() instead of sharing.asAccessibleTo().", __file__, lambda : list(sharing.asAccessibleTo(self.alice, query))) bobQuery = list(sharing.asAccessibleTo(self.bob, query)) self.assertEqual(map(sharing.itemFromProxy, bobQuery), map(lambda x: x.sharedItem, self.bobThings)) self.assertEqual(map(sharing.itemFromProxy, aliceQuery), map(lambda x: x.sharedItem, self.aliceThings)) self.assertEqual([p.sharedInterfaces for p in aliceQuery], [[IPrivateThing]] * 10) self.assertEqual([p.sharedInterfaces for p in bobQuery], [[IReadOnly]] * 10) def test_sortOrdering(self): """ Ensure that asAccessibleTo respects query sort order. """ for i in range(10): self.addSomeThings() query = self.store.query(PrivateThing, sort=PrivateThing.publicData.ascending) # Sanity check. self.assertEquals([x.publicData for x in query], range(-9, 1, 1)) bobQuery = list(sharing.asAccessibleTo(self.bob, query)) self.assertEquals([x.retrieveSomeState() for x in bobQuery], range(-9, 1, 1)) query2 = self.store.query(PrivateThing, sort=PrivateThing.publicData.descending) # Sanity check #2 self.assertEquals([x.publicData for x in query2], range(-9, 1, 1)[::-1]) bobQuery2 = list(sharing.asAccessibleTo(self.bob, query2)) self.assertEquals([x.retrieveSomeState() for x in bobQuery2], range(-9, 1, 1)[::-1]) def test_limit(self): """ Ensure that asAccessibleTo respects query limits. """ for i in range(10): self.addSomeThings() query = self.store.query(PrivateThing, limit=3) bobQuery = list(sharing.asAccessibleTo(self.bob, query)) self.assertEquals(len(bobQuery), 3) def test_limitGetsAllInterfaces(self): """ asAccessibleTo should always collate interfaces together, regardless of its limit parameter. """ t = PrivateThing(store=self.store, publicData=self.i) sharing.shareItem(t, toName=u'bob@example.com', interfaces=[IPrivateThing], shareID=u'test') sharing.shareItem(t, toName=u'Everyone', interfaces=[IReadOnly], shareID=u'test') L = list(sharing.asAccessibleTo( self.bob, self.store.query(PrivateThing, limit=1))) self.assertEquals(len(L), 1) self.assertEquals(set(L[0].sharedInterfaces), set([IReadOnly, IPrivateThing])) def test_limitMultiShare(self): """ asAccessibleTo should stop after yielding the limit number of results, even if there are more shares examined than results. """ L = [] for x in range(10): t = PrivateThing(store=self.store, publicData=self.i) L.append(t) self.i += 1 sharing.shareItem(t, toName=u'bob@example.com', interfaces=[IPrivateThing], shareID=unicode(x)) sharing.shareItem(t, toName=u'Everyone', interfaces=[IReadOnly], shareID=unicode(x)) proxies = list(sharing.asAccessibleTo( self.bob, self.store.query(PrivateThing, limit=5, sort=PrivateThing.publicData.ascending))) self.assertEquals(map(sharing.itemFromProxy, proxies), L[:5]) for proxy in proxies: self.assertEquals(set(proxy.sharedInterfaces), set([IPrivateThing, IReadOnly])) def test_limitWithPrivateStuff(self): """ Verify that a limited query with some un-shared items will return up to the provided limit number of shared items. """ L = [] def makeThing(shared): t = PrivateThing(store=self.store, publicData=self.i) self.i += 1 if shared: sharing.shareItem( t, toRole=self.bob, interfaces=[IPrivateThing], shareID=unicode(self.i)) L.append(t) # 0, 1, 2: shared for x in range(3): makeThing(True) # 3, 4, 5: private for x in range(3): makeThing(False) # 6, 7, 8: shared again for x in range(3): makeThing(True) self.assertEquals( map(sharing.itemFromProxy, sharing.asAccessibleTo( self.bob, self.store.query( PrivateThing, limit=5))), [L[0], L[1], L[2], L[6], L[7]]) def test_limitEfficiency(self): """ Verify that querying a limited number of shared items does not become slower as more items are shared. """ zomg = QueryCounter(self.store) for i in range(10): self.addSomeThings() query = self.store.query( PrivateThing, limit=3, sort=PrivateThing.publicData.ascending) checkit = lambda : list(sharing.asAccessibleTo(self.bob, query)) before = zomg.measure(checkit) for i in range(10): self.addSomeThings() after = zomg.measure(checkit) self.assertEquals(before, after) test_limitEfficiency.todo = ( 'currently gets too many results because we should be using paginate') class HeuristicTestCases(unittest.TestCase): """ These are tests for sharing APIs which heuristically determine identifying information about a user's store or a shared item. """ def setUp(self): """ Set up a store for testing. """ self.store = Store() self.account = LoginAccount(store=self.store, password=u'1234') self.method = LoginMethod(store=self.store, account=self.account, localpart=u'username', domain=u'domain.example.com', internal=True, protocol=u'*', verified=True) def test_getSelfRole(self): """ The self-role of a store should be determined by its L{LoginMethod}s. """ self.assertEquals(list(self.store.query(sharing.Role)), []) me = sharing.getSelfRole(self.store) self.assertEquals(me.externalID, u'username@domain.example.com') self.assertEquals(me.store, self.store) self.assertEquals(list(me.allRoles()), [me, sharing.getAuthenticatedRole(self.store), sharing.getEveryoneRole(self.store)]) def test_getAccountRole(self): """ L{getAccountRole} returns a L{Role} in a given store for one of the account names passed to it. """ role = sharing.getAccountRole( self.store, [(u"username", u"domain.example.com")]) self.assertEquals(role.externalID, u"username@domain.example.com") def test_noAccountRole(self): """ L{getAccountRole} raises L{ValueError} if passed an empty list of account names. """ self.assertRaises(ValueError, sharing.getAccountRole, self.store, []) def test_identifierFromSharedItem(self): """ L{sharing.Identifier.fromSharedItem} should identify a shared Item's shareID. """ t = PrivateThing(store=self.store) sharing.getEveryoneRole(self.store).shareItem(t, shareID=u'asdf') sid = sharing.Identifier.fromSharedItem(t) self.assertEquals(sid.shareID, u'asdf') self.assertEquals(sid.localpart, u'username') self.assertEquals(sid.domain, u'domain.example.com') def test_identifierFromSharedItemMulti(self): """ L{sharing.Identifier.fromSharedItem} should identify a shared Item's shareID even if it is shared multiple times. """ t = PrivateThing(store=self.store) sharing.getEveryoneRole(self.store).shareItem(t, shareID=u'asdf') sharing.getAuthenticatedRole(self.store).shareItem(t, shareID=u'jkl;') sid = sharing.Identifier.fromSharedItem(t) self.assertIn(sid.shareID, [u'asdf', u'jkl;']) self.assertEquals(sid.localpart, u'username') self.assertEquals(sid.domain, u'domain.example.com') def test_identifierFromSharedItemNoShares(self): """ L{sharing.Identifier.fromSharedItem} should raise L{NoSuchShare} if the given item is not shared. """ t = PrivateThing(store=self.store) self.assertRaises(sharing.NoSuchShare, sharing.Identifier.fromSharedItem, t) def test_identifierFromSharedItemNoMethods(self): """ L{sharing.Identifier.fromSharedItem} should raise L{NoSuchShare} if the given item's store contains no L{LoginMethod} objects. """ self.method.deleteFromStore() t = PrivateThing(store=self.store) sharing.getEveryoneRole(self.store).shareItem(t, shareID=u'asdf') self.assertRaises(sharing.NoSuchShare, sharing.Identifier.fromSharedItem, t) class CommandWithIdentifier(Command): """ This command has an Identifier as one of its arguments. """ arguments = [('shareIdentTest', sharing.IdentifierArgument())] class IdentifierTestCases(unittest.TestCase): """ Tests for the behavior of L{xmantissa.sharing.Identifier} """ def setUp(self): """ Create a few identifiers. """ self.aliceObject = sharing.Identifier(u'object', u'alice', u'example.com') self.mostlyAlice = sharing.Identifier(u'not-the-object', u'alice', u'example.com') self.otherAlice = sharing.Identifier(u'object', u'alice', u'example.com') def test_equivalence(self): """ Two L{sharing.Identifier} objects that identify the same shared item should compare the same. """ self.assertEquals(self.aliceObject, self.otherAlice) self.assertFalse(self.aliceObject != self.otherAlice) def test_nonEquivalence(self): """ Two L{sharing.Identifier} objects that identify the same shared item should compare as not equal. """ self.assertNotEqual(self.aliceObject, self.mostlyAlice) def test_otherTypes(self): """ Other types should not compare equal to an L{sharing.Identifier}. """ self.assertNotEqual(self.aliceObject, object()) class IdentifierArgumentTests(unittest.TestCase): """ Tests for serialization and unserialization of L{xmantissa.sharing.Identifier} using L{xmantissa.sharing.IdentifierArgument}. """ def setUp(self): """ Set up an identifier and its expected serialized form. """ self.identifier = sharing.Identifier( u'\u1234object', u'alice', u'example.com') self.expectedData = Box( shareIdentTest=Box(shareID=u'\u1234object'.encode('utf-8'), localpart=u'alice'.encode('utf-8'), domain=u'example.com'.encode('utf-8') ).serialize() ).serialize() def test_parse(self): """ L{sharing.IdentifierArgument} should be able to serialize an L{Identifier} as an AMP argument to a box. """ outputBox = CommandWithIdentifier.makeArguments( dict(shareIdentTest=self.identifier), None) outputData = outputBox.serialize() # Assert on the intermediate / serialized state to make sure the # protocol remains stable. self.assertEquals(self.expectedData, outputData) def test_unparse(self): """ L{sharing.IdentifierArgument} should be able to unserialize an L{Identifier} from a serialized box. """ argDict = CommandWithIdentifier.parseArguments( parseString(self.expectedData)[0], None) self.assertEquals(argDict, dict(shareIdentTest=self.identifier)) PK9FA""xmantissa/test/test_signup.py from twisted.trial import unittest from axiom import store, userbase from axiom.item import Item from axiom.attributes import inmemory, integer from axiom.plugins import mantissacmd from xmantissa import signup, offering from xmantissa.plugins import free_signup from xmantissa.product import Product, Installation class SignupCreationTestCase(unittest.TestCase): def setUp(self): self.store = store.Store() self.ls = userbase.LoginSystem(store=self.store) self.admin = self.ls.addAccount(u'admin', u'localhost', None, internal=True, verified=True) self.substore = self.admin.avatars.open() self.sc = signup.SignupConfiguration(store=self.substore) def _installTestOffering(self): io = offering.InstalledOffering( store=self.store, offeringName=u"mantissa", application=None) def createFreeSignup(self, itemClass, url=u'signup', prompt=u'Sign Up!'): """ A utility method to ensure that the same arguments are always used to create signup mechanisms, since these are the arguments that are going to be coming from the admin form. """ product = Product(store=self.store, types=[]) return self.sc.createSignup( u'testuser@localhost', itemClass, {'prefixURL': url}, product, u'Blank Email Template', prompt) def testCreateFreeSignups(self): self._installTestOffering() for signupMechanismPlugin in [free_signup.freeTicket, free_signup.userInfo]: self.createFreeSignup(signupMechanismPlugin.itemClass) def test_usernameAvailability(self): """ Test that the usernames which ought to be available are and that those which aren't are not: Only syntactically valid localparts are allowed. Localparts which are already assigned are not allowed. Only domains which are actually served by this mantissa instance are allowed. """ signup = self.createFreeSignup(free_signup.userInfo.itemClass) # Allowed: unused localpart, same domain as the administrator created # by setUp. self.failUnless(signup.usernameAvailable(u'alice', u'localhost')[0]) # Not allowed: unused localpart, unknown domain. self.failIf(signup.usernameAvailable(u'alice', u'example.com')[0]) # Not allowed: used localpart, same domain as the administrator created # by setUp. self.failIf(signup.usernameAvailable(u'admin', u'localhost')[0]) self.assertEquals(signup.usernameAvailable(u'fjones', u'localhost'), [True, u'Username already taken']) signup.createUser( realName=u"Frank Jones", username=u'fjones', domain=u'localhost', password=u'asdf', emailAddress=u'fj@crappy.example.com') self.assertEquals(signup.usernameAvailable(u'fjones', u'localhost'), [False, u'Username already taken']) ss = self.ls.accountByAddress(u"fjones", u"localhost").avatars.open() self.assertEquals(ss.query(Installation).count(), 1) def testUserInfoSignupValidation2(self): """ Ensure that invalid characters aren't allowed in usernames, that usernames are parsable as the local part of an email address and that usernames shorter than two characters are invalid. """ signup = self.createFreeSignup(free_signup.userInfo.itemClass) self.assertEquals(signup.usernameAvailable(u'foo bar', u'localhost'), [False, u"Username contains invalid character: ' '"]) self.assertEquals(signup.usernameAvailable(u'foo@bar', u'localhost'), [False, u"Username contains invalid character: '@'"]) # '~' is not expressly forbidden by the validator in usernameAvailable, # yet it is rejected by parseAddress (in xmantissa.smtp). self.assertEquals(signup.usernameAvailable(u'fo~o', u'127.0.0.1'), [False, u"Username fails to parse"]) self.assertEquals(signup.usernameAvailable(u'f', u'localhost'), [False, u"Username too short"]) def test_userInfoSignupUserInfo(self): """ Check that C{createUser} creates a L{signup.UserInfo} item with its C{realName} attribute set. """ freeSignup = self.createFreeSignup(free_signup.userInfo.itemClass) freeSignup.createUser( u'Frank Jones', u'fjones', u'divmod.com', u'asdf', u'fj@example.com') account = self.ls.accountByAddress(u'fjones', u'divmod.com') substore = account.avatars.open() userInfos = list(substore.query(signup.UserInfo)) self.assertEqual(len(userInfos), 1) userInfo = userInfos[0] self.assertEqual(userInfo.realName, u'Frank Jones') def test_userInfoCreatedBeforeProductInstalled(self): """ L{UserInfoSignup.createUser} should create a L{UserInfo} item B{before} it calls L{Product.installProductOn}. """ class StubProduct(Item): """ L{Product}-alike which records the existing L{UserInfo} items in the store when it is installed. """ required_axiom_attribute_garbage = integer( doc=""" mandatory Item attribute. """) userInfos = inmemory() def installProductOn(self, substore): """ Find all the L{UserInfo} items in the given store and remember them. """ self.userInfos = list(substore.query(signup.UserInfo)) product = StubProduct(store=self.store) freeSignup = self.createFreeSignup(free_signup.userInfo.itemClass) freeSignup.product = product freeSignup.createUser( u'Frank Jones', u'fjones', u'example.com', u'password', u'fj@example.org') self.assertEqual(len(product.userInfos), 1) def test_userInfoLoginMethods(self): """ Check that C{createUser} creates only two L{LoginMethod}s on the account. """ username, domain = u'fjones', u'divmod.com' signup = self.createFreeSignup(free_signup.userInfo.itemClass) signup.createUser(u'Frank Jones', username, domain, u'asdf', u'fj@example.com') account = self.ls.accountByAddress(username, domain) query = list( self.store.query(userbase.LoginMethod, userbase.LoginMethod.account == account, sort=userbase.LoginMethod.internal.ascending)) self.assertEquals(len(query), 2) self.assertEquals(query[0].internal, False) self.assertEquals(query[0].verified, False) self.assertEquals(query[0].localpart, u'fj') self.assertEquals(query[0].domain, u'example.com') self.assertEquals(query[1].internal, True) self.assertEquals(query[1].verified, True) self.assertEquals(query[1].localpart, username) self.assertEquals(query[1].domain, domain) def test_freeSignupsList(self): """ Test that if we produce 3 different publicly accessible signups, we get information about all of them back. """ for i, signupMechanismPlugin in enumerate( [free_signup.freeTicket, free_signup.userInfo]): self.createFreeSignup(signupMechanismPlugin.itemClass, url=u'signup%d' % (i+1,), prompt=u"Sign Up %d" % (i+1,)) x = list(signup._getPublicSignupInfo(self.store)) x.sort() self.assertEquals(x, [(u'Sign Up 1', u'/signup1'), (u'Sign Up 2', u'/signup2')]) class ValidatingSignupFormTests(unittest.TestCase): """ Tests for L{ValidatingSignupForm}. """ def test_getInitialArguments(self): """ L{ValidatingSignupForm.getInitialArguments} should return a tuple consisting of a unicode string giving the domain name for which this form will allow signup. """ domain = u"example.com" siteStore = store.Store(filesdir=self.mktemp()) mantissacmd.Mantissa().installSite(siteStore, domain, u"", False) login = siteStore.findUnique(userbase.LoginSystem) login.addAccount(u"alice", domain, u"password", internal=True) userInfo = signup.UserInfoSignup(store=siteStore, prefixURL=u"opaque") form = signup.ValidatingSignupForm(userInfo) self.assertEqual(form.getInitialArguments(), (domain,)) PK9FQ  xmantissa/test/test_siteroot.py """ Tests for L{xmantissa.website.WebSite}'s discovery of L{ISiteRootPlugin} powerups. """ from twisted.trial import unittest from nevow.testutil import FakeRequest from axiom.store import Store from axiom.item import Item from axiom.attributes import text from axiom.dependency import installOn from xmantissa.website import PrefixURLMixin, WebSite from xmantissa.ixmantissa import ISiteRootPlugin from zope.interface import implements class Dummy: def __init__(self, pfx): self.pfx = pfx class PrefixTester(Item, PrefixURLMixin): implements(ISiteRootPlugin) sessioned = True typeName = 'test_prefix_widget' schemaVersion = 1 prefixURL = text() def createResource(self): return Dummy(self.prefixURL) def installSite(self): """ Not using the dependency system for this class because multiple instances can be installed. """ for iface, priority in self.__getPowerupInterfaces__([]): self.store.powerUp(self, iface, priority) class SiteRootTest(unittest.TestCase): def test_prefixPriorityMath(self): """ L{WebSite.locateChild} returns the most specific L{ISiteRootPlugin} based on I{prefixURL} and the request path segments. """ store = Store() PrefixTester(store=store, prefixURL=u"hello").installSite() PrefixTester(store=store, prefixURL=u"").installSite() website = WebSite(store=store) installOn(website, store) res, segs = website.locateChild(FakeRequest(), ('hello',)) self.assertEquals(res.pfx, 'hello') self.assertEquals(segs, ()) res, segs = website.locateChild(FakeRequest(), ('',)) self.assertEquals(res.pfx, '') self.assertEquals(segs, ('',)) PK9F)xmantissa/test/test_smtp.py from twisted.trial.unittest import TestCase from xmantissa.smtp import Address, parseAddress from xmantissa.error import ArgumentError class AddressParserTestCase(TestCase): """ Test the RFC 2821 address parser. """ def _addrTest(self, addr, exp): """ Assert that the given address string parses to the given address object. @type addr: C{str} @type exp: L{Address} """ a = parseAddress(addr) self.assertEquals(a, exp, "Misparsed %r to %r" % (addr, a)) def test_longAddressRejected(self): """ Test that an address longer than 256 bytes is rejected as illegal. """ format = '<%s@example.com>' address = format % ('x' * (257 - len(format) + 2),) self.assertEqual(len(address), 257) self.assertRaises(ArgumentError, parseAddress, address) def test_nullAddress(self): """ Test the parsing of the null address. """ return self._addrTest('<>', Address(None, None, None)) def test_emptyAddress(self): """ Test the parsing of an address with empty local and domain parts. """ return self._addrTest('<@>', Address(None, '', '')) def test_localOnly(self): """ Test the parsing of an address with a non-empty local part and an empty domain part. """ return self._addrTest('', Address(None, 'localpart', '')) def test_domainOnly(self): """ Test the parsing of an address with an empty local part and a non-empty domain part. """ return self._addrTest( '<@example.com>', Address(None, '', 'example.com')) def test_commonAddress(self): """ Test the common case, an address with non-empty local and domain parts. """ return self._addrTest( '', Address(None, 'localpart', 'example.com')) def test_dottedLocalpart(self): """ Test parsing of an address with a dotted local part. """ return self._addrTest( '', Address(None, 'local.part', '')) def test_ipv4Literal(self): """ Test parsing of an IPv4 domain part literal. """ return self._addrTest('<@9.8.7.6>', Address(None, '', '9.8.7.6')) def test_ipv4LiteralWithLocalpart(self): """ Test parsing of an IPv4 domain part literal with a non-empty local part. """ return self._addrTest( '', Address(None, 'localpart', '1.2.3.4')) def test_singlePartSourceRoute(self): """ Test parsing of an address with a one element source route. """ return self._addrTest( '<@foo.bar:localpart@example.com>', Address(['foo.bar'], 'localpart', 'example.com')) def test_multiplePartSourceRoute(self): """ Test parsing of an address with a two element source route. """ return self._addrTest( '<@foo.bar,@bar.baz:localpart@example.com>', Address(['foo.bar', 'bar.baz'], 'localpart', 'example.com')) def test_ipv6Literal(self): """ Test parsing of an IPv6 domain part literal. """ return self._addrTest('<@IPv6:::1>', Address(None, '', '::1')) test_ipv6Literal.skip = "Add IPv6 support to Twisted" def test_ipv6LiteralWithLocalpart(self): """ Test parsing of an IPv6 domain part literal with a non-empty local part. """ return self._addrTest( '', Address(None, 'localpart', '::1')) test_ipv6LiteralWithLocalpart.skip = "Add IPv6 support to Twisted" PK9F52828xmantissa/test/test_tdb.py from epsilon.hotfix import require require("twisted", "trial_assertwarns") from axiom.store import Store from axiom.item import Item from axiom.attributes import integer, text, AND from twisted.trial import unittest from xmantissa import tdb, scrolltable class X(Item): typeName = 'test_tdb_model_dummy' schemaVersion = 1 number = integer() textNumber = text() phoneticDigits = text() digits = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] def breakdown(x): for c in x: yield digits[int(c)] class UnsortableColumn(scrolltable.AttributeColumn): def sortAttribute(self): return None class DeprecatedNamesTest(unittest.TestCase): """ Verify that deprecated names in the 'tdb' module warn appropriately. """ def test_attributeColumn(self): """ Verify that using the AttributeColumn name imported from 'tdb' will warn the user when it is instantiated. """ def function(): attrcol = tdb.AttributeColumn(X.number) self.assertWarns( DeprecationWarning, "tdb.AttributeColumn is deprecated. " "Use scrolltable.AttributeColumn instead.", __file__, function) class ModelTest(unittest.TestCase): def setUp(self): self.store = Store() def _(): for x in range(107): X(store=self.store, number=x, textNumber=unicode(x), phoneticDigits=u' '.join(breakdown(str(x)))) self.store.transact(_) def assertNumbersAre(self, tdm, seq): self.assertEquals(list(x.get('number') for x in tdm.currentPage()), seq) def testUniformValues(self): for x in self.store.query(X): x.number = 1 tdm = tdb.TabularDataModel(self.store, X, [X.number], itemsPerPage=15) self.failUnless(tdm.hasNextPage(), 'expected there to be a next page') def testDeleteEverything(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) self.assertNumbersAre(tdm, range(15)) for item in self.store.query(X): item.deleteFromStore() self.assertNumbersAre(tdm, []) def testOnePage(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], AND(X.number >= 17, X.number < 94), itemsPerPage=15) _assertNumbersAre = lambda seq: self.assertNumbersAre(tdm, seq) _assertNumbersAre(range(17, 17+15)) tdm.nextPage() _assertNumbersAre(range(17+15, 17+30)) tdm.firstPage() _assertNumbersAre(range(17, 17+15)) def testLeafing(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) _assertNumbersAre = lambda seq: self.assertNumbersAre(tdm, seq) _assertNumbersAre(range(15)) tdm.nextPage() _assertNumbersAre(range(15, 30)) tdm.prevPage() _assertNumbersAre(range(15)) tdm.lastPage() # This next assert is kind of weak, because it is valid to have only a # few results on the last page, but right now the strategy is to # always keep the page full of results. No matter what though, the # last page should contain the last value. self.assertEquals(list(tdm.currentPage())[-1]['number'], 106) tdm.firstPage() _assertNumbersAre(range(15)) def testLeafingAndSorting(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) _assertNumbersAre = lambda seq: self.assertNumbersAre(tdm, seq) _assertNumbersAre(range(15)) tdm.lastPage() _assertNumbersAre(range(107-15, 107)) tdm.prevPage() _assertNumbersAre(range(107-30, 107-15)) tdm.firstPage() _assertNumbersAre(range(15)) tdm.resort('number', False) tdm.firstPage() _assertNumbersAre(list(reversed(range(107-15, 107)))) tdm.lastPage() _assertNumbersAre(list(reversed(range(15)))) def testPageUnderrun(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) _assertNumbersAre = lambda seq: self.assertNumbersAre(tdm, seq) _assertNumbersAre(range(15)) # go to the pre-penultimate page for i in xrange(5): tdm.nextPage() _assertNumbersAre(range(15*5, (15*5)+15)) tdm.nextPage() lastFullSet = range(15*6, (15*6)+15) _assertNumbersAre(lastFullSet) tdm.nextPage() lastSet = [105, 106] _assertNumbersAre(lastSet) def testItemUnderrun(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=110) assertFirstPage = lambda: self.assertNumbersAre(tdm, range(107)) assertFirstPage() tdm.nextPage() assertFirstPage() tdm.prevPage() assertFirstPage() tdm.lastPage() assertFirstPage() tdm.firstPage() assertFirstPage() def testTwoPageNextLastEquality(self): tdm = tdb.TabularDataModel(self.store, X, [X.number], itemsPerPage=100) assertFirstPage = lambda: self.assertNumbersAre(tdm, range(100)) assertSecondPage = lambda: self.assertNumbersAre(tdm, range(100, 107)) assertFirstPage() tdm.nextPage() assertSecondPage() tdm.firstPage() assertFirstPage() tdm.lastPage() assertSecondPage() testTwoPageNextLastEquality.todo = 'is this a bug? it seems like one to me' def testPagination(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) self.assertEquals(tdm.pageNumber, 1) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 2) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 3) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 4) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 5) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 6) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 7) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.nextPage() self.assertEquals(tdm.pageNumber, 8) self.assertEquals(tdm.totalItems, 107) self.assertEquals(tdm.totalPages, 8) tdm.lastPage() self.assertEquals(tdm.pageNumber, 8) self.assertEquals(tdm.totalPages, 8) self.assertEquals(tdm.totalItems, 107) tdm.firstPage() self.assertEquals(tdm.pageNumber, 1) self.assertEquals(tdm.totalPages, 8) self.assertEquals(tdm.totalItems, 107) def testSortItemUnderrun(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=110) assertFirstPage = lambda: self.assertNumbersAre(tdm, range(107)) assertFirstPage() tdm.resort(tdm.currentSortColumn.attributeID) assertFirstPage() tdm.resort(tdm.currentSortColumn.attributeID, False) self.assertNumbersAre(tdm, list(reversed(range(107)))) tdm.resort(tdm.currentSortColumn.attributeID) assertFirstPage() tdm.resort(tdm.currentSortColumn.attributeID, True) assertFirstPage() def testChangeSortColumnItemUnderrun(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=110) assertFirstPage = lambda: self.assertNumbersAre(tdm, range(107)) assertFirstPage() tdm.resort(tdm.currentSortColumn.attributeID, True) assertFirstPage() # change sort direction, keep column tdm.resort(tdm.currentSortColumn.attributeID, False) self.assertNumbersAre(tdm, list(reversed(range(107)))) # change sort column and direction tdm.resort('phoneticDigits', True) # switch back to previous sort column & direction tdm.resort('number', False) self.assertNumbersAre(tdm, list(reversed(range(107)))) msg = 'itemsPerPage > totalItems, should only have one page' self.failIf(tdm.hasPrevPage(), msg) self.failIf(tdm.hasNextPage(), msg) def testOnePageHasNextPrev(self): bigtdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=200) bigtdm.nextPage() bigtdm.nextPage() self.failIf(bigtdm.hasNextPage()) self.failIf(bigtdm.hasPrevPage()) def testMultiPageHasNextPrev(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) self.failIf(tdm.hasPrevPage()) self.failUnless(tdm.hasNextPage()) tdm.nextPage() self.failUnless(tdm.hasNextPage()) self.failUnless(tdm.hasPrevPage()) tdm.prevPage() self.failIf(tdm.hasPrevPage()) self.failUnless(tdm.hasNextPage()) tdm.lastPage() self.failIf(tdm.hasNextPage()) self.failUnless(tdm.hasPrevPage()) tdm.prevPage() self.failUnless(tdm.hasNextPage()) self.failUnless(tdm.hasPrevPage()) tdm.firstPage() for x in range(6): tdm.nextPage() self.failUnless(tdm.hasPrevPage()) self.failUnless(tdm.hasNextPage()) tdm.nextPage() self.failUnless(tdm.hasPrevPage()) self.failIf(tdm.hasNextPage()) def testCurrentPageDoesntChange(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) for y in range(3): tdm.nextPage() example = tdm.currentPage() for x in range(3): self.assertEquals(example, tdm.currentPage()) def testDeleteItems(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) r = tdm.currentPage() r0 = r[0]['__item__'] r0.deleteFromStore() rx = tdm.currentPage() r1 = rx[0]['__item__'] self.failIf(r1._Item__deleting, "First item in current page was deleted!") r1.deleteFromStore() tdm.nextPage() self.assertNumbersAre(tdm, range(16, 16+15)) tdm.prevPage() self.assertNumbersAre(tdm, range(2, 2+15)) # The distinction between this test and the nextPage test above is # important: we never "skip" items when paging. The current results of # the TDM represent *what the user has seen*. In the previous case, we # have never called currentPage and received a result with #16 in it # before calling nextPage. In this case, assertNumbersAre(2,...) has # "seen" the page with #16 on it, so we are (correctly) taken to the # next item after that to begin the next page. tdm.nextPage() self.assertNumbersAre(tdm, range(17, 17+15)) def testSorting(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, X.textNumber, X.phoneticDigits], itemsPerPage=15) _assertNumbersAre = lambda seq: self.assertNumbersAre(tdm, seq) tdm.resort('number', True) _assertNumbersAre(range(15)) tdm.resort('number', False) _assertNumbersAre(list(reversed(range(15)))) _assertNumbersAre(list(reversed(range(15)))) tdm.nextPage() _assertNumbersAre(list(reversed(range(15)))) def testUnsortableColumns(self): tdm = tdb.TabularDataModel(self.store, X, [X.number, UnsortableColumn(X.textNumber), UnsortableColumn(X.phoneticDigits)], itemsPerPage=15) self.assertNumbersAre(tdm, range(15)) self.assertRaises(scrolltable.Unsortable, lambda: tdm.resort('textNumber')) self.assertRaises(scrolltable.Unsortable, lambda: tdm.resort('phoneticDigits')) # check to see if the last valid state remains self.assertNumbersAre(tdm, range(15)) PK9Fdp+@H@Hxmantissa/test/test_terminal.py# Copyright 2009 Divmod, Inc. See LICENSE file for details """ Tests for L{xmantissa.terminal}. """ from zope.interface import implements from zope.interface.verify import verifyObject from twisted.internet.protocol import ProcessProtocol from twisted.trial.unittest import TestCase from twisted.cred.credentials import UsernamePassword from twisted.conch.interfaces import IConchUser, ISession from twisted.conch.ssh.keys import Key from twisted.conch.ssh.session import SSHSession from twisted.conch.insults.helper import TerminalBuffer from twisted.conch.insults.insults import ServerProtocol from twisted.conch.manhole import ColoredManhole from axiom.store import Store from axiom.item import Item from axiom.attributes import text, inmemory from axiom.dependency import installOn from axiom.userbase import LoginSystem, LoginMethod from xmantissa.ixmantissa import IProtocolFactoryFactory, ITerminalServerFactory from xmantissa.ixmantissa import IViewer from xmantissa.sharing import getSelfRole from xmantissa.terminal import SecureShellConfiguration, TerminalManhole from xmantissa.terminal import ShellAccount, ShellServer, _ReturnToMenuWrapper from xmantissa.terminal import _AuthenticatedShellViewer class SecureShellConfigurationTests(TestCase): """ Tests for L{xmantissa.shell.SecureShellConfiguration} which defines how to create an SSH server. """ _hostKey = ( "-----BEGIN RSA PRIVATE KEY-----\n" "MIIByAIBAAJhAM/dftm59mJJ1JVy0bsq8J7fp4WUecgaJukRyf637d76ywxRYGdw\n" "47hkBiJaDYgaE9HMlh2eSow3b2YCyom4FLlh/7Buq58A9IofR7ZiNVYv0ZDppbDg\n" "FN+Gl2ZFLFB3dwIBIwJgC+DFa4b4It+lv2Wllaquqf4m1G7iYzSxxCzm+JzLw5lN\n" "bmsM0rX+Yk7bx3LcM6m34vyvhY6p/kQyjHo7/CkpaSQg4bnpOcqEq3oMf8E0c0lp\n" "TQ1TdtfnKKrZZPTaVr7rAjEA7O19/tSLK6by1BpE1cb6W07GK1WcafYLxQLT64o+\n" "GKxbrlsossc8gWJ8GDRjE2S5AjEA4JkYfYkgfucH941r9yDFrhr6FuOdwbLXDESZ\n" "DyLhW/7DHiVIXlaLFnY+51PcTwWvAjBzFESDFsdBFpMz0j6w+j8WaBccXMhQuVYs\n" "fbdjxs20NnWsdWuKCQAhljxGRVSxpfMCMBmrGL3jyTMTFtp2j/78bl0KZbl5GVf3\n" "LoUPJ29xs1r4i1PnAPTWsM9d+I93TGDNcwIxAMRz4KO02tiLXG2igwDw/WWszrkr\n" "r4ggaFDlt4QqoNz0l4tayqzbDV1XceLgP4cXcQ==\n" "-----END RSA PRIVATE KEY-----\n") def setUp(self): """ Create an in-memory L{Store} with a L{SecureShellConfiguration} in it. """ self.store = Store() self.shell = SecureShellConfiguration( store=self.store, hostKey=self._hostKey) installOn(self.shell, self.store) def test_interfaces(self): """ L{SecureShellConfiguration} implements L{IProtocolFactoryFactory}. """ self.assertTrue(verifyObject(IProtocolFactoryFactory, self.shell)) def test_powerup(self): """ L{installOn} powers up the target for L{IProtocolFactoryFactory} with L{SecureShellConfiguration}. """ self.assertIn( self.shell, list(self.store.powerupsFor(IProtocolFactoryFactory))) def test_repr(self): """ The result of C{repr} on a L{SecureShellConfiguration} instance includes only a fingerprint of the private key, not the entire value. """ self.assertEqual( repr(self.shell), "SecureShellConfiguration(storeID=%d, " % (self.shell.storeID,) + "hostKeyFingerprint='68cc7060bb6394060672467e7c4d8f3b')") def assertHostKey(self, shell, factory): """ Assert that the public and private keys provided by C{factory} match those specified by C{shell} and that they are L{Key} instances. """ privateKey = Key.fromString(shell.hostKey) self.assertEqual( factory.publicKeys, {'ssh-rsa': privateKey.public()}) self.assertEqual(factory.privateKeys, {'ssh-rsa': privateKey}) def test_getFactory(self): """ L{SecureShellConfiguration.getFactory} returns an L{SSHFactory} with keys from L{SecureShellConfiguration.hostKey}. """ factory = self.shell.getFactory() self.assertHostKey(self.shell, factory) def test_keyGeneration(self): """ L{SecureShellConfiguration} generates its own key pair if one is not supplied to C{__init__}. """ store = Store() shell = SecureShellConfiguration(store=store) installOn(shell, store) factory = shell.getFactory() self.assertHostKey(shell, factory) def test_portal(self): """ The factory returned by L{SecureShellConfiguration.getFactory} has a C{portal} attribute which allows logins authenticated in the usual L{axiom.userbase} manner. """ localpart = u'foo bar' domain = u'example.com' password = u'baz quux' loginSystem = self.store.findUnique(LoginSystem) account = loginSystem.addAccount( localpart, domain, password, internal=True) subStore = account.avatars.open() avatar = object() subStore.inMemoryPowerUp(avatar, IConchUser) factory = self.shell.getFactory() login = factory.portal.login( UsernamePassword( '%s@%s' % (localpart.encode('ascii'), domain.encode('ascii')), password), None, IConchUser) def cbLoggedIn(result): self.assertIdentical(IConchUser, result[0]) self.assertIdentical(avatar, result[1]) login.addCallback(cbLoggedIn) return login class AuthenticatedShellViewerTests(TestCase): """ Tests for L{_AuthenticatedShellViewer}, an L{IViewer} implementation for use with L{ITerminalServerFactory.buildTerminalProtocol}. """ def test_interface(self): """ L{_AuthenticatedShellViewer} instances provide L{IViewer}. """ self.assertTrue(verifyObject(IViewer, _AuthenticatedShellViewer([]))) def test_roleIn(self): """ L{_AuthenticatedShellViewer.roleIn} returns a L{Role} for one of the account names passed to L{_AuthenticatedShellViewer.__init__}. """ store = Store() viewer = _AuthenticatedShellViewer([(u"alice", u"example.com")]) role = viewer.roleIn(store) self.assertEquals(role.externalID, u"alice@example.com") self.assertIdentical(role.store, store) class ShellAccountTests(TestCase): """ Tests for L{ShellAccount} which provide a basic L{IConchUser} avatar. """ def setUp(self): """ Create an in-memory L{Store} with a L{ShellAccount} in it. """ self.store = Store() self.account = ShellAccount(store=self.store) installOn(self.account, self.store) def test_interfaces(self): """ L{ShellAccount} powers up the item on which it is installed for L{IConchUser} and the L{IConchUser} powerup is adaptable to L{ISession}. """ avatar = IConchUser(self.store) self.assertTrue(verifyObject(IConchUser, avatar)) session = ISession(avatar) self.assertTrue(verifyObject(ISession, session)) def test_lookupSessionChannel(self): """ L{ShellAccount.lookupChannel} returns an L{SSHSession} instance. (This is because L{SSHSession} implements handlers for the standard SSH requests issued to set up a shell.) """ avatar = IConchUser(self.store) channel = avatar.lookupChannel('session', 65536, 16384, '') self.assertTrue(isinstance(channel, SSHSession)) def test_openShell(self): """ The L{ISession} adapter of the L{IConchUser} powerup implements C{openShell} so as to associate the given L{IProcessProtocol} with a transport. """ proto = ProcessProtocol() session = ISession(IConchUser(self.store)) # XXX See Twisted ticket #3864 proto.session = session proto.write = lambda bytes: None # XXX See #2895. session.getPty(None, (123, 456, 789, 1000), None) session.openShell(proto) self.assertNotIdentical(proto.transport, None) class FakeTerminal(TerminalBuffer): """ A fake implementation of L{ITerminalTransport} used by the L{_ReturnToMenuWrapper} tests. """ disconnected = False def loseConnection(self): self.disconnected = True class ReturnToMenuTests(TestCase): """ Tests for L{_ReturnToMenuWrapper} which wraps an L{ITerminalTransport} for an L{ITerminalProtocol} and switches to another L{ITerminalProtocol} when C{loseConnection} is called on it instead of disconnecting. """ def test_write(self): """ L{_ReturnToMenuWrapper.write} passes through to the wrapped terminal. """ terminal = FakeTerminal() terminal.makeConnection(None) wrapper = _ReturnToMenuWrapper(None, terminal) wrapper.write('some bytes') wrapper.write('some more') self.assertIn('some bytessome more', str(terminal)) def test_loseConnection(self): """ L{_ReturnToMenuWrapper.loseConnection} does not disconnect the terminal; instead it calls the C{reactivate} method of its C{shell} attribute. """ class FakeShell(object): activated = False def reactivate(self): self.activated = True shell = FakeShell() terminal = FakeTerminal() wrapper = _ReturnToMenuWrapper(shell, terminal) wrapper.loseConnection() self.assertFalse(terminal.disconnected) self.assertTrue(shell.activated) class MockTerminalProtocol(object): """ Implementation of L{ITerminalProtocol} used in test L{ShellServer}'s interactions with the interface. @ivar terminal: The L{ITerminalTransport} passed to C{makeConnection}. @ivar keystrokes: A C{list} of two-tuples giving each keystroke which this protocol has received. @ivar disconnected: A C{bool} indicating whether C{connectionLost} has been called yet. """ def __init__(self): self.keystrokes = [] self.terminal = None self.disconnected = False def makeConnection(self, terminal): self.terminal = terminal def connectionLost(self, reason): self.disconnected = True def keystrokeReceived(self, keyID, modifier): self.keystrokes.append((keyID, modifier)) class MockTerminalServerFactory(object): """ Implementation of L{ITerminalServerFactory} used in test L{ShellServer}'s interactions with the interface. @ivar terminalProtocolInstance: The L{MockTerminalServer} created and returned by C{buildTerminalProtocol}, or C{None} if that method has not been called. """ implements(ITerminalServerFactory) name = "mock" shellViewer = None terminalProtocolInstance = None def buildTerminalProtocol(self, shellViewer): self.shellViewer = shellViewer self.terminalProtocolInstance = MockTerminalProtocol() return self.terminalProtocolInstance # Sanity check - this isn't a comprehensive (or even close) verification of # MockTerminalServerFactory, but it at least points out obvious mistakes. verifyObject(ITerminalServerFactory, MockTerminalServerFactory()) class MockTerminalServerFactoryItem(Item): """ An L{Item} implementation of L{ITerminalServerFactory} used by tests. """ powerupInterfaces = (ITerminalServerFactory,) implements(*powerupInterfaces) name = text() shellViewer = inmemory( doc=""" The L{IViewer} passed to L{buildTerminalProtocol}. """) terminalProtocolInstance = inmemory( doc=""" The L{MockTerminalServer} created and returned by C{buildTerminalProtocol}, or C{None} if that method has not been called. """) def activate(self): self.shellViewer = None self.terminalProtocolInstance = None def buildTerminalProtocol(self, shellViewer): self.shellViewer = shellViewer self.terminalProtocolInstance = MockTerminalProtocol() return self.terminalProtocolInstance # Sanity check - see above call to verifyObject. verifyObject(ITerminalServerFactory, MockTerminalServerFactoryItem()) class ShellServerTests(TestCase): """ Tests for L{ShellServer} which is the top-level L{ITerminalProtocol}, interacting initially and directly with terminals by presenting a menu of possible activities and delegating to other L{ITerminalProtocol}s which appropriate. """ def test_switchTo(self): """ L{ShellServer.switchTo} takes a L{ITerminalServerFactory} and uses it to create a new L{ITerminalProtocol} which it connects to a L{_ReturnToMenuWrapper}. L{buildTerminalProtocol} is passed an L{IViewer}. """ terminal = FakeTerminal() store = Store() # Put a login method into the store so it can have a role. See #2665. LoginMethod( store=store, internal=True, protocol=u'*', verified=True, localpart=u'alice', domain=u'example.com', # Not really an account, but simpler... account=store) server = ShellServer(store) server.makeConnection(terminal) factory = MockTerminalServerFactory() server.switchTo(factory) self.assertIdentical(factory.shellViewer.roleIn(store), getSelfRole(store)) self.assertTrue(isinstance(server._protocol, MockTerminalProtocol)) self.assertTrue(isinstance(server._protocol.terminal, _ReturnToMenuWrapper)) self.assertIdentical(server._protocol.terminal._shell, server) self.assertIdentical(server._protocol.terminal._terminal, terminal) def test_appButtons(self): """ L{ShellServer._appButtons} returns an iterator the elements of which are L{Button} instances, one for each L{ITerminalServerFactory} powerup. When one of these buttons is activated, L{ShellServer} is switched to the corresponding L{ITerminalServerFactory}'s protocol. """ store = Store() terminal = FakeTerminal() server = ShellServer(store) server.makeConnection(terminal) firstFactory = MockTerminalServerFactoryItem( store=store, name=u"first - \N{ROMAN NUMERAL ONE}") installOn(firstFactory, store) secondFactory = MockTerminalServerFactoryItem( store=store, name=u"second - \N{ROMAN NUMERAL TWO}") installOn(secondFactory, store) buttons = list(server._appButtons()) self.assertEqual(len(buttons), 2) # For now, we'll say the order isn't significant. buttons.sort(key=lambda b: b.label) self.assertEqual( buttons[0].label, firstFactory.name.encode('utf-8')) buttons[0].onPress() server.keystrokeReceived('x', None) self.assertEqual( firstFactory.terminalProtocolInstance.keystrokes, [('x', None)]) self.assertEqual( buttons[1].label, secondFactory.name.encode('utf-8')) buttons[1].onPress() server.keystrokeReceived('y', None) self.assertEqual( secondFactory.terminalProtocolInstance.keystrokes, [('y', None)]) def test_logoffButton(self): """ L{ShellServer._logoffButton} returns a L{Button} which, when activated, disconnects the terminal. """ terminal = FakeTerminal() server = ShellServer(Store()) server.makeConnection(terminal) server._logoffButton().onPress() self.assertTrue(terminal.disconnected) def test_reactivate(self): """ L{ShellServer.reactivate} disconnects the protocol previously switched to, drops the reference to it, and redraws the main menu. """ terminal = FakeTerminal() server = ShellServer(Store()) server.makeConnection(terminal) server.switchTo(MockTerminalServerFactory()) server.reactivate() self.assertIdentical(server._protocol, None) def test_keystrokeReceivedWindow(self): """ L{ShellServer.keystrokeReceived} delivers keystroke data to the main menu widget when no protocol has been switched to. """ class FakeWidget(object): def __init__(self): self.keystrokes = [] def keystrokeReceived(self, keyID, modifier): self.keystrokes.append((keyID, modifier)) terminal = FakeTerminal() window = FakeWidget() server = ShellServer(Store()) server._makeWindow = lambda: window server.makeConnection(terminal) server.keystrokeReceived(' ', ServerProtocol.ALT) self.assertEqual(window.keystrokes, [(' ', ServerProtocol.ALT)]) def test_keystrokeReceivedProtocol(self): """ L{ShellServer.keystrokeReceived} delivers keystroke data to the protocol built by the factory which has been switched to. """ factory = MockTerminalServerFactory() terminal = FakeTerminal() server = ShellServer(Store()) server.makeConnection(terminal) server.switchTo(factory) server.keystrokeReceived(' ', ServerProtocol.ALT) self.assertEqual( factory.terminalProtocolInstance.keystrokes, [(' ', ServerProtocol.ALT)]) class ManholeTests(TestCase): """ Tests for L{TerminalManhole} which provides an L{ITerminalServerFactory} for a protocol which gives a user an in-process Python REPL. """ def test_interface(self): """ L{TerminalManhole} implements L{ITerminalServerFactory}. """ self.assertTrue(verifyObject(ITerminalServerFactory, TerminalManhole())) def test_buildTerminalProtocol(self): """ L{TerminalManhole.buildTerminalProtocol} returns a L{ColoredManhole} with a namespace including the store the L{TerminalManhole} is in. """ store = Store() factory = TerminalManhole(store=store) viewer = object() protocol = factory.buildTerminalProtocol(viewer) self.assertTrue(isinstance(protocol, ColoredManhole)) self.assertEqual(protocol.namespace, {'db': store, 'viewer': viewer}) PK9F@S<<xmantissa/test/test_theme.pyfrom zope.interface import implements from zope.interface import classProvides from twisted.trial.unittest import TestCase from twisted.python.reflect import qual from twisted.python.util import sibpath from twisted.python.filepath import FilePath, InsecurePath from nevow.athena import LivePage from nevow.loaders import stan, xmlstr from nevow.tags import ( html, head, body, invisible, directive) from nevow.context import WovenContext from nevow.testutil import FakeRequest from nevow.flat import flatten from nevow.inevow import IRequest from nevow.athena import LiveFragment from axiom.item import Item from axiom.attributes import integer from axiom.store import Store from axiom.substore import SubStore from axiom.dependency import installOn from axiom.plugins.mantissacmd import Mantissa from xmantissa.ixmantissa import ( ITemplateNameResolver, IOfferingTechnician, ISiteURLGenerator) from xmantissa import webtheme from xmantissa.webtheme import ( getInstalledThemes, MantissaTheme, ThemedFragment, ThemedElement, ThemedDocumentFactory, SiteTemplateResolver, XHTMLDirectoryTheme) from xmantissa.offering import Offering, installOffering from xmantissa.plugins.baseoff import baseOffering from xmantissa.publicweb import PublicAthenaLivePage from xmantissa.webapp import GenericNavigationAthenaPage, _PageComponents from xmantissa.test.test_offering import FakeOfferingTechnician from xmantissa.test.validation import XHTMLDirectoryThemeTestsMixin class ThemedDocumentFactoryTests(TestCase): """ Tests for the automatic document factory descriptor, L{ThemedDocumentFactory}. """ def test_getter(self): """ Retrieving the value of a L{ThemedDocumentFactory} descriptor should cause an L{ITemplateNameResolver} to be requested from the supplied callable and a loader for the template for the fragment name the descriptor was created with to be created and returned. """ _docFactory = object() loadAttempts = [] fragmentName = 'abc' class Dummy(object): class StubResolver(object): classProvides(ITemplateNameResolver) def getDocFactory(name): loadAttempts.append(name) return _docFactory getDocFactory = staticmethod(getDocFactory) docFactory = ThemedDocumentFactory(fragmentName, 'StubResolver') self.assertIdentical(Dummy().docFactory, _docFactory) self.assertEqual(loadAttempts, [fragmentName]) class FakeTheme: """ Stub theme object for template-loader tests. """ implements(ITemplateNameResolver) def __init__(self, name, priority): self.name = name self.priority = priority def getDocFactory(self, n, default): """ Doesn't have to return anything meaningful, just something recognizable for assertions. """ return [self.name, n] class FakeOffering: def __init__(self, name, priority): self.themes = [FakeTheme(name, priority)] class WebThemeTestCase(TestCase): def _render(self, element): """ Put the given L{IRenderer} provider into an L{athena.LivePage} and render it. Return a Deferred which fires with the request object used which is an instance of L{nevow.testutil.FakeRequest}. """ p = LivePage( docFactory=stan( html[ head(render=directive('liveglue')), body[ invisible(render=lambda ctx, data: element)]])) element.setFragmentParent(p) ctx = WovenContext() req = FakeRequest() ctx.remember(req, IRequest) d = p.renderHTTP(ctx) def rendered(ign): p.action_close(None) return req d.addCallback(rendered) return d def test_getAllThemesPrioritization(self): """ Test that the L{xmantissa.webtheme.getAllThemes} function returns L{ITemplateNameResolver} providers from the installed L{xmantissa.ixmantissa.IOffering} plugins in priority order. """ lastPriority = None for theme in webtheme.getAllThemes(): if lastPriority is None: lastPriority = theme.priority else: self.failIf( theme.priority > lastPriority, "Theme out of order: %r" % (theme,)) lastPriority = theme.priority def test_getInstalledThemes(self): """ Test that only themes which belong to offerings installed on a particular store are returned by L{xmantissa.webtheme.getInstalledThemes}. """ s = Store() self.assertEquals(getInstalledThemes(s), []) installOffering(s, baseOffering, {}) installedThemes = getInstalledThemes(s) self.assertEquals(len(installedThemes), 1) self.failUnless(isinstance(installedThemes[0], MantissaTheme)) def _defaultThemedRendering(self, cls): class ThemedSubclass(cls): pass d = self._render(ThemedSubclass()) def rendered(req): self.assertIn( qual(ThemedSubclass), req.v) self.assertIn( 'specified no fragmentName attribute.', req.v) d.addCallback(rendered) return d def test_themedFragmentDefaultRendering(self): """ Test that a ThemedFragment which does not override fragmentName is rendered with some debugging tips. """ return self._defaultThemedRendering(ThemedFragment) def test_themedElementDefaultRendering(self): """ Test that a ThemedElement which does not override fragmentName is rendered with some debugging tips. """ return self._defaultThemedRendering(ThemedElement) class MantissaThemeTests(XHTMLDirectoryThemeTestsMixin, TestCase): """ Stock L{XHTMLDirectoryTheme} tests applied to L{baseOffering} and its theme. """ offering = baseOffering theme = offering.themes[0] CUSTOM_MSG = xmlstr('
Athena unsupported here
') BASE_MSG = file(sibpath(__file__, "../themes/base/athena-unsupported.html") ).read().strip() class StubThemeProvider(Item): """ Trivial implementation of a theme provider, for testing that custom Athena-unsupported pages can be used. """ _attribute = integer(doc="exists to pacify Axiom's hunger for attributes") implements(ITemplateNameResolver) powerupInterfaces = (ITemplateNameResolver,) def getDocFactory(self, name): """ Return the page indicating Athena isn't available, if requested. """ if name == 'athena-unsupported': return CUSTOM_MSG class AthenaUnsupported(TestCase): """ Tests for proper treatment of browsers that don't support Athena. """ def setUp(self): self.siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(self.siteStore, u"example.com", u"", False) def test_publicPage(self): """ Test that L{publicpage.PublicAthenaLivePage} supports themeing of Athena's unsupported-browser page. """ stp = StubThemeProvider(store=self.siteStore) installOn(stp, self.siteStore) p = PublicAthenaLivePage(self.siteStore, None) self.assertEqual(p.renderUnsupported(None), flatten(CUSTOM_MSG)) def test_navPage(self): """ Test that L{webapp.GenericNavigationLivePage} supports theming of Athena's unsupported-browser page based on an L{ITemplateNameResolver} installed on the viewing user's store. """ subStore = SubStore.createNew( self.siteStore, ['athena', 'unsupported']).open() stp = StubThemeProvider(store=subStore) installOn(stp, subStore) p = GenericNavigationAthenaPage(stp, LiveFragment(), _PageComponents([], None, None, None, None), None) self.assertEqual(p.renderUnsupported(None), flatten(CUSTOM_MSG)) class Loader(TestCase): def setUp(self): self._getAllThemes = webtheme.getAllThemes self.gATcalled = 0 def fakeGetAllThemes(): self.gATcalled += 1 return [FakeTheme('foo', 7), FakeTheme('baz', 2)] webtheme._loaderCache.clear() webtheme.getAllThemes = fakeGetAllThemes def tearDown(self): webtheme.getAllThemes = self._getAllThemes def test_getLoader(self): """ getLoader should search available themes for the named template and return it. """ self.assertEquals(webtheme.getLoader('template'), ['foo', 'template']) def test_getLoaderCaching(self): """ getLoader should return identical loaders for equal arguments. """ self.assertIdentical(webtheme.getLoader('template'), webtheme.getLoader('template')) self.assertEqual(self.gATcalled, 1) class TestSiteTemplateResolver(TestCase): """ Tests for L{SiteTemplateResolver} """ def setUp(self): """ Create a L{Store} with a fake L{IOfferingTechnician} powerup which allows fine-grained control of template name resolution. """ self.offeringTech = FakeOfferingTechnician() self.store = Store() self.store.inMemoryPowerUp(self.offeringTech, IOfferingTechnician) self.siteResolver = SiteTemplateResolver(self.store) def getDocFactoryWithoutCaching(self, templateName): """ Use C{self.siteResolver} to get a loader for the named template, flushing the template cache first in order to make the result reflect any changes which in offering or theme availability which may have happened since the last call. """ webtheme.theThemeCache.emptyCache() return self.siteResolver.getDocFactory(templateName) def test_getDocFactory(self): """ L{SiteTemplateResolver.getDocFactory} should return only installed themes for its store. """ class FakeTheme(object): priority = 0 def getDocFactory(self, templateName, default=None): if templateName == 'shell': return object() return default self.assertIdentical(self.getDocFactoryWithoutCaching('shell'), None) self.offeringTech.installOffering( Offering( u'an offering', None, [], [], [], [], [FakeTheme()])) self.assertNotIdentical(self.getDocFactoryWithoutCaching('shell'), None) class XHTMLDirectoryThemeTests(TestCase): """ Tests for L{XHTMLDirectoryTheme}. """ def setUp(self): """ Set up the store, a temporary test dir and a theme for the tests. """ self.store = Store() self.testDir = FilePath(self.mktemp()) self.testDir.makedirs() self.theme = XHTMLDirectoryTheme( 'testtheme', directoryName=self.testDir.path) def test_directoryAttribute(self): """ L{XHTMLDirectoryTheme} should have a directory attribute of type L{twisted.python.filepath.FilePath}. """ self.assertEqual(self.theme.directory, self.testDir) self.assertEqual(self.theme.directory.path, self.theme.directoryName) def test_childFragmentsInGetDocFactory(self): """ L{XHTMLDirectoryTheme.getDocFactory} should handle subdirectories sanely, without exposing parent directories. """ fragmentName = 'dir/file' child = self.testDir.child('dir') child.makedirs() child.child('file.html').touch() resolvedTemplate = self.theme.getDocFactory(fragmentName) foundPath = FilePath(resolvedTemplate.template) expectedPath = FilePath( "%s/%s.html" % (self.theme.directoryName, fragmentName)) self.assertEqual(foundPath, expectedPath) self.assertRaises(InsecurePath, self.theme.getDocFactory, '../insecure/') def test_noStylesheetLocation(self): """ L{XHTMLDirectoryTheme.head} returns C{None} if I{stylesheetLocation} is C{None}. """ self.assertIdentical(self.theme.head(None, None), None) def test_stylesheetLocation(self): """ L{XHTMLDirectoryTheme.head} returns a link tag which gives the location of the stylesheet given by I{stylesheetLocation} if there is one. """ siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(siteStore, u"example.com", u"", False) site = ISiteURLGenerator(siteStore) self.theme.stylesheetLocation = ['foo', 'bar'] request = FakeRequest() link = self.theme.head(request, site) self.assertEqual(link.tagName, 'link') self.assertEqual(link.attributes['rel'], 'stylesheet') self.assertEqual(link.attributes['type'], 'text/css') self.assertEqual( site.rootURL(request).child('foo').child('bar'), link.attributes['href']) class TestThemeCache(TestCase): """ some tests for L{ThemeCache}. """ def setUp(self): """ Replace L{getOfferings} with a mock method returning some fake offerings. """ self._getOfferings = webtheme.getOfferings self.called = 0 def fakeGetOfferings(): self.called += 1 return [FakeOffering('foo', 7), FakeOffering('baz', 2), FakeOffering('boz', 5)] webtheme.getOfferings = fakeGetOfferings def tearDown(self): """ Reset L{getOfferings} to its original value. """ webtheme.getOfferings = self._getOfferings def test_getAllThemes(self): """ C{getAllThemes} should collect themes from available offerings, and only call C{getOfferings} once no matter how many times it's called. """ tc = webtheme.ThemeCache() ths = tc.getAllThemes() self.assertEqual([theme.name for theme in ths], ['foo', 'boz', 'baz']) tc.getAllThemes() self.assertEqual(self.called, 1) def test_realGetAllThemes(self): """ C{_realGetAllThemes} should collect themes from available offerings. """ tc = webtheme.ThemeCache() ths = tc.getAllThemes() self.assertEqual([theme.name for theme in ths], ['foo', 'boz', 'baz']) def test_clearThemeCache(self): """ C{emptyCache} should invalidate the cache contents for both types. """ tc = webtheme.ThemeCache() s = Store() tc.getAllThemes() tc.getInstalledThemes(s) tc.emptyCache() self.assertEqual(tc._getAllThemesCache, None) self.assertEqual(len(tc._getInstalledThemesCache), 0) PK9FS$a=a=xmantissa/test/test_webapp.pyfrom zope.interface import implements from twisted.trial.unittest import TestCase from twisted.internet import defer from epsilon.structlike import record from axiom.store import Store from axiom.item import Item from axiom.attributes import integer from axiom.substore import SubStore from axiom.dependency import installOn from axiom.plugins.axiom_plugins import Create from axiom.plugins.mantissacmd import Mantissa from nevow.athena import LiveElement from nevow import rend from nevow.rend import WovenContext from nevow.testutil import FakeRequest from nevow.inevow import IRequest, IResource from xmantissa.ixmantissa import ( ITemplateNameResolver, ISiteURLGenerator, IWebViewer, INavigableElement) from xmantissa.offering import InstalledOffering from xmantissa.webtheme import theThemeCache from xmantissa.webnav import Tab from xmantissa.sharing import (getSelfRole, getAuthenticatedRole, getPrimaryRole) from xmantissa.webapp import ( PrivateApplication, _AuthenticatedWebViewer, _PrivateRootPage, GenericNavigationAthenaPage) from xmantissa.test.test_publicweb import AuthenticatedNavigationTestMixin from xmantissa.test.fakes import ( FakeTheme, FakeCustomizableElementModel, FakeLoader, ElementViewForFakeModelWithTheme, FakeElementModelWithTheme) from xmantissa.test.test_webshell import WebViewerTestMixin class AuthenticatedWebViewerTests(WebViewerTestMixin, TestCase): """ Tests for L{_AuthenticatedWebViewer}. """ def setupPageFactory(self): """ Create the page factory used by the tests. """ self.privapp = self.adminStore.findUnique(PrivateApplication) self.pageFactory = _AuthenticatedWebViewer(self.privapp) def assertPage(self, page): """ Fail if the given object is not a nevow L{rend.Page} (or if it is an L{LivePage}). Also fail if the username is wrong. """ WebViewerTestMixin.assertPage(self, page) self.assertEqual(page.username, u'admin@localhost') def assertLivePage(self, page): """ Fail if the given object is not a L{MantissaLivePage}, or if the username is wrong. """ WebViewerTestMixin.assertLivePage(self, page) self.assertEqual(page.username, u'admin@localhost') def test_docFactoryFromFragmentNameWithPreference(self): """ When an L{INavigableFragment} provider provides a C{fragmentName} attribute, the theme to load it should be discovered according to the user's preference. """ preferredDocFactory = FakeLoader('good') otherDocFactory = FakeLoader('bad') fn = ElementViewForFakeModelWithTheme.fragmentName self.stubThemeList( [FakeTheme(u'alpha', {fn: otherDocFactory}), FakeTheme(u'beta', {fn: preferredDocFactory})]) self.privapp.preferredTheme = u'beta' elementable = FakeElementModelWithTheme() result = self.pageFactory.wrapModel(elementable) self.assertEqual(result.fragment.docFactory, preferredDocFactory) def test_customizableCustomizeFor(self): """ L{INavigableFragment} providers who have a 'customizeFor' method will have it called with a username when they are wrapped for authenticated rendering. """ elementable = FakeCustomizableElementModel() result = self.pageFactory.wrapModel(elementable) self.assertEqual(elementable.username, u'admin@localhost') def test_roleInMyStore(self): """ L{_AuthenticatedWebViewer} should always return the 'self' role for users looking at their own stores. """ role = getSelfRole(self.adminStore) self.assertIdentical(self.pageFactory.roleIn(self.adminStore), role) def test_roleInSomebodyElsesStoreWhoDoesntKnowMe(self): """ L{_AuthenticatedWebViewer} should return the authenticated role for users with no specific role to map. """ someStore = self.loginSystem.addAccount( u'someguy', u'localhost', u'asdf').avatars.open() role = getAuthenticatedRole(someStore) self.assertIdentical(self.pageFactory.roleIn(someStore), role) def test_roleInSomebodyElsesStoreDoesKnowMe(self): """ L{_AuthenticatedWebViewer} should return the authenticated role for users with no specific role to map. """ someStore = self.loginSystem.addAccount( u'someguy', u'localhost', u'asdf').avatars.open() role = getPrimaryRole(someStore, u'admin@localhost', True) self.assertIdentical(self.pageFactory.roleIn(someStore), role) class FakeResourceItem(Item): unused = integer() implements(IResource) class FakeModelItem(Item): unused = integer() class WebIDLocationTest(TestCase): def setUp(self): store = Store() ss = SubStore.createNew(store, ['test']).open() self.pa = PrivateApplication(store=ss) installOn(self.pa, ss) self.webViewer = IWebViewer(ss) def test_powersUpTemplateNameResolver(self): """ L{PrivateApplication} implements L{ITemplateNameResolver} and should power up the store it is installed on for that interface. """ self.assertIn( self.pa, self.pa.store.powerupsFor(ITemplateNameResolver)) def test_suchWebID(self): """ Verify that retrieving a webID gives the correct resource. """ i = FakeResourceItem(store=self.pa.store) wid = self.pa.toWebID(i) ctx = FakeRequest() res = self.pa.createResourceWith(self.webViewer) self.assertEqual(res.locateChild(ctx, [wid]), (i, [])) def test_noSuchWebID(self): """ Verify that non-existent private URLs generate 'not found' responses. """ ctx = FakeRequest() for segments in [ # something that looks like a valid webID ['0000000000000000'], # something that doesn't ["nothing-here"], # more than one segment ["two", "segments"]]: res = self.pa.createResourceWith(self.webViewer) self.assertEqual(res.locateChild(ctx, segments), rend.NotFound) def test_webIDForFragment(self): """ Retrieving a webID that specifies a fragment gives the correct resource. """ class FakeView(record("model")): "A fake view that wraps a FakeModelItem." class FakeWebViewer(object): def wrapModel(self, model): return FakeView(model) i = FakeModelItem(store=self.pa.store) wid = self.pa.toWebID(i) ctx = FakeRequest() res = self.pa.createResourceWith(FakeWebViewer()) child, segs = res.locateChild(ctx, [wid]) self.assertIsInstance(child, FakeView) self.assertIdentical(child.model, i) class TestElement(LiveElement): def head(self): pass def locateChild(self, ctx, segs): if segs[0] == 'child-of-fragment': return ('I AM A CHILD OF THE FRAGMENT', segs[1:]) return rend.NotFound class TestClientFactory(object): """ Dummy L{LivePageFactory}. @ivar magicSegment: The segment for which to return L{returnValue} from L{getClient}. @type magicSegment: C{str} @ivar returnValue: The value to return from L{getClient} when it is passed L{magicSegment}. @type returnValue: C{str}. """ def __init__(self, magicSegment, returnValue): self.magicSegment = magicSegment self.returnValue = returnValue def getClient(self, seg): if seg == self.magicSegment: return self.returnValue class GenericNavigationAthenaPageTests(TestCase, AuthenticatedNavigationTestMixin): """ Tests for L{GenericNavigationAthenaPage}. """ def setUp(self): """ Set up a site store, user store, and page instance to test with. """ self.siteStore = Store(filesdir=self.mktemp()) def siteStoreTxn(): Mantissa().installSite(self.siteStore, u"localhost", u"", False) self.userStore = SubStore.createNew( self.siteStore, ['child', 'lookup']).open() self.siteStore.transact(siteStoreTxn) def userStoreTxn(): self.privateApp = PrivateApplication(store=self.userStore) installOn(self.privateApp, self.userStore) self.navpage = self.createPage(None) self.userStore.transact(userStoreTxn) def createPage(self, username): """ Create a L{GenericNavigationAthenaPage} for the given user. """ return GenericNavigationAthenaPage( self.privateApp, TestElement(), self.privateApp.getPageComponents(), username) def rootURL(self, request): """ Return the root URL as reported by C{self.website}. """ return ISiteURLGenerator(self.siteStore).rootURL(request) def test_childLookup(self): """ L{GenericNavigationAthenaPage} should delegate to its fragment and its L{LivePageFactory} when it cannot find a child itself. """ self.navpage.factory = tcf = TestClientFactory( 'client-of-livepage', 'I AM A CLIENT OF THE LIVEPAGE') self.assertEqual(self.navpage.locateChild(None, ('child-of-fragment',)), ('I AM A CHILD OF THE FRAGMENT', ())) self.assertEqual(self.navpage.locateChild(None, (tcf.magicSegment,)), (tcf.returnValue, ())) def test_jsModuleLocation(self): """ L{GenericNavigationAthenaPage.beforeRender} should should call L{xmantissa.website.MantissaLivePage.beforeRender}, which shares its Athena JavaScript module location with all other pages that use L{xmantissa.cachejs}, and provide links to /__jsmodule__/. """ ctx = WovenContext() req = FakeRequest() ctx.remember(req, IRequest) self.navpage.beforeRender(ctx) urlObj = self.navpage.getJSModuleURL('Mantissa') self.assertEqual(urlObj.pathList()[0], '__jsmodule__') def test_beforeRenderDelegation(self): """ L{GenericNavigationAthenaPage.beforeRender} should call C{beforeRender} on the wrapped fragment, if it's defined, and return its result. """ contexts = [] result = defer.succeed(None) def beforeRender(ctx): contexts.append(ctx) return result self.navpage.fragment.beforeRender = beforeRender ctx = WovenContext() ctx.remember(FakeRequest(), IRequest) self.assertIdentical( self.navpage.beforeRender(ctx), result) self.assertEqual(contexts, [ctx]) class PrivateApplicationTestCase(TestCase): """ Tests for L{PrivateApplication}. """ def setUp(self): self.siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(self.siteStore, u"example.com", u"", False) self.userAccount = Create().addAccount( self.siteStore, u'testuser', u'example.com', u'password') self.userStore = self.userAccount.avatars.open() self.privapp = PrivateApplication(store=self.userStore) installOn(self.privapp, self.userStore) self.webViewer = IWebViewer(self.userStore) def test_createResourceUsername(self): """ L{PrivateApplication.createResourceWith} should figure out the right username and pass it to L{_PrivateRootPage}. """ rootPage = self.privapp.createResourceWith(self.webViewer) self.assertEqual(rootPage.username, u'testuser@example.com') def test_getDocFactory(self): """ L{PrivateApplication.getDocFactory} finds a document factory for the specified template name from among the installed themes. """ # Get something from the Mantissa theme self.assertNotIdentical(self.privapp.getDocFactory('shell'), None) # Get rid of the Mantissa offering and make sure the template is no # longer found. self.siteStore.query(InstalledOffering).deleteFromStore() # And flush the cache. :/ -exarkun theThemeCache.emptyCache() self.assertIdentical(self.privapp.getDocFactory('shell'), None) def test_powersUpWebViewer(self): """ L{PrivateApplication} should provide an indirected L{IWebViewer} powerup, and its indirected powerup should be the default provider of that interface. """ webViewer = IWebViewer(self.privapp.store) self.assertIsInstance(webViewer, _AuthenticatedWebViewer) self.assertIdentical(webViewer._privateApplication, self.privapp) def test_producePrivateRoot(self): """ L{PrivateApplication.produceResource} should return a L{_PrivateRootPage} when asked for '/private'. """ rsrc, segments = self.privapp.produceResource(FakeRequest(), tuple(['private']), None) self.assertIsInstance(rsrc, _PrivateRootPage) self.assertEqual(segments, ()) def test_produceRedirect(self): """ L{_PrivateRootPage.produceResource} should return a redirect to '/private/' when asked for '/'. This is a bad way to do it, because it isn't optional; all logged-in users are instantly redirected to their private page, even if the application has something interesting to display. See ticket #2708 for details. """ item = FakeModelItem(store=self.userStore) class TabThingy(object): implements(INavigableElement) def getTabs(self): return [Tab("supertab", item.storeID, 1.0)] tt = TabThingy() self.userStore.inMemoryPowerUp(tt, INavigableElement) rsrc, segments = self.privapp.produceResource( FakeRequest(), tuple(['']), None) self.assertIsInstance(rsrc, _PrivateRootPage) self.assertEqual(segments, tuple([''])) url, newSegs = rsrc.locateChild(FakeRequest(), ('',)) self.assertEqual(newSegs, ()) req = FakeRequest() target = self.privapp.linkTo(item.storeID) self.assertEqual('/'+url.path, target) def test_produceNothing(self): """ L{_PrivateRootPage.produceResource} should return None when asked for a resources other than '/' and '/private'. """ self.assertIdentical( self.privapp.produceResource(FakeRequest(), tuple(['hello', 'world']), None), None) def test_privateRootHasWebViewer(self): """ The L{_PrivateRootPage} returned from L{PrivateApplication.produceResource} should refer to an L{IWebViewer}. """ webViewer = object() rsrc, segments = self.privapp.produceResource( FakeRequest(), tuple(['private']), webViewer) self.assertIdentical(webViewer, rsrc.webViewer) PK9F]xmantissa/test/test_webcmd.py import os, sys from cStringIO import StringIO from twisted.python.usage import UsageError from twisted.trial.unittest import TestCase from axiom.plugins import webcmd from axiom.store import Store from axiom.test.util import CommandStubMixin from axiom.plugins.mantissacmd import Mantissa from xmantissa.web import SiteConfiguration from xmantissa.website import APIKey def _captureStandardOutput(f, *a, **k): """ Capture standard output produced during the invocation of a function, and return it. Since this is for testing command-line tools, SystemExit errors that indicate a successful return are caught. """ io = StringIO() oldout = sys.stdout sys.stdout = io try: try: f(*a, **k) finally: sys.stdout = oldout except SystemExit, se: if se.args[0]: raise return io.getvalue() class TestIdempotentListing(CommandStubMixin, TestCase): def setUp(self): self.store = Store() self.options = webcmd.WebConfiguration() self.options.parent = self def test_requiresBaseOffering(self): """ L{WebConfiguration.postOptions} raises L{UsageError} if it is used on a store which does not have the Mantissa base offering installed. """ self.assertRaises(UsageError, self.options.postOptions) def _list(self): wconf = webcmd.WebConfiguration() wconf.parent = self wout = _captureStandardOutput(wconf.parseOptions, ['--list']) return wout def testListDoesNothing(self): """ Verify that 'axiomatic -d foo.axiom web --list' does not modify anything, by running it twice and verifying that the generated output is identical the first and second time. """ self.assertEquals(self._list(), self._list()) class ConfigurationTestCase(CommandStubMixin, TestCase): def setUp(self): self.store = Store(filesdir=self.mktemp()) Mantissa().installSite(self.store, u"example.com", u"", False) def test_shortOptionParsing(self): """ Test that the short form of all the supported command line options are parsed correctly. """ opt = webcmd.WebConfiguration() opt.parent = self certFile = self.store.filesdir.child('name') opt.parseOptions(['-h', 'http.log', '-H', 'example.com']) self.assertEquals(opt['http-log'], 'http.log') self.assertEquals(opt['hostname'], 'example.com') def test_longOptionParsing(self): """ Test that the long form of all the supported command line options are parsed correctly. """ opt = webcmd.WebConfiguration() opt.parent = self certFile = self.store.filesdir.child('name') opt.parseOptions([ '--http-log', 'http.log', '--hostname', 'example.com', '--urchin-key', 'A123']) self.assertEquals(opt['http-log'], 'http.log') self.assertEquals(opt['hostname'], 'example.com') self.assertEquals(opt['urchin-key'], 'A123') def test_staticParsing(self): """ Test that the --static option parses arguments of the form "url:filename" correctly. """ opt = webcmd.WebConfiguration() opt.parent = self opt.parseOptions([ '--static', 'foo:bar', '--static', 'quux/fooble:/bar/baz']) self.assertEquals( opt.staticPaths, [('foo', os.path.abspath('bar')), ('quux/fooble', '/bar/baz')]) def test_hostname(self): """ The I{hostname} option changes the C{hostname} attribute of the L{SiteConfiguration} object installed on the store. The hostname cannot be set to the empty string. """ opt = webcmd.WebConfiguration() opt.parent = self opt['hostname'] = 'example.com' opt.postOptions() opt['hostname'] = '' self.assertRaises(UsageError, opt.postOptions) self.assertEqual( self.store.findUnique(SiteConfiguration).hostname, u"example.com") def test_urchinKey(self): """ Specifying a Google Analytics key inserts an item into the database recording it. """ opt = webcmd.WebConfiguration() opt.parent = self opt['urchin-key'] = 'A123' opt.postOptions() self.assertEquals(APIKey.getKeyForAPI(self.store, APIKey.URCHIN).apiKey, u'A123') PK9F},},xmantissa/test/test_webnav.py# Copyright 2007 Divmod, Inc. # See LICENSE file for details """ Tests for L{xmantissa.webnav}. """ from twisted.trial import unittest from epsilon.structlike import record from axiom.store import Store from axiom.dependency import installOn from nevow.url import URL from nevow import tags, context from nevow.testutil import FakeRequest from xmantissa import webnav from xmantissa.webapp import PrivateApplication class FakeNavigator(record('tabs')): def getTabs(self): return self.tabs class NavConfigTests(unittest.TestCase): """ Tests for free functions in L{xmantissa.webnav}. """ def test_tabMerge(self): """ L{webnav.getTabs} should combine tabs from the L{INavigableElement} providers passed to it into a single structure. It should preserve the attributes of all of the tabs and order them and their children by priority. """ nav = webnav.getTabs([ FakeNavigator([webnav.Tab('Hello', 1, 0.5, [webnav.Tab('Super', 2, 1.0, (), False, '/Super/2'), webnav.Tab('Mega', 3, 0.5, (), False, '/Mega/3')], False, '/Hello/1')]), FakeNavigator([webnav.Tab('Hello', 4, 1., [webnav.Tab('Ultra', 5, 0.75, (), False, '/Ultra/5'), webnav.Tab('Hyper', 6, 0.25, (), False, '/Hyper/6')], True, '/Hello/4'), webnav.Tab('Goodbye', 7, 0.9, (), True, '/Goodbye/7')])]) hello, goodbye = nav self.assertEqual(hello.name, 'Hello') self.assertEqual(hello.storeID, 4) self.assertEqual(hello.priority, 1.0) self.assertEqual(hello.authoritative,True) self.assertEqual(hello.linkURL, '/Hello/4') super, ultra, mega, hyper = hello.children self.assertEqual(super.name, 'Super') self.assertEqual(super.storeID, 2) self.assertEqual(super.priority, 1.0) self.assertEqual(super.authoritative, False) self.assertEqual(super.linkURL, '/Super/2') self.assertEqual(ultra.name, 'Ultra') self.assertEqual(ultra.storeID, 5) self.assertEqual(ultra.priority, 0.75) self.assertEqual(ultra.authoritative, False) self.assertEqual(ultra.linkURL, '/Ultra/5') self.assertEqual(mega.name, 'Mega') self.assertEqual(mega.storeID, 3) self.assertEqual(mega.priority, 0.5) self.assertEqual(mega.authoritative, False) self.assertEqual(mega.linkURL, '/Mega/3') self.assertEqual(hyper.name, 'Hyper') self.assertEqual(hyper.storeID, 6) self.assertEqual(hyper.priority, 0.25) self.assertEqual(hyper.authoritative, False) self.assertEqual(hyper.linkURL, '/Hyper/6') self.assertEqual(goodbye.name, 'Goodbye') self.assertEqual(goodbye.storeID, 7) self.assertEqual(goodbye.priority, 0.9) self.assertEqual(goodbye.authoritative, True) self.assertEqual(goodbye.linkURL, '/Goodbye/7') def test_setTabURLs(self): """ Check that L{webnav.setTabURLs} correctly sets the C{linkURL} attribute of L{webnav.Tab} instances to the result of passing tab.storeID to L{xmantissa.ixmantissa.IWebTranslator.linkTo} if C{linkURL} is not set, and that it leaves it alone if it is """ s = Store() privapp = PrivateApplication(store=s) installOn(privapp,s) tabs = [webnav.Tab('PrivateApplication', privapp.storeID, 0), webnav.Tab('Something Else', None, 0, linkURL='/foo/bar')] webnav.setTabURLs(tabs, privapp) self.assertEqual(tabs[0].linkURL, privapp.linkTo(privapp.storeID)) self.assertEqual(tabs[1].linkURL, '/foo/bar') def test_getSelectedTabExactMatch(self): """ Check that L{webnav.getSelectedTab} returns the tab whose C{linkURL} attribute exactly matches the path of the L{nevow.url.URL} it is passed """ tabs = list(webnav.Tab(str(i), None, 0, linkURL='/' + str(i)) for i in xrange(5)) for (i, tab) in enumerate(tabs): selected = webnav.getSelectedTab(tabs, URL.fromString(tab.linkURL)) self.assertIdentical(selected, tab) selected = webnav.getSelectedTab(tabs, URL.fromString('/XYZ')) self.failIf(selected) def test_getSelectedTabPrefixMatch(self): """ Check that L{webnav.getSelectedTab} returns the tab whose C{linkURL} attribute contains the longest prefix of path segments that appears at the beginning of the L{nevow.url.URL} it is passed (if there is not an exact match) """ tabs = [webnav.Tab('thing1', None, 0, linkURL='/a/b/c/d'), webnav.Tab('thing2', None, 0, linkURL='/a/b/c')] def assertSelected(tab): selected = webnav.getSelectedTab(tabs, URL.fromString('/a/b/c/d/e')) self.assertIdentical(selected, tab) assertSelected(tabs[0]) tabs.reverse() assertSelected(tabs[1]) tabs.append(webnav.Tab('thing3', None, 0, linkURL='a/b/c/e/e')) assertSelected(tabs[1]) t = webnav.Tab('thing4', None, 0, linkURL='/a/b/c/d/e') tabs.append(t) assertSelected(t) class FakeTranslator(object): """ A dumb translator which follows a very simple translation rule and can only translate in one direction. """ def linkTo(self, obj): """ Return a fake link based on the given object. """ return '/link/' + str(obj) class RendererTests(unittest.TestCase): """ Tests for certain free functions in L{xmantissa.webnav} which render different things. """ def test_startMenuSetsTabURLs(self): """ L{Tabs} which have C{None} for a C{linkURL} attribute should have a value set for that attribute based on the L{IWebTranslator} passed to L{startMenu}. """ tab = webnav.Tab('alpha', 123, 0) webnav.startMenu(FakeTranslator(), [tab], tags.span()) self.assertEqual(tab.linkURL, '/link/123') def test_startMenuRenders(self): """ Test that the L{startMenu} renderer creates a tag for each tab, filling its I{href}, I{name}, and I{kids} slots. """ tabs = [ webnav.Tab('alpha', 123, 0), webnav.Tab('beta', 234, 0)] node = tags.span[tags.div(pattern='tab')] tag = webnav.startMenu(FakeTranslator(), tabs, node) self.assertEqual(tag.tagName, 'span') navTags = list(tag.slotData['tabs']) self.assertEqual(len(navTags), 2) alpha, beta = navTags self.assertEqual(alpha.slotData['name'], 'alpha') self.assertEqual(alpha.slotData['href'], '/link/123') self.assertEqual(alpha.slotData['kids'], '') self.assertEqual(beta.slotData['name'], 'beta') self.assertEqual(beta.slotData['href'], '/link/234') self.assertEqual(beta.slotData['kids'], '') def test_settingsLink(self): """ L{settingsLink} should add a link to the settings item supplied as a child of the tag supplied. """ self.storeID = 123 node = tags.span() tag = webnav.settingsLink(FakeTranslator(), self, node) self.assertEqual(tag.tagName, 'span') self.assertEqual(tag.children, ['/link/123']) def _renderAppNav(self, tabs, template=None): """ Render application navigation and return the resulting tag. @param template: a Tag containing a template for navigation. """ if template is None: template = tags.span[ tags.div(pattern='app-tab'), tags.div(pattern='tab-contents')] ctx = context.WebContext(tag=template) request = FakeRequest() ctx.remember(request) return webnav.applicationNavigation(ctx, FakeTranslator(), tabs) def test_applicationNavigation(self): """ Test that the L{applicationNavigation} renderer creates a tag for each tab, fillings I{name} and I{tab-contents} slots. """ tag = self._renderAppNav([ webnav.Tab('alpha', 123, 0), webnav.Tab('beta', 234, 0)]) self.assertEqual(tag.tagName, 'span') navTags = list(tag.slotData['tabs']) self.assertEqual(len(navTags), 2) alpha, beta = navTags self.assertEqual(alpha.slotData['name'], 'alpha') alphaContents = alpha.slotData['tab-contents'] self.assertEqual(alphaContents.slotData['href'], '/link/123') self.assertEqual(beta.slotData['name'], 'beta') betaContents = beta.slotData['tab-contents'] self.assertEqual(betaContents.slotData['href'], '/link/234') def test_applicationNavigationChildren(self): """ The L{applicationNavigation} renderer should fill the 'subtabs' slot with copies of the 'subtab' pattern for each tab, if that pattern is present. (This is only tested to one level of depth because we currently only support one level of depth.) """ tag = self._renderAppNav( [webnav.Tab('alpha', 123, 0), webnav.Tab('beta', 234, 0, children=[ webnav.Tab('gamma', 345, 0), webnav.Tab('delta', 456, 0)])], tags.span[tags.div(pattern='app-tab'), tags.div(pattern='tab-contents'), tags.div(pattern='subtab'), tags.div(pattern='subtab-contents', class_='subtab-contents-class')]) navTags = list(tag.slotData['tabs']) self.assertEqual(len(navTags), 2) alpha, beta = navTags self.assertEqual(alpha.slotData['subtabs'], []) self.assertEqual(len(beta.slotData['subtabs']), 2) subtab1 = beta.slotData['subtabs'][0] self.assertEqual(subtab1.slotData['name'], 'gamma') self.assertEqual(subtab1.slotData['href'], '/link/345') self.assertEqual(subtab1.slotData['tab-contents'].attributes['class'], 'subtab-contents-class') subtab2 = beta.slotData['subtabs'][1] self.assertEqual(subtab2.slotData['name'], 'delta') self.assertEqual(subtab2.slotData['href'], '/link/456') self.assertEqual(subtab2.slotData['tab-contents'].attributes['class'], 'subtab-contents-class') def test_applicationNavigationMissingSubtabsPattern(self): """ The L{applicationNavigation} renderer should fill the 'subtabs' slot with the empty list if the 'subtabs' pattern is not found. This is to ensure that it remains compatible with older customized 'shell' templates. """ tag = self._renderAppNav([ webnav.Tab("alpha", 123, 0, children=[webnav.Tab("beta", 234, 0)])]) navTags = list(tag.slotData['tabs']) self.assertEqual(navTags[0].slotData['subtabs'], []) PK9FAA!xmantissa/test/test_websession.py# Copyright 2006-2008 Divmod, Inc. # See LICENSE file for details """ Tests for L{xmantissa.websession}. """ from twisted.trial.unittest import TestCase from nevow.testutil import FakeRequest from xmantissa.websession import PersistentSessionWrapper, usernameFromRequest class TestUsernameFromRequest(TestCase): def test_domainUnspecified(self): """ Test that L{usernameFromRequest} adds the value of host header to the username in the request if the username doesn't already specify a domain. """ request = FakeRequest(headers={'host': 'divmod.com'}) request.args = {'username': ['joe']} username = usernameFromRequest(request) self.assertEqual(username, 'joe@divmod.com') def test_domainSpecified(self): """ Test that L{usernameFromRequest} returns the username in the request if that username specifies a domain. """ request = FakeRequest(headers={'host': 'divmod.com'}) request.args = {'username': ['joe@notdivmod.com']} username = usernameFromRequest(request) self.assertEqual(username, 'joe@notdivmod.com') class TestPersistentSessionWrapper(TestCase): """ Tests for L{PersistentSessionWrapper}. """ def test_savorSessionCookie(self): """ L{PersistentSessionWrapper.savorSessionCookie} adds a cookie with a large maximum age and a request-appropriate domain to the request. """ request = FakeRequest(headers={'host': 'example.com'}) resource = PersistentSessionWrapper( None, None, domains=['example.org', 'example.com']) resource.savorSessionCookie(request) self.assertEqual( request.cookies, {resource.cookieKey: request.getSession().uid}) def _cookieTest(self, host, cookie, **kw): """ Assert that a L{PersistentSessionWrapper} created with the given keyword arguments returns C{cookie} from its C{cookieDomainForRequest} method when passed a request with C{host} as the value for its I{Host} header. """ request = FakeRequest(headers={'host': host}) resource = PersistentSessionWrapper(None, None, **kw) self.assertEqual(resource.cookieDomainForRequest(request), cookie) def test_missingHostHeaderCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns C{None} if no host header is present. """ self._cookieTest(None, None) def test_noDomainsNoSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns C{None} if no domain sequence is provided and subdomains are disabled. """ self._cookieTest('example.com', None) def test_noDomainsSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns the hostname from the request prefixed with C{"."} if no domain sequence is provided and subdomains are enabled. """ self._cookieTest('example.com', '.example.com', enableSubdomains=True) def test_domainNotFoundNoSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns the C{None} if the hostname from the request is not found in the supplied domain sequence and subdomains are disabled. """ self._cookieTest('example.com', None, domains=['example.org']) def test_domainNotFoundSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns the hostname from the request prefixed with C{"."} if the hostname from the request is not found in the supplied domain sequence and subdomains are enabled. """ self._cookieTest('example.com', ".example.com", domains=['example.org'], enableSubdomains=True) def test_domainFoundNoSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns C{None} if the hostname from the request is found in the supplied domain sequence and subdomains are disabled. """ self._cookieTest('example.com', None, domains=['example.com']) def test_domainFoundSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns the hostname from the request prefixed with C{"."} if the hostname from the request is found in the supplied domain sequence and subdomains are enabled. """ self._cookieTest('example.com', ".example.com", domains=['example.com'], enableSubdomains=True) def test_subdomainFoundNoSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns C{None} if the hostname from the request is a subdomain of one of the domains in the supplied domain sequence but subdomains are disabled. """ self._cookieTest('alice.example.com', None, domains=['example.com']) def test_subdomainFoundSubdomainsCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} returns the domain from the supplied domain sequence prefixed with C{"."} that the hostname from the request is found to be a subdomain of, if it is found to be a subdomain of any of them and subdomains are enabled. """ self._cookieTest('alice.example.com', '.example.com', domains=['example.com'], enableSubdomains=True) def test_explicitPortNumberCookie(self): """ L{PersistentSessionWrapper.cookieDomainForRequest} disregards the port number in the request host. """ self._cookieTest('alice.example.com:8080', '.example.com', domains=['example.com'], enableSubdomains=True) PK9F}NN!xmantissa/test/test_websharing.py""" Tests for L{xmantissa.websharing} and L{xmantissa.publicweb}. """ from zope.interface import Interface, Attribute, implements from twisted.python.components import registerAdapter from twisted.trial.unittest import TestCase from nevow import rend, url from nevow.athena import LiveElement from epsilon.structlike import record from axiom.item import Item from axiom.attributes import integer, text from axiom.store import Store from axiom.userbase import LoginSystem, LoginMethod from axiom.dependency import installOn from axiom.plugins.mantissacmd import Mantissa from xmantissa import ( websharing, sharing, signup, offering, product, ixmantissa) class _TemplateNameResolver(Item): """ An L{ixmantissa.ITemplateNameResolver} with an implementation of L{getDocFactory} which doesn't require the presence any disk templates. """ powerupInterfaces = (ixmantissa.ITemplateNameResolver,) magicTemplateName = text(doc=""" L{magicDocFactoryValue} will be returned by L{getDocFactory} if it is passed this string as the first argument.""") magicDocFactoryValue = text(doc=""" The string value to be returned from L{getDocFactory} when the name it is passed matches L{magicTemplateName}.""" # if anything starts to care too much about what the docFactory is, we # won't be able to get away with just using a string. ) # ITemplateNameResolver def getDocFactory(self, name, default=None): """ If C{name} matches L{self.magicTemplateName}, return L{self.magicTemplateName}, otherwise return C{default}. """ if name == self.magicTemplateName: return self.magicDocFactoryValue return default class ITest(Interface): """ Interface for L{TestAppPowerup} to be shared on. """ store = Attribute("expose 'store' for testing") class TestAppPowerup(Item): implements(ITest) attr = integer() def installed(self): """ Share this item once installed. """ shareid = u'test' sharing.getEveryoneRole(self.store ).shareItem(self, shareID=shareid) websharing.addDefaultShareID(self.store, shareid, 0) class WebSharingTestCase(TestCase): """ Tests for L{xmantissa.websharing.linkTo} """ def setUp(self): """ Set up some state. """ self.s = Store() self.ls = LoginSystem(store=self.s) installOn(self.ls, self.s) acct = self.ls.addAccount( u'right', u'host', u'', verified=True, internal=True) acct.addLoginMethod( u'wrong', u'host', internal=False, verified=False) self.share = sharing.shareItem(self.ls, shareID=u'loginsystem') def test_noLoginMethods(self): """ L{websharing.linkTo} raises a L{RuntimeError} when the shared item is in a store with no internal L{LoginMethod}s. """ for lm in self.s.query(LoginMethod): lm.internal = False self.assertRaises(RuntimeError, websharing.linkTo, self.share) def test_linkToShare(self): """ Test that L{xmantissa.websharing.linkTo} generates a URL using the localpart of the account's internal L{axiom.userbase.LoginMethod} """ self._verifyPath(websharing.linkTo(self.share)) def _verifyPath(self, linkURL): """ Verify that the given url matches the test's expectations. """ self.failUnless(isinstance(linkURL, url.URL), "linkTo should return a nevow.url.URL, not %r" % (type(linkURL))) self.assertEquals(str(linkURL), '/users/right/loginsystem') def test_linkToProxy(self): """ Test that L{xmantissa.websharing.linkTo} generates a URL that I can link to. """ self._verifyPath( websharing.linkTo(sharing.getShare(self.s, sharing.getEveryoneRole( self.s), u'loginsystem'))) def test_shareURLInjectsShareID(self): """ Test that L{xmantissa.websharing._ShareURL} injects the share ID the constructor is passed when C{child} is called. """ for (shareID, urlID) in [(u'a', 'a'), (u'\xe9', '%C3%A9')]: shareURL = websharing._ShareURL(shareID, netloc='', scheme='') self.assertEqual(str(shareURL.child('c')), '/%s/c' % urlID) # make sure subsequent child calls on the original have the same # behaviour self.assertEqual(str(shareURL.child('d')), '/%s/d' % urlID) # and that child calls on the returned urls don't (i.e. not # '/a/c/a/d' self.assertEqual(str(shareURL.child('c').child('d')), '/%s/c/d' % urlID) def test_shareURLNoStoreID(self): """ Test that L{xmantissa.websharing._ShareURL} behaves like a regular L{nevow.url.URL} when no store ID is passed. """ shareURL = websharing._ShareURL(None, netloc='', scheme='') self.assertEqual(str(shareURL.child('a')), '/a') self.assertEqual(str(shareURL.child('a').child('b')), '/a/b') def test_shareURLNoClassmethodConstructors(self): """ Verify that the C{fromRequest}, C{fromContext} and C{fromString} constructors on L{xmantissa.websharing._ShareURL} throw L{NotImplementedError}. """ for meth in (websharing._ShareURL.fromRequest, websharing._ShareURL.fromString, websharing._ShareURL.fromContext): self.assertRaises( NotImplementedError, lambda: meth(None)) def test_shareURLCloneMaintainsShareID(self): """ Test that L{xmantissa.websharing._ShareURL} can be cloned, and that clones will remember the share ID. """ shareURL = websharing._ShareURL(u'a', netloc='', scheme='') shareURL = shareURL.cloneURL('', '', None, None, '') self.assertEqual(shareURL._shareID, u'a') def test_defaultShareIDInteractionMatching(self): """ Verify that L{websharing.linkTo} does not explicitly include a share ID in the URL if the ID of the share it is passed matches the default. """ websharing.addDefaultShareID(self.s, u'share-id', 0) sharing.shareItem(Shareable(store=self.s), shareID=u'share-id') share = sharing.getShare( self.s, sharing.getEveryoneRole(self.s), u'share-id') url = websharing.linkTo(share) self.assertEqual(str(url), '/users/right/') # and if we call child() self.assertEqual(str(url.child('child')), '/users/right/share-id/child') def test_defaultShareIDInteractionNoMatch(self): """ Verify that L{websharing.linkTo} explicitly includes a share ID in the URL if the ID of the share it is passed doesn't match the default. """ websharing.addDefaultShareID(self.s, u'share-id', 0) shareable = Shareable(store=self.s) sharing.shareItem(Shareable(store=self.s), shareID=u'not-the-share-id') share = sharing.getShare( self.s, sharing.getEveryoneRole(self.s), u'not-the-share-id') url = websharing.linkTo(share) self.assertEqual(str(url), '/users/right/not-the-share-id') def test_appStoreLinkTo(self): """ When L{websharing.linkTo} is called on a shared item in an app store, it returns an URL with a single path segment consisting of the app's name. """ s = Store(dbdir=self.mktemp()) Mantissa().installSite(s, u"localhost", u"", False) Mantissa().installAdmin(s, u'admin', u'localhost', u'asdf') off = offering.Offering( name=u'test_offering', description=u'Offering for creating a sample app store', siteRequirements=[], appPowerups=[TestAppPowerup], installablePowerups=[], loginInterfaces=[], themes=[], ) userbase = s.findUnique(LoginSystem) adminAccount = userbase.accountByAddress(u'admin', u'localhost') conf = adminAccount.avatars.open().findUnique( offering.OfferingConfiguration) conf.installOffering(off, None) ss = userbase.accountByAddress(off.name, None).avatars.open() sharedItem = sharing.getEveryoneRole(ss).getShare( websharing.getDefaultShareID(ss)) linkURL = websharing.linkTo(sharedItem) self.failUnless(isinstance(linkURL, url.URL), "linkTo should return a nevow.url.URL, not %r" % (type(linkURL))) self.assertEquals(str(linkURL), '/test_offering/') class _UserIdentificationMixin: def setUp(self): self.siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(self.siteStore, u"localhost", u"", False) Mantissa().installAdmin(self.siteStore, u'admin', u'localhost', '') self.loginSystem = self.siteStore.findUnique(LoginSystem) self.adminStore = self.loginSystem.accountByAddress( u'admin', u'localhost').avatars.open() sc = self.adminStore.findUnique(signup.SignupConfiguration) self.signup = sc.createSignup( u'testuser@localhost', signup.UserInfoSignup, {'prefixURL': u''}, product.Product(store=self.siteStore, types=[]), u'', u'') class UserIdentificationTestCase(_UserIdentificationMixin, TestCase): """ Tests for L{xmantissa.websharing._storeFromUsername} """ def test_sameLocalpartAndUsername(self): """ Test that L{xmantissa.websharing._storeFromUsername} doesn't get confused when the username it is passed is the same as the localpart of that user's email address """ self.signup.createUser( u'', u'username', u'localhost', u'', u'username@internet') self.assertIdentical( websharing._storeFromUsername(self.siteStore, u'username'), self.loginSystem.accountByAddress( u'username', u'localhost').avatars.open()) def test_usernameMatchesOtherLocalpart(self): """ Test that L{xmantissa.websharing._storeFromUsername} doesn't get confused when the username it is passed matches the localpart of another user's email address """ self.signup.createUser( u'', u'username', u'localhost', u'', u'notusername@internet') self.signup.createUser( u'', u'notusername', u'localhost', u'', u'username@internet') self.assertIdentical( websharing._storeFromUsername(self.siteStore, u'username'), self.loginSystem.accountByAddress( u'username', u'localhost').avatars.open()) class IShareable(Interface): """ Dummy interface for Shareable. """ magicValue = Attribute( """ A magical value. """) fragmentName = Attribute( """ The value that the corresponding L{ShareableView} should use for its C{fragmentName} attribute. """) class Shareable(Item): """ This is a dummy class that may be shared. """ implements(IShareable) magicValue = integer() class ShareableView(LiveElement): """ Nothing to see here, move along. @ivar customizedFor: The username we were customized for, or C{None}. """ implements(ixmantissa.INavigableFragment, ixmantissa.ICustomizable) customizedFor = None fragmentName = 'bogus' def __init__(self, shareable): """ adapt a shareable to INavigableFragment """ super(ShareableView, self).__init__() self.shareable = shareable def showMagicValue(self): """ retrieve the magic value from my model """ return self.shareable.magicValue # XXX: Everything below in this class should not be required. It's here to # satisfy implicit requirements in SharingIndex.locateChild, but there # should be test coverage ensuring that it is not required and that # customizeFor is only invoked if you provide the ICustomizable interface. def customizeFor(self, user): """ Customize me by returning myself, and storing the username we were customized for as L{self.customizedFor}. """ self.customizedFor = user return self registerAdapter(ShareableView, IShareable, ixmantissa.INavigableFragment) class FakePage(record('wrappedFragment')): """ A fake page that simply contains a wrapped fragment; analagous to the various shell page classes in the tests which use it. """ class FakeShellFactory(record('username')): """ An L{IWebViewer} which returns a fake shell page and maps a role directly to its given username. """ def wrapModel(self, model): """ Adapt the given model to L{INavigableFragment} and then wrap it in a L{FakePage}. """ return FakePage(ixmantissa.INavigableFragment(model)) def roleIn(self, userStore): """ Return the primary role for the username passed to me. """ return sharing.getPrimaryRole(userStore, self.username) class UserIndexPageTestCase(_UserIdentificationMixin, TestCase): """ Tests for L{xmantissa.websharing.UserIndexPage} """ username = u'alice' domain = u'example.com' shareID = u'ashare' def setUp(self): """ Create an additional user for UserIndexPage, and share a single item with a shareID of the empty string. """ _UserIdentificationMixin.setUp(self) self.magicValue = 123412341234 self.signup.createUser( u'', self.username, self.domain, u'', u'username@internet') self.userStore = websharing._storeFromUsername( self.siteStore, self.username) self.shareable = Shareable(store=self.userStore, magicValue=self.magicValue) self.share = sharing.shareItem(self.shareable, shareID=self.shareID) def makeSharingIndex(self, username): """ Create a L{SharingIndex}. """ return websharing.SharingIndex( self.userStore, webViewer=FakeShellFactory(username)) def test_locateChild(self): """ L{websharing.UserIndexPage.locateChild} should return the named user's L{websharing.SharingIndex} (and any remaining segments), or L{rend.NotFound}. """ # Test against at least one other valid user. self.signup.createUser( u'Andr\xe9', u'andr\xe9', u'localhost', u'', u'andr\xe9@internet') userStore2 = websharing._storeFromUsername(self.siteStore, u'andr\xe9') index = websharing.UserIndexPage(self.loginSystem, FakeShellFactory(None)) for _username, _store in [(self.username, self.userStore), (u'andr\xe9', userStore2)]: (found, remaining) = index.locateChild( None, [_username.encode('utf-8'), 'x', 'y', 'z']) self.assertTrue(isinstance(found, websharing.SharingIndex)) self.assertIdentical(found.userStore, _store) self.assertEquals(remaining, ['x', 'y', 'z']) self.assertIdentical(index.locateChild(None, ['bogus', 'username']), rend.NotFound) def test_linkToMatchesUserURL(self): """ Test that L{xmantissa.websharing.linkTo} generates a URL using the localpart of the account's internal L{axiom.userbase.LoginMethod} """ pathString = str(websharing.linkTo(self.share)) expected = u'/users/%s/%s' % (self.username, self.shareID) self.assertEqual(pathString, expected.encode('ascii')) def test_emptySegmentNoDefault(self): """ Verify that we get L{rend.NotFound} from L{websharing.SharingIndex.locateChild} if there is no default share ID and we access the empty child. """ sharingIndex = self.makeSharingIndex(None) result = sharingIndex.locateChild(None, ('',)) self.assertIdentical(result, rend.NotFound) def test_emptySegmentWithDefault(self): """ Verify that we get the right resource and segments from L{websharing.SharingIndex.locateChild} if there is a default share ID and we access the empty child. """ websharing.addDefaultShareID(self.userStore, u'ashare', 0) sharingIndex = self.makeSharingIndex(None) SEGMENTS = ('', 'foo', 'bar') (res, segments) = sharingIndex.locateChild(None, SEGMENTS) self.assertEqual( res.wrappedFragment.showMagicValue(), self.magicValue) self.assertEqual(segments, SEGMENTS[1:]) def test_invalidShareIDNoDefault(self): """ Verify that we get L{rend.NotFound} from L{websharing.SharingIndex.locateChild} if there is no default share ID and we access an invalid segment. """ sharingIndex = self.makeSharingIndex(None) result = sharingIndex.locateChild(None, ('foo',)) self.assertIdentical(result, rend.NotFound) def test_validShareID(self): """ Verify that we get the right resource and segments from L{websharing.SharingIndex.locateChild} if we access a valid share ID. """ websharing.addDefaultShareID(self.userStore, u'', 0) otherShareable = Shareable(store=self.userStore, magicValue=self.magicValue + 3) for _shareID in [u'foo', u'f\xf6\xf6']: otherShare = sharing.shareItem(otherShareable, shareID=_shareID) sharingIndex = self.makeSharingIndex(None) SEGMENTS = (_shareID.encode('utf-8'), 'bar') (res, segments) = sharingIndex.locateChild(None, SEGMENTS) self.assertEqual( res.wrappedFragment.showMagicValue(), self.magicValue + 3) self.assertEqual(segments, SEGMENTS[1:]) class DefaultShareIDTestCase(TestCase): """ Tests for L{websharing.addDefaultShareID} and L{websharing.getDefaultShareID}. """ def test_createsItem(self): """ Verify that L{websharing.addDefaultShareID} creates a L{websharing._DefaultShareID} item. """ store = Store() websharing.addDefaultShareID(store, u'share id', -22) item = store.findUnique(websharing._DefaultShareID) self.assertEqual(item.shareID, u'share id') self.assertEqual(item.priority, -22) def test_findShareID(self): """ Verify that L{websharing.getDefaultShareID} reads the share ID set by a L{websharing.addDefaultShareID} call. """ store = Store() websharing.addDefaultShareID(store, u'share id', 0) self.assertEqual(websharing.getDefaultShareID(store), u'share id') def test_findHighestPriorityShareID(self): """ Verify that L{websharing.getDefaultShareID} reads the highest-priority share ID set by L{websharing.addDefaultShareID}. """ store = Store() websharing.addDefaultShareID(store, u'share id!', 24) websharing.addDefaultShareID(store, u'share id', 25) websharing.addDefaultShareID(store, u'share id.', -1) self.assertEqual(websharing.getDefaultShareID(store), u'share id') def test_findsNoItem(self): """ Verify that L{websharing.getDefaultShareID} returns C{u''} if there is no default share ID. """ self.assertEqual(websharing.getDefaultShareID(Store()), u'') PK9FNb|--xmantissa/test/test_webshell.py """ Tests for common functionality between L{IWebViewer} implementations. No runnable tests are currently defined here; the mixin defined here is imported from test_webapp (for the authenticated view) and test_publicweb (for the anonymous view). """ from axiom.userbase import LoginSystem from axiom.store import Store from nevow.rend import Page, NotFound from nevow.page import deferflatten, Element from nevow.loaders import stan from nevow.athena import LivePage from nevow import tags from nevow.inevow import IRequest from nevow.context import WovenContext from nevow.testutil import FakeRequest from xmantissa.error import CouldNotLoadFromThemes from xmantissa.ixmantissa import ISiteURLGenerator from xmantissa.webtheme import theThemeCache from xmantissa.test.fakes import ( FakeLoader, FakeTheme, FakeFragmentModel, FakeLiveElementModel, FakeLiveFragmentModel, FakeModel, ResourceViewForFakeModel, FakeElementModel, FakeElementModelWithTheme, FakeElementModelWithDocFactory, FakeElementModelWithThemeAndDocFactory, ElementViewForFakeModelWithTheme, FakeElementModelWithLocateChildView, FakeElementModelWithHead, ElementViewForFakeModelWithThemeAndDocFactory, __file__ as fakesfile) from xmantissa.website import MantissaLivePage from axiom.plugins.mantissacmd import Mantissa class WebViewerTestMixin(object): """ Tests for implementations of L{IWebViewer}. """ def setUp(self): """ Create a site store containing an admin user, then do implementation-specific setup. """ self.siteStore = Store(filesdir=self.mktemp()) m = Mantissa() m.installSite(self.siteStore, u"localhost", u"", False) m.installAdmin(self.siteStore, u'admin', u'localhost', u'asdf') self.loginSystem = self.siteStore.findUnique(LoginSystem) self.adminStore = self.loginSystem.accountByAddress( u'admin', u'localhost').avatars.open() self.setupPageFactory() def assertPage(self, page): """ Fail if the given object is not a nevow L{rend.Page} (or if it is an L{LivePage}). """ self.assertIsInstance(page, Page) self.assertNotIsInstance(page, LivePage) def assertLivePage(self, page): """ Fail if the given object is not a L{MantissaLivePage} """ self.assertIsInstance(page, MantissaLivePage) def test_wrapResource(self): """ An object adaptable to L{IResource} has its IResource adapter returned when wrapModel is called with it. """ resourceable = FakeModel() result = self.pageFactory.wrapModel(resourceable) self.assertIsInstance(result, ResourceViewForFakeModel) def test_wrapElement(self): """ An object adaptable to L{INavigableFragment}, whose adapter is an L{Element}, is wrapped in a shell page (containing navigation for the current user) when wrapModel is called on it. """ elementable = FakeElementModel() result = self.pageFactory.wrapModel(elementable) self.assertPage(result) self.assertEqual(result.fragment.model, elementable) def test_wrapFragment(self): """ An object adaptable to L{INavigableFragment}, whose adapter is a L{Fragment}, is wrapped in a shell page (containing navigation for the current user) when wrapModel is called on it. """ fragmentable = FakeFragmentModel() result = self.pageFactory.wrapModel(fragmentable) self.assertPage(result) self.assertEqual(result.fragment.original, fragmentable) def test_wrapLiveElement(self): """ An object adaptable to L{INavigableFragment}, whose adapter is a L{LiveElement}, is wrapped in a shell page (containing navigation for the current user) when wrapModel is called on it. """ elementable = FakeLiveElementModel() result = self.pageFactory.wrapModel(elementable) self.assertLivePage(result) self.assertEqual(result.fragment.model, elementable) def test_wrapLiveFragment(self): """ An object adaptable to L{INavigableFragment}, whose adapter is a L{LiveFragment}, is wrapped in a shell page (containing navigation for the current user) when wrapModel is called on it. """ fragmentable = FakeLiveFragmentModel() result = self.assertWarns( DeprecationWarning, '[v0.10] LiveFragment has been superceded by LiveElement.', fakesfile, self.pageFactory.wrapModel, fragmentable) self.assertLivePage(result) self.assertEqual(result.fragment.original, fragmentable) def stubThemeList(self, themes): """ Replace the global themes list with the given list of themes for the duration of this test. This relies on the implementation details of the global theme cache; please update if the theme cache implementation changes. """ theThemeCache._getInstalledThemesCache[self.siteStore] = themes self.addCleanup(theThemeCache.emptyCache) def test_docFactoryFromFragmentName(self): """ If the L{INavigableFragment} provider provides a C{fragmentName} attribute, it should be looked up via the theme system and assigned to the C{docFactory} attribute of the L{INavigableFragment} provider. """ fakeDocFactory = FakeLoader('fake') self.stubThemeList([FakeTheme('theme', {ElementViewForFakeModelWithTheme.fragmentName: fakeDocFactory})]) elementable = FakeElementModelWithTheme() result = self.pageFactory.wrapModel(elementable) self.assertEqual(result.fragment.docFactory, fakeDocFactory) def test_dontThrowOutTheBabyWithTheBathwater(self): """ If the L{INavigableFragment} provider provides a C{docFactory} attribute, neither lack of a C{fragmentName} attribute, a C{fragmentName} that is None, nor a C{fragmentName} for which there is no loader in any theme, should change its C{docFactory} to C{None}. """ fakeDocFactory = FakeLoader('fake') self.stubThemeList([FakeTheme('theme', {})]) model = FakeElementModelWithDocFactory(fakeDocFactory) modelWithFragmentName = FakeElementModelWithThemeAndDocFactory('wrong', fakeDocFactory) result = self.pageFactory.wrapModel(model) self.assertEqual(result.fragment.docFactory, fakeDocFactory) result = self.pageFactory.wrapModel(modelWithFragmentName) self.assertEqual(result.fragment.docFactory, fakeDocFactory) def test_noLoaderAnywhere(self): """ If the L{INavigableFragment}provider provides no C{docFactory} or C{fragmentName} attribute, then L{_AuthenticatedWebViewer.wrapModel} should raise L{CouldNotLoadFromThemes}. """ model = FakeElementModelWithThemeAndDocFactory(None, None) aFakeTheme = FakeTheme('fake', {}) self.stubThemeList([aFakeTheme]) exc = self.assertRaises( CouldNotLoadFromThemes, self.pageFactory.wrapModel, model) self.assertIsInstance(exc.element, ElementViewForFakeModelWithThemeAndDocFactory) self.assertIdentical(exc.element.model, model) self.assertEquals(exc.themes, [aFakeTheme]) self.assertEquals( repr(exc), 'CouldNotLoadFromThemes: %r (fragment name %r) has no docFactory. Searched these themes: %r' % (exc.element, exc.element.fragmentName, [aFakeTheme])) def test_navFragmentHasLocateChild(self, beLive=False): """ The L{IResource} returned by L{IWebViewer.wrapModel} must have a C{locateChild} which forwards to its wrapped L{INavigableFragment}. """ expectChild = object() elementable = FakeElementModelWithLocateChildView([expectChild], beLive=beLive) result = self.pageFactory.wrapModel(elementable) resultChild = result.locateChild(FakeRequest(), ['']) self.assertIdentical(resultChild, expectChild) def test_navFragmentHasLiveLocateChild(self): """ The same as L{test_navFragmentHasLiveLocateChild}, but for the Athena implementation of the resource adapters. """ self.test_navFragmentHasLocateChild(True) def test_navFragmentHasNoChildMethods(self, beLive=False): """ The L{IResource} returned by L{IWebViewer.wrapModel} must have a C{locateChild} which returns NotFound in the case where its wrapped L{INavigableFragment} does not have a C{locateChild} method. """ if beLive: elementable = FakeLiveElementModel() else: elementable = FakeElementModel() result = self.pageFactory.wrapModel(elementable) resultChild = result.locateChild(FakeRequest(), ['']) self.assertEqual(resultChild, NotFound) def test_navFragmentHasNoLiveChildMethods(self): """ The same as L{test_navFragmentHasNoChildMethods} but for the Athena implementation of the resource adapters. """ self.test_navFragmentHasNoChildMethods(True) def _renderHead(self, result): """ Go through all the gyrations necessary to get the head contents produced by the page rendering process. """ site = ISiteURLGenerator(self.siteStore) t = tags.invisible() ctx = WovenContext(tag=t) req = FakeRequest() ctx.remember(req, IRequest) expected = [th.head(req, site) for th in self.pageFactory._preferredThemes()] head = result.render_head(ctx, None) return t, expected def test_renderHeadNoHeadMethod(self): """ Rendering the tag in the shell template for a non-athena L{INavigableFragment} without a C{head()} method should result in each theme adding its external content (stylesheets, etc) to the . """ elementable = FakeElementModel() result = self.pageFactory.wrapModel(elementable) t, expected = self._renderHead(result) self.assertStanEqual(list(t.children), expected) def test_renderHeadWithHeadMethod(self): """ Rendering the tag in the shell template for a non-athena L{INavigableFragment} with a C{head()} method should result in each theme adding its external content (stylesheets, etc) to the , followed by the contents provided by the L{INavigableFragment}. """ h = tags.div["some stuff"] model = FakeElementModelWithHead(h) result = self.pageFactory.wrapModel(model) t, expected = self._renderHead(result) self.assertStanEqual(list(t.children), expected + [h]) def _instaString(self, x): """ Convert a new-style-renderable thing into a string, raising an exception if any deferred rendering took place. """ e = Element(docFactory=stan(x)) l = [] d = deferflatten(None, e, False, False, l.append) return ''.join(l) def assertStanEqual(self, stan1, stan2): """ Assert that two chunks of Nevow-renderable stuff will result in the same XML. """ a = self._instaString(stan1) b = self._instaString(stan2) self.assertEqual(a, b) PK9F_ڨӽӽxmantissa/test/test_website.py from epsilon import hotfix hotfix.require('twisted', 'trial_assertwarns') from hashlib import sha1 from zope.interface import implements from zope.interface.verify import verifyObject try: from cssutils import CSSParser CSSParser except ImportError: CSSParser = None from twisted.python.components import registerAdapter from twisted.internet.address import IPv4Address from twisted.trial.unittest import TestCase from twisted.trial import util from twisted.python.filepath import FilePath from nevow import tags from nevow.flat import flatten from nevow.context import WebContext from nevow.testutil import FakeRequest from nevow.url import URL from nevow.inevow import IResource, IRequest from nevow.rend import WovenContext, NotFound from nevow.athena import LivePage, LiveElement, AthenaModule, jsDeps from nevow.guard import LOGIN_AVATAR from nevow.loaders import stan from axiom import userbase from axiom.userbase import LoginSystem from axiom.store import Store from axiom.dependency import installOn from axiom.plugins.mantissacmd import Mantissa from xmantissa.ixmantissa import ( IProtocolFactoryFactory, ISiteURLGenerator, ISiteRootPlugin, IWebViewer, ISessionlessSiteRootPlugin) from xmantissa.port import TCPPort, SSLPort from xmantissa import website, publicweb from xmantissa._webutil import VirtualHostWrapper from xmantissa.publicweb import LoginPage from xmantissa.offering import installOffering from xmantissa.plugins.baseoff import baseOffering from xmantissa.cachejs import theHashModuleProvider from xmantissa.website import WebSite from xmantissa.webapp import PrivateApplication from xmantissa.websharing import SharingIndex from xmantissa.website import MantissaLivePage, APIKey, PrefixURLMixin from xmantissa.web import SecuringWrapper, _SecureWrapper, StaticContent, UnguardedWrapper, SiteConfiguration maybeEncryptedRootWarningMessage = ( "Use ISiteURLGenerator.rootURL instead of WebSite.maybeEncryptedRoot") maybeEncryptedRootSuppression = util.suppress( message=maybeEncryptedRootWarningMessage, category=DeprecationWarning) class SiteConfigurationTests(TestCase): """ L{xmantissa.web.Site} defines how to create an HTTP server. """ def setUp(self): self.domain = u"example.com" self.store = Store() self.site = SiteConfiguration(store=self.store, hostname=self.domain) def test_interfaces(self): """ L{SiteConfiguration} implements L{IProtocolFactoryFactory} and L{ISiteURLGenerator}. """ self.assertTrue(verifyObject(ISiteURLGenerator, self.site)) self.assertTrue(verifyObject(IProtocolFactoryFactory, self.site)) def _baseTest(self, portType, scheme, portNumber, method): portType(store=self.store, portNumber=portNumber, factory=self.site) self.assertEquals( getattr(self.site, method)(), URL(scheme, self.domain)) def test_cleartextRoot(self): """ L{SiteConfiguration.cleartextRoot} method returns the proper URL for HTTP communication with this site. """ self._baseTest(TCPPort, 'http', 80, 'cleartextRoot') def test_encryptedRoot(self): """ L{SiteConfiguration.encryptedRoot} method returns the proper URL for HTTPS communication with this site. """ self._baseTest(SSLPort, 'https', 443, 'encryptedRoot') def _nonstandardPortTest(self, portType, scheme, portNumber, method): portType(store=self.store, portNumber=portNumber, factory=self.site) self.assertEquals( getattr(self.site, method)(), URL(scheme, '%s:%s' % (self.domain, portNumber))) def test_cleartextRootNonstandardPort(self): """ L{SiteConfiguration.cleartextRoot} method returns the proper URL for HTTP communication with this site even if the server is listening on a non-standard port number. """ self._nonstandardPortTest(TCPPort, 'http', 8000, 'cleartextRoot') def test_encryptedRootNonstandardPort(self): """ L{SiteConfiguration.encryptedRoot} method returns the proper URL for HTTPS communication with this site even if the server is listening on a non-standard port number. """ self._nonstandardPortTest(SSLPort, 'https', 8443, 'encryptedRoot') def _unavailableTest(self, method): self.assertEquals(getattr(self.site, method)(), None) def test_cleartextRootUnavailable(self): """ L{SiteConfiguration.cleartextRoot} method returns None if there is no HTTP server listening. """ self._unavailableTest('cleartextRoot') def test_encryptedRootUnavailable(self): """ L{SiteConfiguration.encryptedRoot} method returns None if there is no HTTPS server listening. """ self._unavailableTest('encryptedRoot') def _hostOverrideTest(self, portType, scheme, portNumber, method): portType(store=self.store, portNumber=portNumber, factory=self.site) self.assertEquals( getattr(self.site, method)(u'example.net'), URL(scheme, 'example.net')) def test_cleartextRootHostOverride(self): """ A hostname passed to L{SiteConfiguration.cleartextRoot} overrides the configured hostname in the result. """ self._hostOverrideTest(TCPPort, 'http', 80, 'cleartextRoot') def test_encryptedRootHostOverride(self): """ A hostname passed to L{SiteConfiguration.encryptedRoot} overrides the configured hostname in the result. """ self._hostOverrideTest(SSLPort, 'https', 443, 'encryptedRoot') def _portZero(self, portType, scheme, method): randomPort = 7777 class FakePort(object): def getHost(self): return IPv4Address('TCP', u'example.com', randomPort) port = portType(store=self.store, portNumber=0, factory=self.site) port.listeningPort = FakePort() self.assertEquals( getattr(self.site, method)(), URL(scheme, '%s:%s' % (self.domain, randomPort))) def test_cleartextRootPortZero(self): """ When the C{portNumber} of a started L{TCPPort} which refers to the C{SiteConfiguration} is C{0}, L{SiteConfiguration.cleartextRoot} returns an URL with the port number which was actually bound in the netloc. """ self._portZero(TCPPort, 'http', 'cleartextRoot') def test_encryptedRootPortZero(self): """ When the C{portNumber} of a started L{SSLPort} which refers to the C{SiteConfiguration} is C{0}, L{SiteConfiguration.encryptedRoot} returns an URL with the port number which was actually bound in the netloc. """ self._portZero(SSLPort, 'https', 'encryptedRoot') def _portZeroDisconnected(self, portType, method): portType(store=self.store, portNumber=0, factory=self.site) self.assertEquals(None, getattr(self.site, method)()) def test_cleartextRootPortZeroDisconnected(self): """ When the C{portNumber} of an unstarted L{TCPPort} which refers to the C{SiteConfiguration} is C{0}, L{SiteConfiguration.cleartextRoot} returns C{None}. """ self._portZeroDisconnected(TCPPort, 'cleartextRoot') def test_encryptedRootPortZeroDisconnected(self): """ When the C{portNumber} of an unstarted L{SSLPort} which refers to the C{SiteConfiguration} is C{0}, L{SiteConfiguration.encryptedRoot} returns C{None}. """ self._portZeroDisconnected(SSLPort, 'encryptedRoot') def test_rootURL(self): """ L{SiteConfiguration.rootURL} returns C{/} for a request made onto the hostname with which the L{SiteConfiguration} is configured. """ request = FakeRequest(headers={ 'host': self.domain.encode('ascii')}) self.assertEqual(self.site.rootURL(request), URL('', '')) def test_rootURLWithoutHost(self): """ L{SiteConfiguration.rootURL} returns C{/} for a request made without a I{Host} header. """ request = FakeRequest() self.assertEqual(self.site.rootURL(request), URL('', '')) def test_rootURLWWWSubdomain(self): """ L{SiteConfiguration.rootURL} returns C{/} for a request made onto the I{www} subdomain of the hostname of the L{SiteConfiguration}. """ request = FakeRequest(headers={ 'host': 'www.' + self.domain.encode('ascii')}) self.assertEqual(self.site.rootURL(request), URL('', '')) def _differentHostnameTest(self, portType, portNumber, isSecure, scheme): request = FakeRequest(isSecure=isSecure, headers={ 'host': 'alice.' + self.domain.encode('ascii')}) portType(store=self.store, factory=self.site, portNumber=portNumber) self.assertEqual(self.site.rootURL(request), URL(scheme, self.domain)) def test_cleartextRootURLDifferentHostname(self): """ L{SiteConfiguration.rootURL} returns an absolute URL with the HTTP scheme and its hostname as the netloc and with a path of C{/} for a request made over HTTP onto a hostname different from the hostname of the L{SiteConfiguration}. """ self._differentHostnameTest(TCPPort, 80, False, 'http') def test_encryptedRootURLDifferentHostname(self): """ L{SiteConfiguration.rootURL} returns an absolute URL with its hostname as the netloc and with a path of C{/} for a request made over HTTPS onto a hostname different from the hostname of the L{SiteConfiguration}. """ self._differentHostnameTest(SSLPort, 443, True, 'https') def _differentHostnameNonstandardPort(self, portType, isSecure, scheme): portNumber = 12345 request = FakeRequest(isSecure=isSecure, headers={ 'host': 'alice.' + self.domain.encode('ascii')}) portType(store=self.store, factory=self.site, portNumber=portNumber) self.assertEqual( self.site.rootURL(request), URL(scheme, '%s:%s' % (self.domain.encode('ascii'), portNumber))) def test_cleartextRootURLDifferentHostnameNonstandardPort(self): """ L{SiteConfiguration.rootURL} returns an absolute URL with an HTTP scheme and an explicit port number in the netloc for a request made over HTTP onto a hostname different from the hostname of the L{SiteConfiguration} if the L{SiteConfiguration} has an HTTP server on a non-standard port. """ self._differentHostnameNonstandardPort(TCPPort, False, 'http') def test_encryptedRootURLDifferentHostnameNonstandardPort(self): """ L{SiteConfiguration.rootURL} returns an absolute URL with an HTTPS scheme and an explicit port number in the netloc for a request made over HTTPS onto a hostname different from the hostname of the L{SiteConfiguration} if the L{SiteConfiguration} has an HTTPS server on a non-standard port. """ self._differentHostnameNonstandardPort(SSLPort, True, 'https') def test_rootURLNonstandardRequestPort(self): """ L{SiteConfiguration.rootURL} returns C{/} for a request made onto a non-standard port which is one on which the L{SiteConfiguration} is configured to listen. """ request = FakeRequest(headers={ 'host': '%s:%s' % (self.domain.encode('ascii'), 54321)}) TCPPort(store=self.store, factory=self.site, portNumber=54321) self.assertEqual(self.site.rootURL(request), URL('', '')) class StylesheetRewritingRequestWrapperTests(TestCase): """ Tests for L{StylesheetRewritingRequestWrapper}. """ def test_replaceMantissa(self): """ L{StylesheetRewritingRequestWrapper._replace} changes URLs of the form I{/Mantissa/foo} to I{/static/mantissa-base/foo}. """ request = object() roots = {request: URL.fromString('/bar/')} wrapper = website.StylesheetRewritingRequestWrapper(request, [], roots.get) self.assertEqual( wrapper._replace('/Mantissa/foo.png'), '/bar/static/mantissa-base/foo.png') def test_replaceOtherOffering(self): """ L{StylesheetRewritingRequestWrapper._replace} changes URLs of the form I{/Something/foo} to I{/static/Something/foo} if C{Something} gives the name of an installed offering with a static content path. """ request = object() roots = {request: URL.fromString('/bar/')} wrapper = website.StylesheetRewritingRequestWrapper(request, ['OfferingName'], roots.get) self.assertEqual( wrapper._replace('/OfferingName/foo.png'), '/bar/static/OfferingName/foo.png') def test_nonOfferingOnlyGivenPrefix(self): """ L{StylesheetRewritingRequestWrapper._replace} only changes URLs of the form I{/Something/foo} so they are beneath the root URL if C{Something} does not give the name of an installed offering. """ request = object() roots = {request: URL.fromString('/bar/')} wrapper = website.StylesheetRewritingRequestWrapper( request, ['Foo'], roots.get) self.assertEqual( wrapper._replace('/OfferingName/foo.png'), '/bar/OfferingName/foo.png') def test_shortURL(self): """ L{StylesheetRewritingRequestWrapper._replace} changes URLs with only one segment so they are beneath the root URL. """ request = object() roots = {request: URL.fromString('/bar/')} wrapper = website.StylesheetRewritingRequestWrapper( request, [], roots.get) self.assertEqual( wrapper._replace('/'), '/bar/') def test_absoluteURL(self): """ L{StylesheetRewritingRequestWrapper._replace} does not change absolute URLs. """ wrapper = website.StylesheetRewritingRequestWrapper(object(), [], None) self.assertEqual( wrapper._replace('http://example.com/foo'), 'http://example.com/foo') def test_relativeUnmodified(self): """ L{StylesheetRewritingRequestWrapper._replace} does not change URLs with relative paths. """ wrapper = website.StylesheetRewritingRequestWrapper(object(), [], None) self.assertEqual(wrapper._replace('relative/path'), 'relative/path') def test_finish(self): """ L{StylesheetRewritingRequestWrapper.finish} causes all written bytes to be translated with C{_replace} written to the wrapped request. """ stylesheetFormat = """ .foo { background-image: url(%s) } """ originalStylesheet = stylesheetFormat % ("/Foo/bar",) expectedStylesheet = stylesheetFormat % ("/bar/Foo/bar",) request = FakeRequest() roots = {request: URL.fromString('/bar/')} wrapper = website.StylesheetRewritingRequestWrapper( request, [], roots.get) wrapper.write(originalStylesheet) wrapper.finish() # Parse and serialize both versions to normalize whitespace so we can # make a comparison. parser = CSSParser() self.assertEqual( parser.parseString(request.accumulator).cssText, parser.parseString(expectedStylesheet).cssText) if CSSParser is None: test_finish.skip = "Stylesheet rewriting test requires cssutils package." class LoginPageTests(TestCase): """ Tests for functionality related to login. """ domain = u"example.com" def setUp(self): """ Create a L{Store}, L{WebSite} and necessary request-related objects to test L{LoginPage}. """ self.siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(self.siteStore, self.domain, u"", False) self.site = self.siteStore.findUnique(SiteConfiguration) installOn( TCPPort(store=self.siteStore, factory=self.site, portNumber=80), self.siteStore) self.context = WebContext() self.request = FakeRequest() self.context.remember(self.request) def test_fromRequest(self): """ L{LoginPage.fromRequest} should return a two-tuple of the class it is called on and an empty tuple. """ request = FakeRequest( uri='/foo/bar/baz', currentSegments=['foo'], args={'quux': ['corge']}) class StubLoginPage(LoginPage): def __init__(self, store, segments, arguments): self.store = store self.segments = segments self.arguments = arguments page = StubLoginPage.fromRequest(self.siteStore, request) self.assertTrue(isinstance(page, StubLoginPage)) self.assertIdentical(page.store, self.siteStore) self.assertEqual(page.segments, ['foo', 'bar']) self.assertEqual(page.arguments, {'quux': ['corge']}) def test_staticShellContent(self): """ The L{IStaticShellContent} adapter for the C{store} argument to L{LoginPage.__init__} should become its C{staticContent} attribute. """ originalInterface = publicweb.IStaticShellContent adaptions = [] result = object() def stubInterface(object, default): adaptions.append((object, default)) return result publicweb.IStaticShellContent = stubInterface try: page = LoginPage(self.siteStore) finally: publicweb.IStaticShellContent = originalInterface self.assertEqual(len(adaptions), 1) self.assertIdentical(adaptions[0][0], self.siteStore) self.assertIdentical(page.staticContent, result) def test_segments(self): """ L{LoginPage.beforeRender} should fill the I{login-action} slot with an L{URL} which includes all the segments given to the L{LoginPage}. """ segments = ('foo', 'bar') page = LoginPage(self.siteStore, segments) page.beforeRender(self.context) loginAction = self.context.locateSlotData('login-action') expectedLocation = URL.fromString('/') for segment in (LOGIN_AVATAR,) + segments: expectedLocation = expectedLocation.child(segment) self.assertEqual(loginAction, expectedLocation) def test_queryArguments(self): """ L{LoginPage.beforeRender} should fill the I{login-action} slot with an L{URL} which includes all the query arguments given to the L{LoginPage}. """ args = {'foo': ['bar']} page = LoginPage(self.siteStore, (), args) page.beforeRender(self.context) loginAction = self.context.locateSlotData('login-action') expectedLocation = URL.fromString('/') expectedLocation = expectedLocation.child(LOGIN_AVATAR) expectedLocation = expectedLocation.add('foo', 'bar') self.assertEqual(loginAction, expectedLocation) def test_locateChildPreservesSegments(self): """ L{LoginPage.locateChild} should create a new L{LoginPage} with segments extracted from the traversal context. """ segments = ('foo', 'bar') page = LoginPage(self.siteStore) child, remaining = page.locateChild(self.context, segments) self.assertTrue(isinstance(child, LoginPage)) self.assertEqual(remaining, ()) self.assertEqual(child.segments, segments) def test_locateChildPreservesQueryArguments(self): """ L{LoginPage.locateChild} should create a new L{LoginPage} with query arguments extracted from the traversal context. """ self.request.args = {'foo': ['bar']} page = LoginPage(self.siteStore) child, remaining = page.locateChild(self.context, None) self.assertTrue(isinstance(child, LoginPage)) self.assertEqual(child.arguments, self.request.args) class UnguardedWrapperTests(TestCase): """ Tests for L{UnguardedWrapper}. """ def setUp(self): """ Set up a store with a valid offering to test against. """ self.store = Store() installOffering(self.store, baseOffering, {}) self.site = ISiteURLGenerator(self.store) def test_live(self): """ L{UnguardedWrapper} has a I{live} child which returns a L{LivePage} instance. """ request = FakeRequest(uri='/live/foo', currentSegments=[]) wrapper = UnguardedWrapper(self.store, None) resource = wrapper.child_live(request) self.assertTrue(isinstance(resource, LivePage)) def test_jsmodules(self): """ L{UnguardedWrapper} has a I{__jsmodules__} child which returns a L{LivePage} instance. """ request = FakeRequest(uri='/__jsmodule__/foo', currentSegments=[]) wrapper = UnguardedWrapper(None, None) resource = wrapper.child___jsmodule__(request) # This is weak. Identity of this object doesn't matter. The caching # and jsmodule serving features are what matter. -exarkun self.assertIdentical(resource, theHashModuleProvider) def test_static(self): """ L{UnguardedWrapper} has a I{static} child which returns a L{StaticContent} instance. """ request = FakeRequest(uri='/static/extra', currentSegments=[]) wrapper = UnguardedWrapper(self.store, None) resource = wrapper.child_static(request) self.assertTrue(isinstance(resource, StaticContent)) self.assertEqual( resource.staticPaths, {baseOffering.name: baseOffering.staticContentPath}) def test_sessionlessPlugin(self): """ L{UnguardedWrapper.locateChild} looks up L{ISessionlessSiteRootPlugin} powerups on its store, and invokes their C{sessionlessProduceResource} methods to discover resources. """ wrapper = UnguardedWrapper(self.store, None) req = FakeRequest() segments = ('foo', 'bar') calledWith = [] result = object() class SiteRootPlugin(object): def sessionlessProduceResource(self, request, segments): calledWith.append((request, segments)) return result self.store.inMemoryPowerUp(SiteRootPlugin(), ISessionlessSiteRootPlugin) wrapper.locateChild(req, segments) self.assertEqual(calledWith, [(req, ("foo", "bar"))]) def test_sessionlessLegacyPlugin(self): """ L{UnguardedWrapper.locateChild} honors old-style L{ISessionlessSiteRootPlugin} providers that only implement a C{resourceFactory} method. """ wrapper = UnguardedWrapper(self.store, None) req = FakeRequest() segments = ('foo', 'bar') calledWith = [] result = object() class SiteRootPlugin(object): def resourceFactory(self, segments): calledWith.append(segments) return result self.store.inMemoryPowerUp(SiteRootPlugin(), ISessionlessSiteRootPlugin) wrapper.locateChild(req, segments) self.assertEqual(calledWith, [segments]) def test_confusedNewPlugin(self): """ If L{UnguardedWrapper.locateChild} discovers a plugin that implements both C{sessionlessProduceResource} and C{resourceFactory}, it should prefer the new C{sessionlessProduceResource} method and return that resource. """ wrapper = UnguardedWrapper(self.store, None) req = FakeRequest() test = self segments = ('foo', 'bar') result = object() calledWith = [] class SiteRootPlugin(object): def resourceFactory(self, segments): test.fail("Don't call this.") def sessionlessProduceResource(self, request, segments): calledWith.append((request, segments)) return result, segments[1:] self.store.inMemoryPowerUp(SiteRootPlugin(), ISessionlessSiteRootPlugin) resource, resultSegments = wrapper.locateChild(req, segments) self.assertEqual(calledWith, [(req, segments)]) self.assertIdentical(resource, result) self.assertEqual(resultSegments, ('bar',)) class SiteTestsMixin(object): """ Tests that apply to both subclasses of L{SiteRootMixin}, L{WebSite} and L{AnonymousSite}. @ivar store: a store containing something that inherits from L{SiteRootMixin}. @ivar resource: the result of adapting the given L{SiteRootMixin} item to L{IResource}. """ def setUp(self): """ Set up a site store with the base offering installed. """ self.siteStore = Store() installOffering(self.siteStore, baseOffering, {}) def addUser(self, username=u'nobody', domain=u'nowhere'): """ Add a user to the site store, and install a L{PrivateApplication}. @return: the user store of the added user. """ userStore = self.siteStore.findUnique(LoginSystem).addAccount( username, domain, u'asdf', internal=True).avatars.open() installOn(PrivateApplication(store=userStore), userStore) return userStore def test_crummyOldSiteRootPlugin(self): """ L{SiteRootMixin.locateChild} queries for L{ISiteRootPlugin} providers and returns the result of their C{resourceFactory} method if it is not C{None}. """ result = object() calledWith = [] class SiteRootPlugin(object): def resourceFactory(self, segments): calledWith.append(segments) return result self.store.inMemoryPowerUp(SiteRootPlugin(), ISiteRootPlugin) self.assertIdentical( self.resource.locateChild( FakeRequest(headers={"host": "example.com"}), ("foo", "bar")), result) self.assertEqual(calledWith, [("foo", "bar")]) def test_shinyNewSiteRootPlugin(self): """ L{SiteRootMixin.locateChild} queries for L{ISiteRootPlugin} providers and returns the result of their C{produceResource} methods. """ navthing = object() result = object() calledWith = [] class GoodSiteRootPlugin(object): implements(ISiteRootPlugin) def produceResource(self, req, segs, webViewer): calledWith.append((req, segs, webViewer)) return result self.store.inMemoryPowerUp(GoodSiteRootPlugin(), ISiteRootPlugin) self.store.inMemoryPowerUp(navthing, IWebViewer) req = FakeRequest(headers={ 'host': 'localhost'}) self.resource.locateChild(req, ("foo", "bar")) self.assertEqual(calledWith, [(req, ("foo", "bar"), navthing)]) def test_confusedNewPlugin(self): """ When L{SiteRootMixin.locateChild} finds a L{ISiteRootPlugin} that implements both C{produceResource} and C{resourceFactory}, it should prefer the new-style C{produceResource} method. """ navthing = object() result = object() calledWith = [] test = self class ConfusedSiteRootPlugin(object): implements(ISiteRootPlugin) def produceResource(self, req, segs, webViewer): calledWith.append((req, segs, webViewer)) return result def resourceFactory(self, segs): test.fail("resourceFactory called.") self.store.inMemoryPowerUp(ConfusedSiteRootPlugin(), ISiteRootPlugin) self.store.inMemoryPowerUp(navthing, IWebViewer) req = FakeRequest(headers={ 'host': 'localhost'}) self.resource.locateChild(req, ("foo", "bar")) self.assertEqual(calledWith, [(req, ("foo", "bar"), navthing)]) def test_virtualHosts(self): """ When a virtual host of the form (x.y) is requested from a site root resource where 'y' is a known domain for that server, a L{SharingIndex} for the user identified as 'x@y' should be returned. """ # Let's make sure we're looking at the correct store for our list of # domains first... self.assertIdentical(self.resource.siteStore, self.siteStore) somebody = self.addUser(u'somebody', u'example.com') req = FakeRequest(headers={'host': 'somebody.example.com'}) res, segs = self.resource.locateChild(req, ('',)) self.assertIsInstance(res, SharingIndex) # :( self.assertIdentical(res.userStore, somebody) class WebSiteTests(SiteTestsMixin, TestCase): """ Isn't this class like four years late? """ def setUp(self): """ Set up a store with a valid offering to test against. """ SiteTestsMixin.setUp(self) userStore = self.addUser() ws = userStore.findUnique(WebSite) self.store = ws.store self.resource = IResource(self.store) class PrefixURLMixinTests(TestCase): """ Tests for L{PrefixURLMixin}. """ def test_createResource(self): """ L{PrefixURLMixin} subclasses that implement C{createResource} have that method called upon resource lookup with no arguments. """ target = object() class PrefixUser(PrefixURLMixin): prefixURL= u'foo' def createResource(self): return target p = PrefixUser() req = FakeRequest(headers={"host": "example.com"}) rsrc, resultSegments = p.produceResource(req, ('foo', 'bar'), None) self.assertEqual(resultSegments, ('bar',)) self.assertIdentical(rsrc, target) def test_createResourceWith(self): """ L{PrefixURLMixin} subclasses that implement C{createResourceWith} have that method called upon resource lookup with the shell factory. """ calledWith = [] target = object() webViewer = object() class PrefixUser(PrefixURLMixin): prefixURL= u'foo' def createResourceWith(self, webViewer): calledWith.append(webViewer) return target p = PrefixUser() req = FakeRequest(headers={"host": "example.com"}) rsrc, resultSegments = p.produceResource(req, ('foo', 'bar'), webViewer) self.assertEqual(resultSegments, ('bar',)) self.assertIdentical(rsrc, target) self.assertIdentical(webViewer, calledWith[0]) def test_sessionlessResource(self): """ L{PrefixURLMixin.sessionlessProduceResource} will produce the resource described by the L{createResource} for appropriate lists of segments. """ target = object() class SessionlessPrefixThing(PrefixURLMixin): implements(ISessionlessSiteRootPlugin) prefixURL = u'foo' def createResource(self): return target p = SessionlessPrefixThing() verifyObject(ISessionlessSiteRootPlugin, p) req = FakeRequest(headers={"host": "example.com"}) rsrc, segments = p.sessionlessProduceResource(req, ('foo', 'bar')) self.assertIdentical(rsrc, target) self.assertEqual(segments, ('bar',)) result = p.sessionlessProduceResource(req, ('baz', 'boz')) self.assertIdentical(result, None) class StubResource(object): """ An L{IResource} implementation which behaves in a way which is useful for testing L{SecuringWrapper}. @ivar childResource: The object which will be returned as a child resource from C{locateChild}. @ivar childSegments: The object which will be returned as the unconsumed segments from C{locateChild}. @ivar renderContent: The object to return from C{renderHTTP}. @ivar renderedWithContext: The argument passed to C{renderHTTP}. """ implements(IResource) def __init__(self, childResource, childSegments, renderContent): self.childResource = childResource self.childSegments = childSegments self.renderContent = renderContent def renderHTTP(self, context): self.renderedWithContext = context return self.renderContent def locateChild(self, context, segments): self.locatedWith = (context, segments) return self.childResource, self.childSegments class NotResource(object): """ A class which does not implement L{IResource}. """ class NotResourceAdapter(object): """ An adapter from L{NotResource} to L{IResource} (not really - but close enough for the tests). """ def __init__(self, notResource): self.notResource = notResource registerAdapter(NotResourceAdapter, NotResource, IResource) class SecuringWrapperTests(TestCase): """ L{SecuringWrapper} makes sure that any resource which is eventually retrieved from a wrapped resource and then rendered is rendered over HTTPS if possible and desired. """ def setUp(self): """ Create a resource and a wrapper to test. """ self.store = Store() self.urlGenerator = SiteConfiguration(store=self.store, hostname=u"example.com") self.child = StubResource(None, None, None) self.childSegments = ("baz", "quux") self.content = "some bytes perhaps" self.resource = StubResource( self.child, self.childSegments, self.content) self.wrapper = SecuringWrapper(self.urlGenerator, self.resource) def test_locateChildHTTPS(self): """ L{SecuringWrapper.locateChild} returns the wrapped resource and the unmodified segments if it is called with a secure request. """ segments = ("foo", "bar") request = FakeRequest(isSecure=True) newResource, newSegments = self.wrapper.locateChild(request, segments) self.assertIdentical(newResource, self.resource) self.assertEqual(newSegments, segments) def test_locateChildHTTP(self): """ L{SecuringWrapper.locateChild} returns a L{_SecureWrapper} wrapped around its own wrapped resource along with the unmodified segments if it is called with an insecure request. """ segments = ("foo", "bar") request = FakeRequest(isSecure=False) newResource, newSegments = self.wrapper.locateChild(request, segments) self.assertTrue(isinstance(newResource, _SecureWrapper)) self.assertIdentical(newResource.urlGenerator, self.urlGenerator) self.assertIdentical(newResource.wrappedResource, self.resource) self.assertEqual(newSegments, segments) def test_renderHTTPNeedsSecure(self): """ L{SecuringWrapper.renderHTTP} returns a L{URL} pointing at the same location as the request URI but with an https scheme if the wrapped resource has a C{needsSecure} attribute with a true value and the request is over http. """ SSLPort(store=self.store, factory=self.urlGenerator, portNumber=443) request = FakeRequest( isSecure=False, uri='/bar/baz', currentSegments=['bar', 'baz']) self.resource.needsSecure = True result = self.wrapper.renderHTTP(request) self.assertEqual( result, URL('https', self.urlGenerator.hostname, ['bar', 'baz'])) def test_renderHTTP(self): """ L{SecuringWrapper.renderHTTP} returns the result of the wrapped resource's C{renderHTTP} method if the wrapped resource does not have a C{needsSecure} attribute with a true value. """ request = FakeRequest( isSecure=False, uri='/bar/baz', currentSegments=['bar', 'baz']) result = self.wrapper.renderHTTP(request) self.assertIdentical(self.resource.renderedWithContext, request) self.assertEqual(result, self.content) def test_renderHTTPS(self): """ L{SecuringWrapper.renderHTTP} returns the result of the wrapped resource's C{renderHTTP} method if it is called with a secure request. """ request = FakeRequest(isSecure=True) result = self.wrapper.renderHTTP(request) self.assertIdentical(self.resource.renderedWithContext, request) self.assertEqual(result, self.content) def test_renderHTTPCannotSecure(self): """ L{SecuringWrapper.renderHTTP} returns the result of the wrapped resource's C{renderHTTP} method if it is invoked over http but there is no https location available. """ request = FakeRequest(isSecure=False) result = self.wrapper.renderHTTP(request) self.assertIdentical(self.resource.renderedWithContext, request) self.assertEqual(result, self.content) def test_childLocateChild(self): """ L{_SecureWrapper.locateChild} returns a L{Deferred} which is called back with the result of the wrapped resource's C{locateChild} method wrapped in another L{_SecureWrapper}. """ segments = ('foo', 'bar') request = FakeRequest() wrapper = _SecureWrapper(self.urlGenerator, self.resource) result = wrapper.locateChild(request, segments) def locatedChild((resource, segments)): self.assertTrue(isinstance(resource, _SecureWrapper)) self.assertIdentical(resource.wrappedResource, self.child) self.assertEqual(segments, self.childSegments) result.addCallback(locatedChild) return result def test_notFound(self): """ A L{_SecureWrapper.locateChild} lets L{NotFound} results from the wrapped resource pass through. """ segments = ('foo', 'bar') request = FakeRequest() self.resource.childResource = None self.resource.childSegments = () wrapper = _SecureWrapper(self.urlGenerator, self.resource) result = wrapper.locateChild(request, segments) def locatedChild(result): self.assertIdentical(result, NotFound) result.addCallback(locatedChild) return result def test_adaption(self): """ A L{_SecureWrapper} constructed with an object which does not provide L{IResource} adapts it to L{IResource} and operates on the result. """ notResource = NotResource() wrapper = _SecureWrapper(self.urlGenerator, notResource) self.assertTrue(isinstance(wrapper.wrappedResource, NotResourceAdapter)) self.assertIdentical(wrapper.wrappedResource.notResource, notResource) class AthenaResourcesTestCase(TestCase): """ Test aspects of L{GenericNavigationAthenaPage}. """ hostname = 'test-mantissa-live-page-mixin.example.com' def _preRender(self, resource): """ Test helper which executes beforeRender on the given resource. This is used on live resources so that they don't start message queue timers. """ ctx = WovenContext() req = FakeRequest(headers={'host': self.hostname}) ctx.remember(req, IRequest) resource.beforeRender(ctx) def makeLivePage(self): """ Create a MantissaLivePage instance for testing. """ siteStore = Store(filesdir=self.mktemp()) Mantissa().installSite(siteStore, self.hostname.decode('ascii'), u"", False) return MantissaLivePage(ISiteURLGenerator(siteStore)) def test_transportRoot(self): """ The transport root should always point at the '/live' transport root provided to avoid database interaction while invoking the transport. """ livePage = self.makeLivePage() self.assertEquals(flatten(livePage.transportRoot), 'http://localhost/live') def test_debuggableMantissaLivePage(self): """ L{MantissaLivePage.getJSModuleURL}'s depends on state from page rendering, but it should provide a helpful error message in the case where that state has not yet been set up. """ livePage = self.makeLivePage() self.assertRaises(NotImplementedError, livePage.getJSModuleURL, 'Mantissa') def test_beforeRenderSetsModuleRoot(self): """ L{MantissaLivePage.beforeRender} should set I{_moduleRoot} to the C{__jsmodule__} child of the URL returned by the I{rootURL} method of the L{WebSite} it wraps. """ receivedRequests = [] root = URL(netloc='example.com', pathsegs=['a', 'b']) class FakeWebSite(object): def rootURL(self, request): receivedRequests.append(request) return root request = FakeRequest() page = MantissaLivePage(FakeWebSite()) page.beforeRender(request) self.assertEqual(receivedRequests, [request]) self.assertEqual(page._moduleRoot, root.child('__jsmodule__')) def test_getJSModuleURL(self): """ L{MantissaLivePage.getJSModuleURL} should return a child of its C{_moduleRoot} attribute of the form:: _moduleRoot//Package.ModuleName """ module = 'Mantissa' url = URL(scheme='https', netloc='example.com', pathsegs=['foo']) page = MantissaLivePage(None) page._moduleRoot = url jsDir = FilePath(__file__).parent().parent().child("js") modulePath = jsDir.child(module).child("__init__.js") moduleContents = modulePath.open().read() expect = sha1(moduleContents).hexdigest() self.assertEqual(page.getJSModuleURL(module), url.child(expect).child(module)) def test_jsCaching(self): """ Rendering a L{MantissaLivePage} causes each of its dependent modules to be loaded at most once. """ _realdeps = AthenaModule.dependencies def dependencies(self): self.count = getattr(self, 'count', 0) + 1 return _realdeps(self) self.patch(AthenaModule, 'dependencies', dependencies) module = jsDeps.getModuleForName('Mantissa.Test.Dummy.DummyWidget') module.count = 0 root = URL(netloc='example.com', pathsegs=['a', 'b']) class FakeWebSite(object): def rootURL(self, request): return root def _renderPage(): page = MantissaLivePage(FakeWebSite()) element = LiveElement(stan(tags.span(render=tags.directive('liveElement')))) element.setFragmentParent(page) element.jsClass = u'Mantissa.Test.Dummy.DummyWidget' page.docFactory = stan([tags.span(render=tags.directive('liveglue')), element]) ctx = WovenContext() req = FakeRequest(headers={'host': self.hostname}) ctx.remember(req, IRequest) page.beforeRender(ctx) page.renderHTTP(ctx) page._messageDeliverer.close() # Make sure we have a fresh dependencies memo. self.patch(theHashModuleProvider, 'depsMemo', {}) _renderPage() self.assertEqual(module.count, 1) _renderPage() self.assertEqual(module.count, 1) class APIKeyTestCase(TestCase): """ Tests for L{APIKey}. """ def setUp(self): """ Make a store. """ self.store = Store() def test_getKeyForAPINone(self): """ If there is no existing key for the named API, L{APIKey.getKeyForAPI} should return C{None}. """ self.assertIdentical( APIKey.getKeyForAPI(self.store, u'this is an API name.'), None) def test_getKeyForAPIExisting(self): """ If there is an existing key for the named API, L{APIKey.getKeyForAPI} should return it. """ theAPIName = u'this is an API name.' existingAPIKey = APIKey( store=self.store, apiName=theAPIName, apiKey=u'this is an API key.') self.assertIdentical( existingAPIKey, APIKey.getKeyForAPI(self.store, theAPIName)) def test_setKeyForAPINew(self): """ If there is no existing key for the named API, L{APIKey.setKeyForAPI} should create a new L{APIKey} item. """ theAPIKey = u'this is an API key.' theAPIName = u'this is an API name.' apiKey = APIKey.setKeyForAPI( self.store, theAPIName, theAPIKey) self.assertIdentical(apiKey, self.store.findUnique(APIKey)) self.assertEqual(theAPIKey, apiKey.apiKey) self.assertEqual(theAPIName, apiKey.apiName) def test_setKeyForAPIExisting(self): """ If there is an existing for the named API, L{APIKey.setKeyForAPI} should update its I{apiKey} attribute. """ theAPIKey = u'this is an API key.' theAPIName = u'this is an API name.' existingAPIKey = APIKey( store=self.store, apiName=theAPIName, apiKey=theAPIKey) newAPIKey = u'this is a new API key' returnedAPIKey = APIKey.setKeyForAPI( self.store, theAPIName, newAPIKey) self.assertIdentical(existingAPIKey, returnedAPIKey) self.assertEqual(existingAPIKey.apiName, theAPIName) self.assertEqual(existingAPIKey.apiKey, newAPIKey) class VirtualHostWrapperTests(TestCase): """ Tests for L{VirtualHostWrapper}. """ # XXX only integration tests cover L{VirtualHostWrapper.locateChild}. That # is important functionality, it should be covered here. def test_nonSubdomain(self): """ L{VirtualHostWrapper.subdomain} returns C{None} when passed a hostname which is not a subdomain of a domain of the site. """ site = Store() wrapper = VirtualHostWrapper(site, None, None) self.assertIdentical(wrapper.subdomain("example.com"), None) def test_subdomain(self): """ L{VirtualHostWrapper.subdomain} returns a two-tuple of a username and a domain name when passed a hostname which is a subdomain of a known domain. """ site = Store() wrapper = VirtualHostWrapper(site, None, None) userbase.LoginMethod( store=site, account=site, protocol=u'*', internal=True, verified=True, localpart=u'alice', domain=u'example.com') self.assertEqual( wrapper.subdomain("bob.example.com"), ("bob", "example.com")) def test_wwwSubdomain(self): """ L{VirtualHostWrapper.subdomain} returns C{None} when passed a hostname which is the I{www} subdomain of a domain of the site. """ site = Store() wrapper = VirtualHostWrapper(site, None, None) userbase.LoginMethod( store=site, account=site, protocol=u'*', internal=True, verified=True, localpart=u'alice', domain=u'example.com') self.assertIdentical(wrapper.subdomain("www.example.com"), None) def test_subdomainWithPort(self): """ L{VirtualHostWrapper.subdomain} handles hostnames with a port component as if they did not have a port component. """ site = Store() wrapper = VirtualHostWrapper(site, None, None) userbase.LoginMethod( store=site, account=site, protocol=u'*', internal=True, verified=True, localpart=u'alice', domain=u'example.com') self.assertEqual( wrapper.subdomain("bob.example.com:8080"), ("bob", "example.com")) PK9F+[^xmantissa/test/validation.py """ Test utilities providing coverage for implementations of Mantissa interfaces. """ from epsilon.descriptor import requiredAttribute from xmantissa.webtheme import XHTMLDirectoryTheme class XHTMLDirectoryThemeTestsMixin: """ Mixin defining tests for themes based on L{XHTMLDirectoryTheme}. Mix this in to a L{twisted.trial.unittest.TestCase} and set C{theme} and C{offering}. @ivar theme: An instance of the theme to be tested. Set this. @ivar offering: The offering which includes the theme. Set this. """ theme = requiredAttribute('theme') offering = requiredAttribute('offering') def test_stylesheet(self): """ The C{stylesheetLocation} of the theme being tested identifies an existing file beneath the offering's static content path. """ self.assertTrue(isinstance(self.theme, XHTMLDirectoryTheme)) self.assertEqual(self.theme.stylesheetLocation[0], 'static') self.assertEqual(self.theme.stylesheetLocation[1], self.offering.name) path = self.offering.staticContentPath for segment in self.theme.stylesheetLocation[2:]: path = path.child(segment) self.assertTrue(path.exists(), "Indicated stylesheet %r does not exist" % (path,)) PK$eF݃كك"xmantissa/test/test_scrolltable.py """ This module includes tests for the L{xmantissa.scrolltable} module. """ from re import escape from epsilon.hotfix import require require("twisted", "trial_assertwarns") from zope.interface import implements from twisted.trial import unittest from twisted.trial.util import suppress as SUPPRESS from axiom.store import Store from axiom.item import Item from axiom.attributes import integer, text from axiom.dependency import installOn from axiom.test.util import QueryCounter from xmantissa.webapp import PrivateApplication from xmantissa.ixmantissa import IWebTranslator, IColumn from xmantissa.error import Unsortable from xmantissa.scrolltable import ( InequalityModel, ScrollableView, ScrollingFragment, ScrollingElement, SequenceScrollingFragment, StoreIDSequenceScrollingFragment, AttributeColumn, UnsortableColumnWrapper, UnsortableColumn) _unsortableColumnSuppression = SUPPRESS( message=escape( "Use UnsortableColumnWrapper(AttributeColumn(*a, **kw)) " "instead of UnsortableColumn(*a, **kw)."), category=DeprecationWarning) class DataThunk(Item): a = integer() b = integer() c = text() class DataThunkWithIndex(Item): """ Another testing utility, similar to L{DataThunk}, but with an indexed attribute, so that sorting complexity is independent of the number of instances which exist, so that performance testing can be done. """ a = integer(indexed=True) class ScrollTestMixin(object): def setUp(self): self.store = Store() installOn(PrivateApplication(store=self.store), self.store) self.six = DataThunk(a=6, b=8157, c=u'six', store=self.store) self.three = DataThunk(a=3, b=821375, c=u'three', store=self.store) self.seven = DataThunk(a=7, b=4724, c=u'seven', store=self.store) self.eight = DataThunk(a=8, b=61, c=u'eight', store=self.store) self.one = DataThunk(a=1, b=435716, c=u'one', store=self.store) self.two = DataThunk(a=2, b=67145, c=u'two', store=self.store) self.four = DataThunk(a=4, b=6327, c=u'four', store=self.store) self.five = DataThunk(a=5, b=91856, c=u'five', store=self.store) self.scrollFragment = self.getScrollFragment() def test_performQueryAscending(self): """ Test that some simple ranges can be correctly retrieved when the sort order is ascending on the default column. """ self.scrollFragment.isAscending = True for low, high in [(0, 2), (1, 3), (2, 4)]: self.assertEquals( self.scrollFragment.performQuery(low, high), [self.five, self.six, self.seven, self.eight][low:high]) def test_performQueryDescending(self): """ Like L{test_performQueryAscending} but for the descending sort order. """ self.scrollFragment.isAscending = False for low, high in [(0, 2), (1, 3), (2, 4)]: self.assertEquals( self.scrollFragment.performQuery(low, high), [self.eight, self.seven, self.six, self.five][low:high]) class ScrollingFragmentTestCase(ScrollTestMixin, unittest.TestCase): """ Test cases which simulate various client behaviors to exercise the legacy L{ScrollingFragment}. """ def getScrollFragment(self): sf = ScrollingFragment( self.store, DataThunk, DataThunk.a > 4, [DataThunk.b, DataThunk.c], DataThunk.a) sf.linkToItem = lambda ign: None return sf def testGetTwoChunks(self): self.assertEquals( self.scrollFragment.requestRowRange(0, 2), [{'c': u'five', 'b': 91856}, {'c': u'six', 'b': 8157}]) self.assertEquals( self.scrollFragment.requestRowRange(2, 4), [{'c': u'seven', 'b': 4724}, {'c': u'eight', 'b': 61}]) self.scrollFragment.resort('b') self.assertEquals(self.scrollFragment.requestRowRange(0, 2), [{'c': u'eight', 'b': 61}, {'c': u'seven', 'b': 4724}]) self.assertEquals(self.scrollFragment.requestRowRange(2, 4), [{'c': u'six', 'b': 8157}, {'c': u'five', 'b': 91856}]) def testSortsOnFirstSortable(self): """ Test that the scrolltable sorts on the first sortable column """ sf = ScrollingFragment( self.store, DataThunk, None, (UnsortableColumn(DataThunk.a), DataThunk.b)) self.assertEquals(sf.currentSortColumn, DataThunk.b) testSortsOnFirstSortable.suppress = [_unsortableColumnSuppression] def testSortsOnFirstSortable2(self): """ Same as L{testSortsOnFirstSortable}, but for the case where the first sortable column is the first in the column list """ sf = ScrollingFragment( self.store, DataThunk, None, (DataThunk.a, UnsortableColumn(DataThunk.b))) self.assertIdentical(sf.currentSortColumn.sortAttribute(), DataThunk.a) testSortsOnFirstSortable2.suppress = [_unsortableColumnSuppression] def testTestNoSortables(self): """ Test that the scrolltable can handle the case where all columns are unsortable """ sf = ScrollingFragment( self.store, DataThunk, None, (UnsortableColumn(DataThunk.a), UnsortableColumn(DataThunk.b))) self.assertEquals(sf.currentSortColumn, None) testTestNoSortables.suppress = [_unsortableColumnSuppression] def test_unsortableColumnType(self): """ L{UnsortableColumn.getType} should return the same value as L{AttributeColumn.getType} for a particular attribute. """ self.assertEqual( AttributeColumn(DataThunk.a).getType(), UnsortableColumn(DataThunk.a).getType()) test_unsortableColumnType.suppress = [_unsortableColumnSuppression] def test_unsortableColumnDeprecated(self): """ L{UnsortableColumn} is a deprecated almost-alias for L{UnsortableColumnWrapper}. """ self.assertWarns( DeprecationWarning, "Use UnsortableColumnWrapper(AttributeColumn(*a, **kw)) " "instead of UnsortableColumn(*a, **kw).", __file__, lambda: UnsortableColumn(DataThunk.a)) def testUnsortableColumnWrapper(self): """ Test that an L{UnsortableColumnWrapper} wrapping an L{AttributeColumn} is treated the same as L{UnsortableColumn} """ sf = ScrollingFragment( self.store, DataThunk, None, (UnsortableColumnWrapper(AttributeColumn(DataThunk.a)), DataThunk.b)) self.assertEquals(sf.currentSortColumn, DataThunk.b) def test_allUnsortableSortMetadata(self): """ Test that C{getTableMetadata} is correct with respect to the sortability of columns """ sf = ScrollingFragment( self.store, DataThunk, None, (UnsortableColumn(DataThunk.a), UnsortableColumn(DataThunk.b))) meta = sf.getTableMetadata() cols = meta[1] self.assertEquals(cols['a'][1], False) self.assertEquals(cols['b'][1], False) test_allUnsortableSortMetadata.suppress = [_unsortableColumnSuppression] def test_oneSortableSortMetadata(self): """ Same as L{test_allUnsortableSortMetadata}, but with one sortable column """ sf = ScrollingFragment( self.store, DataThunk, None, (DataThunk.a, UnsortableColumn(DataThunk.b))) meta = sf.getTableMetadata() cols = meta[1] self.assertEquals(cols['a'][1], True) self.assertEquals(cols['b'][1], False) test_oneSortableSortMetadata.suppress = [_unsortableColumnSuppression] class SequenceScrollingFragmentTestCase(ScrollTestMixin, unittest.TestCase): """ Run the general scrolling tests against L{SequenceScrollingFragment}. """ def getScrollFragment(self): return SequenceScrollingFragment( self.store, [self.five, self.six, self.seven, self.eight], [DataThunk.b, DataThunk.c], DataThunk.a) class StoreIDSequenceScrollingFragmentTestCase(ScrollTestMixin, unittest.TestCase): """ Run the general scrolling tests against L{StoreIDSequenceScrollingFragmentTestCase}. """ def getScrollFragment(self): return StoreIDSequenceScrollingFragment( self.store, [self.five.storeID, self.six.storeID, self.seven.storeID, self.eight.storeID], [DataThunk.b, DataThunk.c], DataThunk.a) class TestableInequalityModel(InequalityModel): """ Helper for InequalityModel tests which implements the row construction hook. """ def constructRows(self, items): """ Squash the given items iterator into a list and return it so it can be inspected by tests. """ return list(items) class InequalityModelTestCase(unittest.TestCase): """ Tests for the inequality-based scrolling model implemented by L{InequalityModel}. """ def setUp(self): """ Set up an inequality model (by way of L{TestableInequalityModel}) backed by a user store with some sample data, and an L{IWebTranslator} powerup, to provide a somewhat realistic test setup. The data provided has some duplicates in the sort column, and it is intentionally inserted out of order, so that storeID order and sort column ordering will not coincide. """ self.store = Store() privApp = PrivateApplication(store=self.store) installOn(privApp, self.store) self.data = [] for a, b, c in [(9, 1928, u'nine'), (1, 983, u'one'), (8, 843, u'eight'), (2, 827, u'two'), # (8, 1874, u'eight (DUP)'), (2, 294, u'two (DUP)'), (7, 18, u'seven'), (3, 19, u'three'), (6, 218, u'six'), (4, 2198, u'four'), (5, 1982, u'five'), (0, 10, u'zero')]: self.data.append(DataThunk(store=self.store, a=a, b=b, c=c)) self.data.sort(key=lambda item: (item.a, item.storeID)) self.model = TestableInequalityModel( self.store, DataThunk, None, [DataThunk.a, DataThunk.b, DataThunk.c], DataThunk.a, True) def test_noSortableColumns(self): """ Attempting to construct a L{InequalityModel} without any sortable columns should result in a L{Unsortable} exception being thrown. """ makeModel = lambda: InequalityModel(self.store, DataThunk, None, [UnsortableColumn(DataThunk.a)], None, True) self.assertRaises(Unsortable, makeModel) test_noSortableColumns = [_unsortableColumnSuppression] def test_rowsAtStart(self): """ Verify that retrieving rows after the "None" value will retrieve rows at the start of the order. """ count = 5 expected = self.data[:count] self.assertEquals(self.model.rowsAfterValue(None, count), expected) def test_rowsAtEnd(self): """ Verify that retrieving rows before the "None" value will retrieve rows at the end of the sort order. """ count = 5 expected = self.data[-count:] self.assertEquals(self.model.rowsBeforeValue(None, count), expected) def test_rowsAfterValue(self): """ Verify that rows after a particular value with a particular ordering are returned by L{InequalityModel.rowsAfterValue}. """ for count in range(1, len(self.data)): for value in range(len(self.data)): expected = self.data[value:value + count] self.assertEqual( self.model.rowsAfterValue(value, count), expected) def test_rowsAfterRow(self): """ Verify that the exposed method, L{rowsAfterRow}, returns results comparable to the logical method, L{rowsAfterItem}. """ args = [] def rowsAfterItem(item, count): args.append((item, count)) self.model.rowsAfterItem = rowsAfterItem theItem = object() theTranslator = FakeTranslator() theTranslator.fromWebID = lambda webID: theItem self.model.webTranslator = theTranslator self.model.rowsAfterRow({u'__id__': u'webid!'}, 10) self.assertEqual(args, [(theItem, 10)]) def test_rowsBeforeRow(self): """ Verify that the exposed method, L{rowsBeforeRow}, return results comparable to the logical method, L{rowsBeforeItem}. """ args = [] def rowsBeforeItem(item, count): args.append((item, count)) self.model.rowsBeforeItem = rowsBeforeItem theItem = object() theTranslator = FakeTranslator() theTranslator.fromWebID = lambda webID: theItem self.model.webTranslator = theTranslator self.model.rowsBeforeRow({u'__id__': u'webid!'}, 10) self.assertEqual(args, [(theItem, 10)]) def test_rowsAfterItem(self): """ Verify that rows after a particular item with a particular ordering are returned by L{InequalityModel.rowsAfterItem}. """ for count in range(1, len(self.data)): for value in range(1, len(self.data)): expected = self.data[value:value + count] self.assertEqual( self.model.rowsAfterItem(self.data[value - 1], count), expected) def test_rowsBeforeValue(self): """ Like L{test_rowsAfterValue} but for data going in the opposite direction. """ for count in range(1, len(self.data)): for value in range(len(self.data)): expected = self.data[max(value - count, 0):value] self.assertEqual( self.model.rowsBeforeValue(value, count), expected) def test_rowsBeforeItem(self): """ Like L{test_rowsAfterItem} but for data going in the opposite direction. """ for count in range(1, len(self.data)): for value in range(len(self.data)): expected = self.data[max(value - count, 0):value] self.assertEqual( self.model.rowsBeforeItem(self.data[value], count), expected) class FakeTranslator: """ A fake implementation of the L{IWebTranslator} interface, for tests which do not use this. """ implements(IWebTranslator) def fromWebID(self, webID): return None def toWebID(self, item): return "none" def linkTo(self, storeID): return "none" def linkFrom(self, webID): return 0 class ScrollingElementTests(unittest.TestCase): """ Test cases for the ultimate client-facing subclass, L{ScrollingElement}. """ def test_deprecatedMissingWebTranslator(self): """ Instantiating a L{ScrollingElement} without supplying an L{IWebTranslator}, either explicitly or via a store powerup, will emit a L{DeprecationWarning} explaining that this should not be done. """ def makeScrollingElement(): return ScrollingElement( Store(), DataThunk, None, [DataThunk.a], DataThunk.a, True) self.assertWarns( DeprecationWarning, "No IWebTranslator plugin when creating Scrolltable - broken " "configuration, now deprecated! Try passing webTranslator " "keyword argument.", __file__, makeScrollingElement) def test_callComparableValue(self): """ L{ScrollingElement} should attempt to call L{IColumn.toComparableValue} to translate input from JavaScript if it is provided by the sort column. """ calledWithValues = [] column = AttributeColumn(DataThunk.a) column.toComparableValue = lambda v: (calledWithValues.append(v), 0)[1] scrollingElement = ScrollingElement( Store(), DataThunk, None, [column], webTranslator=FakeTranslator()) scrollingElement.rowsAfterValue(16, 10) self.assertEqual(calledWithValues, [16]) calledWithValues.pop() scrollingElement.rowsBeforeValue(11, 10) self.assertEqual(calledWithValues, [11]) def test_deprecatedNoToComparableValue(self): """ If L{IColumn.toComparableValue} is I{not} provided by the sort column, then L{ScrollingElement} should notify the developer of a deprecation warning but default to using the value itself. """ class FakeComparator: def __ge__(fc, other): self.shouldBeFakeValue = other __lt__ = __ge__ theFakeComparator = FakeComparator() class FakeOldColumn: implements(IColumn) # but not really; we're missing something! attributeID = 'fake' def sortAttribute(self): return theFakeComparator scrollingElement = ScrollingElement( Store(), DataThunk, None, [FakeOldColumn()], webTranslator=FakeTranslator()) # Now, completely hamstring the implementation; we are interested in # something very specific here, so we just want to capture one value. # (Passing it further on, for example to Axiom, would not result in # testing anything useful, since it is the individual column's # responsibility to yield useful values here anyway, and this test is # explicitly for the case where it's *not* doing that, but happened to # work before anyway!) scrollingElement.inequalityQuery = lambda a, b, c: None scrollingElement.constructRows = lambda nothing : () for lenientMethod in [scrollingElement.rowsBeforeValue, scrollingElement.rowsAfterValue]: fakeValue = object() self.assertWarns(DeprecationWarning, "IColumn implementor %s.FakeOldColumn does not " "implement method toComparableValue. This is " "required since Mantissa 0.6.6." % (__name__,), __file__, lenientMethod, fakeValue, 10) self.assertIdentical(fakeValue, self.shouldBeFakeValue) def test_initialWidgetArguments(self): """ Verify that the arguments the client widget expects: the name of the current sort column, a list of the available data columns, and the sort order. """ s = Store() testElement = ScrollingElement(s, DataThunk, None, [DataThunk.a, DataThunk.c], DataThunk.a, True, FakeTranslator()) self.assertEqual(testElement.getInitialArguments(), [u"a", [{u"name": u"a", u"type": u"integer"}, {u"name": u"c", u"type": u"text"}], True]) def test_missingTypeDefaultsToText(self): """ When constructed with an L{IColumn} which returns C{None} from its C{getType} method, L{ScrollingElement} should use the default of C{text} for that column's type. """ class UntypedColumn(object): implements(IColumn) attributeID = 'foo' def sortAttribute(self): return None def getType(self): return None column = UntypedColumn() scroller = ScrollingElement( None, None, None, [DataThunk.a, column], None, webTranslator=object()) attribute, columnList, ascending = scroller.getInitialArguments() self.assertEqual( columnList, [{u'type': u'integer', u'name': u'a'}, {u'type': u'text', u'name': u'foo'}]) class InequalityModelDuplicatesTestCase(unittest.TestCase): """ Similar to L{InequalityModelTestCase}, but test cases where there are multiple rows with the same value for the sort key. """ def setUp(self): self.store = Store() privApp = PrivateApplication(store=self.store) installOn(privApp, self.store) # Create some data to test with in an order which is not naturally # sorted in any way. # 4 1s, 4 0s, 4 2s self.data = [] for number in [1, 0, 2, 2, 0, 1, 1, 0, 2, 0, 2, 1]: self.data.append(DataThunk(store=self.store, a=number)) # But order it for the sake of simplicity while testing. self.data.sort(key=lambda item: (item.a, item.storeID)) self.model = TestableInequalityModel( self.store, DataThunk, None, [DataThunk.a, DataThunk.b, DataThunk.c], DataThunk.a, True) def test_rowsAfterValue(self): """ Test that when there are duplicate values in the sort column, L{InequalityModel.rowsAfterValue} returns all the rows (up to the indicated maximum) with the value it is passed and then any rows with values greater than the given value. """ self.assertEqual( self.model.rowsAfterValue(1, 3), self.data[4:4+3]) def test_rowsAfterItem(self): """ Like L{test_rowsAfterValue}, but for L{InequalityModel.rowsAfterItem} method. """ # Test that starting at the first item with a particular value and # crossing over to another value works properly. self.assertEqual( self.model.rowsAfterItem(self.data[0], 3), self.data[1:4]) # Test that starting at an item "in the middle" of a particular value # and crossing over to another value works properly. self.assertEqual( self.model.rowsAfterItem(self.data[1], 3), self.data[2:5]) # Test that starting at the first item with a particular value and not # requesting enough rows to cross into another value works properly. self.assertEqual( self.model.rowsAfterItem(self.data[0], 1), [self.data[1]]) def test_rowsBeforeValue(self): """ Like L{test_rowsAfterValue}, but for L{InequalityModel.rowsBeforeValue} """ self.assertEqual( self.model.rowsBeforeValue(2, 3), self.data[5:8]) def test_rowsBeforeItem(self): """ Like L{test_rowsAfterItem}, but for L{InequalityModel.rowsBeforeItem}. """ for x in range(len(self.data)): self.assertEqual( self.model.rowsBeforeItem(self.data[x], 20), self.data[:x]) class InequalityPerformanceTests(unittest.TestCase): """ Tests for the complexity and runtime costs of the methods of L{InequalityModel}. """ def setUp(self): self.store = Store() privApp = PrivateApplication(store=self.store) installOn(privApp, self.store) self.model = TestableInequalityModel( self.store, DataThunkWithIndex, None, [DataThunkWithIndex.a], DataThunkWithIndex.a, True, privApp) self.counter = QueryCounter(self.store) self.data = [] for i in range(4): self.data.append(DataThunkWithIndex(store=self.store, a=i)) def rowsAfterValue(self, value, count): return self.counter.measure(self.model.rowsAfterValue, value, count) def rowsAfterItem(self, item, count): return self.counter.measure(self.model.rowsAfterItem, item, count) def rowsBeforeValue(self, value, count): return self.counter.measure(self.model.rowsBeforeValue, value, count) def rowsBeforeItem(self, item, count): return self.counter.measure(self.model.rowsBeforeItem, item, count) def test_rowsAfterValue(self): """ Verify that the cost of L{InequalityModel.rowsAfterValue} is independent of the total number of rows in the table being queried, as long as that number is greater than the number of rows requested. """ first = self.rowsAfterValue(1, 2) DataThunkWithIndex(store=self.store, a=4) second = self.rowsAfterValue(1, 2) self.assertEqual(first, second) def test_rowsAfterValueWithDuplicatesBeforeStart(self): """ Like L{test_rowsAfterValue}, but verify the behavior in the face of duplicate rows before the start value. """ first = self.rowsAfterValue(1, 2) DataThunkWithIndex(store=self.store, a=0) second = self.rowsAfterValue(1, 2) self.assertEqual(first, second) def test_rowsAfterValueWithDuplicatesAtStart(self): """ Like L{test_rowsAfterValue}, but verify the behavior in the face of duplicate rows exactly at the start value. """ first = self.rowsAfterValue(1, 2) DataThunkWithIndex(store=self.store, a=1) second = self.rowsAfterValue(1, 2) self.assertEqual(first, second) def test_rowsAfterValueWithDuplicatesInResult(self): """ Like L{test_rowsAfterValue}, but verify the behavior in the face of duplicate rows in the result set. """ first = self.rowsAfterValue(1, 2) DataThunkWithIndex(store=self.store, a=2) second = self.rowsAfterValue(1, 2) self.assertEqual(first, second) def test_rowsAfterValueWithDuplicatesAfter(self): """ Like L{test_rowsAfterValue}, but verify the behavior in the face of duplicate rows past the end of the result set. """ first = self.rowsAfterValue(1, 2) DataThunkWithIndex(store=self.store, a=4) second = self.rowsAfterValue(1, 2) self.assertEqual(first, second) def test_rowsAfterItem(self): """ Like L{test_rowsAfterValue}, but for L{InequalityModel.rowsAfterItem}. """ first = self.rowsAfterItem(self.data[0], 2) DataThunkWithIndex(store=self.store, a=4) second = self.rowsAfterItem(self.data[0], 2) self.assertEqual(first, second) def test_rowsAfterItemWithDuplicatesBeforeStart(self): """ Like L{test_rowsAfterValueWithDuplicatesBeforeStart}, but for L{InequalityModel.rowsAfterItem}. """ DataThunkWithIndex(store=self.store, a=-1) first = self.rowsAfterItem(self.data[0], 2) DataThunkWithIndex(store=self.store, a=-1) second = self.rowsAfterItem(self.data[0], 2) self.assertEqual(first, second) def test_rowsAfterItemWithDuplicatesAtStart(self): """ Like L{test_rowsAfterValueWithDuplicatesAtStart}, but for L{InequalityModel.rowsAfterItem}. """ first = self.rowsAfterItem(self.data[0], 2) DataThunkWithIndex(store=self.store, a=0) second = self.rowsAfterItem(self.data[0], 2) self.assertEqual(first, second) test_rowsAfterItemWithDuplicatesAtStart.todo = ( "Index scan to find appropriate storeID starting point once the " "value index has been used to seek to /near/ the correct starting " "place causes this to be O(N) on the number of rows with duplicate " "values.") def test_rowsAfterItemWithDuplicatesInResult(self): """ Like L{test_rowsAfterValueWithDuplicatesInResult}, but for L{InequalityModel.rowsAfterItem}. """ first = self.rowsAfterItem(self.data[0], 2) DataThunkWithIndex(store=self.store, a=1) second = self.rowsAfterItem(self.data[0], 2) self.assertEqual(first, second) def test_rowsAfterItemWithDuplicatesAfter(self): """ Like L{test_rowsAfterValueWithDuplicatesAfter}, but for L{InequalityModel.rowsAfterItem}. """ first = self.rowsAfterItem(self.data[0], 2) DataThunkWithIndex(store=self.store, a=3) second = self.rowsAfterItem(self.data[0], 2) self.assertEqual(first, second) def test_rowsBeforeValue(self): """ Like L{test_rowsAfterValue}, but for L{InequalityModel.rowsBeforeValue}. """ first = self.rowsBeforeValue(2, 2) DataThunkWithIndex(store=self.store, a=-1) second = self.rowsBeforeValue(2, 2) self.assertEqual(first, second) def test_rowsBeforeValueWithDuplicatesBeforeStart(self): """ Like L{test_rowsAfterValueWithDuplicatesBeforeStart}, but for L{InequalityModel.rowsBeforeValue}. """ first = self.rowsBeforeValue(2, 2) DataThunkWithIndex(store=self.store, a=3) second = self.rowsBeforeValue(2, 2) self.assertEqual(first, second) def test_rowsBeforeValueWithDuplicatesAtStart(self): """ Like L{test_rowsAfterValueWithDuplicatesAtStart}, but for L{InequalityModel.rowsBeforeValue}. """ first = self.rowsBeforeValue(2, 2) DataThunkWithIndex(store=self.store, a=2) second = self.rowsBeforeValue(2, 2) self.assertEqual(first, second) def test_rowsBeforeValueWithDuplicatesInResult(self): """ Like L{test_rowsAfterValueWithDuplicatesInResult}, but for L{InequalityModel.rowsBeforeValue}. """ first = self.rowsBeforeValue(2, 2) DataThunkWithIndex(store=self.store, a=1) second = self.rowsBeforeValue(2, 2) self.assertEqual(first, second) def test_rowsBeforeValueWithDuplicatesAfter(self): """ Like L{test_rowsAfterValueWithDuplicatesAfter}, but for L{InequalityModel.rowsBeforeValue}. """ first = self.rowsBeforeValue(2, 2) DataThunkWithIndex(store=self.store, a=0) second = self.rowsBeforeValue(2, 2) self.assertEqual(first, second) def test_rowsBeforeItem(self): """ Like L{test_rowsAfterItem}, but for L{InequalityModel.rowsBeforeItem}. """ first = self.rowsBeforeItem(self.data[3], 2) DataThunkWithIndex(store=self.store, a=-1) second = self.rowsBeforeItem(self.data[3], 2) self.assertEqual(first, second) def test_rowsBeforeItemWithDuplicatesBeforeStart(self): """ Like L{test_rowsAfterItemWithDuplicatesBeforeStart}, but for L{InequalityModel.rowsBeforeItem}. """ DataThunkWithIndex(store=self.store, a=4) first = self.rowsBeforeItem(self.data[3], 2) DataThunkWithIndex(store=self.store, a=4) second = self.rowsBeforeItem(self.data[3], 2) self.assertEqual(first, second) def test_rowsBeforeItemWithDuplicatesAtStart(self): """ Like L{test_rowsAfterItemWithDuplicatesAtStart}, but for L{Inequality.rowsBeforeItem}. """ first = self.rowsBeforeItem(self.data[3], 2) DataThunkWithIndex(store=self.store, a=3) second = self.rowsBeforeItem(self.data[3], 2) self.assertEqual(first, second) test_rowsBeforeItemWithDuplicatesAtStart.todo = ( "Index scan to find appropriate storeID starting point once the " "value index has been used to seek to /near/ the correct starting " "place causes this to be O(N) on the number of rows with duplicate " "values.") def test_rowsBeforeItemWithDuplicatesInResult(self): """ Like L{test_rowsAfterItemWithDuplicatesInResult}, but for L{Inequality.rowsBeforeItem}. """ first = self.rowsBeforeItem(self.data[3], 2) DataThunkWithIndex(store=self.store, a=2) second = self.rowsBeforeItem(self.data[3], 2) self.assertEqual(first, second) def test_rowsBeforeItemWithDuplicatesAfter(self): """ Like L{test_rowsAfterItemWithDuplicatesAfter}, but for L{InequalityModel.rowsBeforeItem}. """ first = self.rowsBeforeItem(self.data[3], 2) DataThunkWithIndex(store=self.store, a=0) second = self.rowsBeforeItem(self.data[3], 2) self.assertEqual(first, second) class UnsortableColumnWrapperTestCase(unittest.TestCase): """ Tests for L{UnsortableColumnWrapper} """ def test_unsortableColumnWrapper(self): attr = DataThunk.a col = AttributeColumn(attr) unsortableCol = UnsortableColumnWrapper(col) item = DataThunk(store=Store(), a=26) value = unsortableCol.extractValue(None, item) self.assertEquals(value, item.a) self.assertEquals(value, col.extractValue(None, item)) typ = unsortableCol.getType() self.assertEquals(typ, 'integer') self.assertEquals(typ, col.getType()) self.assertEquals(unsortableCol.sortAttribute(), None) PK,eF>xmantissa/test/test_people.py# Copyright 2008 Divmod, Inc. See LICENSE file for details """ Tests for L{xmantissa.people}. """ from __future__ import division import warnings from string import lowercase from twisted.python.reflect import qual from twisted.python.filepath import FilePath from twisted.trial import unittest from formless import nameToLabel from nevow.tags import div, slot from nevow.flat import flatten from nevow.athena import expose, LiveElement from nevow.page import renderer, Element from nevow.testutil import FakeRequest from nevow.taglibrary import tabbedPane from nevow import context from epsilon import extime from epsilon.extime import Time from epsilon.structlike import record from epsilon.hotfix import require require('twisted', 'trial_assertwarns') from axiom.store import Store, AtomicFile from axiom.dependency import installOn from axiom.item import Item from axiom.attributes import text, AND from axiom.errors import DeletionDisallowed from axiom import tags from axiom.userbase import LoginSystem from axiom.plugins.axiom_plugins import Create from axiom.plugins.mantissacmd import Mantissa from xmantissa.test.rendertools import renderLiveFragment, TagTestingMixin from xmantissa.scrolltable import UnsortableColumn, ScrollingElement from xmantissa.offering import installOffering from xmantissa import people from xmantissa.people import ( Organizer, Person, EmailAddress, AddPersonFragment, ImportPeopleWidget, Mugshot, PersonDetailFragment, PhoneNumber, PhoneNumberContactType, ReadOnlyPhoneNumberView, PersonScrollingFragment, OrganizerFragment, EditPersonView, BaseContactType, EmailContactType, _normalizeWhitespace, PostalAddress, PostalContactType, VIPPersonContactType, _PersonVIPStatus, getPersonURL, _stringifyKeys, makeThumbnail, _descriptiveIdentifier, ReadOnlyContactInfoView, PersonSummaryView, MugshotUploadForm, ORGANIZER_VIEW_STATES, MugshotResource, Notes, NotesContactType, ContactGroup, AllPeopleFilter, VIPPeopleFilter, TaggedPeopleFilter, MugshotURLColumn, _objectToName, ContactInfoOrganizerPlugin, PersonPluginView, _ElementWrapper, _organizerPluginName, SimpleReadOnlyView) from xmantissa.webapp import PrivateApplication from xmantissa.liveform import ( TEXT_INPUT, InputError, Parameter, LiveForm, ListChangeParameter, ListChanges, CreateObject, EditObject, FormParameter, ChoiceParameter, TEXTAREA_INPUT) from xmantissa.ixmantissa import ( IOrganizerPlugin, IContactType, IWebTranslator, IPeopleFilter, IColumn) from xmantissa.signup import UserInfo from xmantissa.test.peopleutil import ( PeopleFilterTestMixin, StubContactType, StubOrganizerPlugin, StubOrganizer, StubPerson, StubTranslator) from xmantissa.plugins.baseoff import baseOffering # the number of non-plugin IContactType implementations provided by Mantissa. builtinContactTypeCount = 5 # the number of non-plugin IPeopleFilter implementations provided by Mantissa builtinPeopleFilterCount = 2 class AllPeopleFilterTests(PeopleFilterTestMixin, unittest.TestCase): """ Tests for L{AllPeopleFilter}. """ peopleFilterClass = AllPeopleFilter peopleFilterName = 'All' def test_queryComparison(self): """ L{AllPeopleFilter}'s query comparison should include all people. """ self.assertIdentical( self.peopleFilterClass().getPeopleQueryComparison(Store()), None) class VIPPeopleFilterTests(PeopleFilterTestMixin, unittest.TestCase): """ Tests for L{VIPPeopleFilter}. """ peopleFilterClass = VIPPeopleFilter peopleFilterName = 'VIP' def test_queryComparison(self): """ L{VIPPeopleFilter}'s query comparison should include only VIP people. """ self.assertComparisonEquals(Person.vip == True) class TaggedPeopleFilterTests(unittest.TestCase): """ Tests for L{TaggedPeopleFilter}. """ # this TestCase doesn't inherit from PeopleFilterTestMixin because of the # constructor argument and more complicated query. def test_implementsInterface(self): """ L{TaggedPeopleFilter} should provide L{IPeopleFilter}. """ self.assertTrue( IPeopleFilter.providedBy(TaggedPeopleFilter(u'tag'))) def test_filterName(self): """ Our L{TaggedPeopleFilter}'s I{filterName} should be the tag passed to its constructor. """ self.assertEqual( TaggedPeopleFilter(u'test_filterName').filterName, u'test_filterName') def test_queryComparison(self): """ L{TaggedPeopleFilter}'s query comparison should include only people who have had a certain tag applied to them. """ actualComparison = TaggedPeopleFilter( u'test_queryOrdering').getPeopleQueryComparison(Store()) expectedComparison = AND( tags.Tag.object == Person.storeID, tags.Tag.name == u'test_queryOrdering') # none of the Axiom query objects have meaningful equality # comparisons, but their string representations are just as good to # compare. self.assertEqual( str(actualComparison), str(expectedComparison)) def emptyMantissaSiteStore(): """ Create and return a site store with the base mantissa offering installed on it. """ site = Store() installOffering(site, baseOffering, None) return site def emptyMantissaUserStore(): """ Create a site store with the base mantissa offering installed on it and return an empty store which has that as its parent. """ site = emptyMantissaSiteStore() user = Store() user.parent = site return user class PeopleUtilitiesTestCase(unittest.TestCase): """ Tests for module-level utility functions in L{xmantissa.people}. """ def test_stringifyKeys(self): """ Verify that L{_stringifyKeys} returns a dictionary which is the same as the input except for having C{str} keys. """ input = {u'a': u'b', u'b': u'c'} output = _stringifyKeys(input) self.assertEqual(len(output), 2) keys = output.keys() self.failUnless(isinstance(keys[0], str)) self.failUnless(isinstance(keys[1], str)) self.assertEqual(sorted(keys), ['a', 'b']) self.failUnless(isinstance(output['a'], unicode)) self.failUnless(isinstance(output['b'], unicode)) self.assertEqual(output['a'], u'b') self.assertEqual(output['b'], u'c') def _makeThumbnailPairs(self, inputSizes, outputSize): """ Generate a collection of L{makeThumbnail} input/output image pairs in various formats, for the given input sizes. """ try: from PIL import Image except ImportError: raise unittest.SkipTest('PIL is not available') formatsToModes = { 'JPEG': ['L', 'RGB'], 'PNG': ['1', 'L', 'P', 'RGB', 'RGBA'], } modesToWhite = { '1': 1, 'L': 0xFF, 'P': 0xFF, 'RGB': (0xFF, 0xFF, 0xFF), 'RGBA': (0xFF, 0xFF, 0xFF, 0xFF), } for format in formatsToModes: for mode in formatsToModes[format]: for inputSize in inputSizes: cause = ('Image.new(%r, %r) via %s' % (mode, inputSize, format)) (inFile, outFile) = (self.mktemp(), self.mktemp()) # Input image... image = Image.new(mode, inputSize) # Plot pixels along the diagonal to provoke aliasing. for i in xrange(min(inputSize)): image.putpixel((i, i), modesToWhite[mode]) image.save(file(inFile, 'w'), format) self.assertEqual(Image.open(inFile).mode, mode, cause) untouchedInput = file(inFile).read() # Output image... makeThumbnail(file(inFile), file(outFile, 'w'), outputSize, format) self.assertEqual(file(inFile).read(), untouchedInput, cause) yield (Image.open(inFile), Image.open(outFile), cause) def test_makeThumbnail(self): """ L{makeThumbnail} should scale images, preserving their aspect ratio, and expanding their color space if necessary. """ sizes = [(x, y) for x in [30, 60, 120] for y in [30, 60, 120] if 60 < max(x, y)] for (input, output, cause) in self._makeThumbnailPairs(sizes, 60): (x1, y1) = input.size (x2, y2) = output.size self.assertEquals(max(x2, y2), 60, cause) self.assertEquals(x2/y2, x1/y1, cause) expectedMode = {'1': 'L', 'P': 'RGB'}.get(input.mode, input.mode) self.assertEquals(output.mode, expectedMode, cause) self.assertEquals(output.format, input.format, cause) def test_makeThumbnailNoResize(self): """ L{makeThumbnail} should leave images under thumbnail size unchanged. """ sizes = [(x, y) for x in [30, 60] for y in [30, 60]] for (input, output, cause) in self._makeThumbnailPairs(sizes, 60): self.assertEquals(output.size, input.size, cause) self.assertEquals(output.mode, input.mode, cause) self.assertEquals(output.format, input.format, cause) def test_objectToName(self): """ L{_objectToName} should be able to figure out a helpful name more readable than the class name of an object. """ class MyNeatClass: pass self.assertEqual(_objectToName(MyNeatClass()), u'My Neat Class') def test_objectToNameObject(self): """ Similar to L{test_objectToName}, but for classes derived from C{object}. """ class MyNeatClass(object): pass self.assertEqual(_objectToName(MyNeatClass()), u'My Neat Class') def test_descriptiveIdentifier(self): """ Verify that L{_descriptiveIdentifier} returns the result of the C{descriptiveIdentifier} method if its passed an object that defines one. """ identifier = u'lol identifier' class MyContactType: def descriptiveIdentifier(self): return identifier self.assertEqual( _descriptiveIdentifier(MyContactType()), identifier) def test_noDescriptiveIdentifier(self): """ Verify that L{_descriptiveIdentifier} returns a sensible identifier based on the class name of the object it is passed, and issues a warning, if the object doesn't implement C{descriptiveIdentifier}. """ class MyContactType: pass self.assertEqual( _descriptiveIdentifier(MyContactType()), _objectToName(MyContactType())) self.assertWarns( PendingDeprecationWarning, "IContactType now has the 'descriptiveIdentifier'" " method, xmantissa.test.test_people.MyContactType" " did not implement it", people.__file__, lambda: _descriptiveIdentifier(MyContactType())) def test_organizerPluginName(self): """ L{_organizerPluginName} should return the value of the plugin's I{name} attribute if it is set. """ _name = u'organizer plugin name!' class OrganizerPlugin: name = _name self.assertEqual(_organizerPluginName(OrganizerPlugin()), _name) def test_noOrganizerPluginName(self): """ L{_organizerPluginName} should figure out a reasonable default, and issue a warning if the given plugin doesn't define a I{name} attribute. """ class NoOrganizerPluginName: pass self.assertEqual( _organizerPluginName(NoOrganizerPluginName()), _objectToName(NoOrganizerPluginName())) self.assertWarns( PendingDeprecationWarning, "IOrganizerPlugin now has the 'name' attribute and" " xmantissa.test.test_people.NoOrganizerPluginName" " does not define it", people.__file__, lambda: _organizerPluginName(NoOrganizerPluginName())) class MugshotUploadFormTestCase(unittest.TestCase): """ Tests for L{MugshotUploadForm}. """ def setUp(self): """ Construct a L{Person}, suitable for passing to L{MugshotUploadForm}'s constructor. """ user = emptyMantissaUserStore() # can't use mock objects because we need ITemplateNameResolver to # render MugshotUploadForm self.organizer = Organizer(store=user) installOn(self.organizer, user) self.person = Person(store=user, organizer=self.organizer) def test_callback(self): """ Verify that L{MugshotUploadForm} calls the supplied callback after a successful POST. """ cbGotMugshotArgs = [] def cbGotMugshot(contentType, file): cbGotMugshotArgs.append((contentType, file)) form = MugshotUploadForm(self.person, cbGotMugshot) theContentType = 'image/tiff' theFile = object() class FakeUploadField: type = theContentType file = theFile request = FakeRequest() request.method = 'POST' request.fields = {'uploaddata': FakeUploadField} ctx = context.PageContext( tag=form, parent=context.RequestContext( tag=request)) form.renderHTTP(ctx) self.assertEqual( cbGotMugshotArgs, [(u'image/tiff', theFile)]) def test_smallerMugshotURL(self): """ L{MugshotUploadForm.render_smallerMugshotURL} should return the correct URL. """ form = MugshotUploadForm(self.person, None) self.assertEqual( form.render_smallerMugshotURL(None, None), self.organizer.linkToPerson(self.person) + '/mugshot/smaller') class MugshotTestCase(unittest.TestCase): """ Tests for L{Mugshot}. """ def _doFromFileTest(self, store, person): """ Verify that the L{Mugshot} returned from L{Mugshot.fromFile} has the correct attribute values. """ newBody = store.newFilePath('newBody') newSmallerBody = store.newFilePath('newSmallerBody') newFormat = u'TIFF' def _makeThumbnail(cls, inputFile, person, format, smaller): if smaller: return newSmallerBody return newBody originalMakeThumbnail = Mugshot.makeThumbnail try: Mugshot.makeThumbnail = classmethod(_makeThumbnail) mugshot = Mugshot.fromFile( person, file(self.mktemp(), 'w'), newFormat) finally: Mugshot.makeThumbnail = originalMakeThumbnail # and no others should have been created self.assertEqual(store.count(Mugshot), 1) # the item should have been updated with the paths returned from our # fake Mugshot.makeThumbnail() self.assertEqual(mugshot.body, newBody) self.assertEqual(mugshot.smallerBody, newSmallerBody) # the 'person' attribute should be unchanged self.assertIdentical(mugshot.person, person) # the format attribute should be updated self.assertEqual(mugshot.type, u'image/' + newFormat) return mugshot def test_fromFileExistingMugshot(self): """ Verify that L{Mugshot.fromFile} will update the attributes on an existing L{Mugshot} item for the given person, if one exists. """ store = Store(filesdir=self.mktemp()) person = Person(store=store) mugshot = Mugshot( store=store, type=u'JPEG', body=store.newFilePath('body'), smallerBody=store.newFilePath('smallerBody'), person=person) self.assertIdentical( self._doFromFileTest(store, person), mugshot) def test_fromFileNoMugshot(self): """ Verify that L{Mugshot.fromFile} creates a new L{Mugshot} for the given person, if one does not exist. """ store = Store(filesdir=self.mktemp()) person = Person(store=store) self._doFromFileTest(store, person) def _doMakeThumbnailTest(self, smaller): """ Verify that L{Mugshot.makeThumbnail} passes the correct arguments to L{makeThumbnail}, when passed the given value for the C{smaller} argument. """ makeThumbnailCalls = [] def _makeThumbnail( inputFile, outputFile, thumbnailSize, outputFormat='jpeg'): makeThumbnailCalls.append(( inputFile, outputFile, thumbnailSize, outputFormat)) store = Store(filesdir=self.mktemp()) person = Person(store=store) inputFile = file(self.mktemp(), 'w') inputFormat = 'JPEG' originalMakeThumbnail = people.makeThumbnail try: people.makeThumbnail = _makeThumbnail thumbnailPath = Mugshot.makeThumbnail( inputFile, person, inputFormat, smaller) finally: people.makeThumbnail = originalMakeThumbnail self.assertEqual(len(makeThumbnailCalls), 1) (gotInputFile, outputFile, thumbnailSize, outputFormat) = ( makeThumbnailCalls[0]) self.assertEqual(gotInputFile, inputFile) if smaller: self.assertEqual(thumbnailSize, Mugshot.smallerSize) else: self.assertEqual(thumbnailSize, Mugshot.size) self.assertEqual(outputFormat, inputFormat) self.assertTrue(isinstance(outputFile, AtomicFile)) # it should return the right path self.assertEqual(outputFile.finalpath, thumbnailPath) def test_makeThumbnail(self): """ Verify that L{Mugshot.makeThumbnail} passes the correct arguments to L{makeThumbnail}. """ self._doMakeThumbnailTest(smaller=False) def test_makeThumbnailSmaller(self): """ Like L{test_makeThumbnail}, but for when the method is asked to make a smaller-sized thumbnail. """ self._doMakeThumbnailTest(smaller=True) def test_placeholderForPerson(self): """ L{Mugshot.placeholderForPerson} should return a correctly-initialized L{Mugshot} for the given person. """ store = Store(self.mktemp()) organizer = Organizer(store=store) installOn(organizer, store) person = organizer.createPerson(u'Alice') mugshot = Mugshot.placeholderForPerson(person) self.assertTrue(isinstance(mugshot, Mugshot)) self.assertIdentical(mugshot.store, None) self.assertIdentical(mugshot.person, person) self.assertEqual(mugshot.type, u'image/png') imageDir = FilePath(people.__file__).parent().child( 'static').child('images') self.assertEqual( mugshot.body, imageDir.child('mugshot-placeholder.png')) self.assertEqual( mugshot.smallerBody, imageDir.child('mugshot-placeholder-smaller.png')) class WhitespaceNormalizationTests(unittest.TestCase): """ Tests for L{_normalizeWhitespace}. """ def test_empty(self): """ L{_normalizeWhitespace} should return an empty string for an empty string. """ self.assertEqual(_normalizeWhitespace(u''), u'') def test_spaces(self): """ L{_normalizeWhitespace} should return an empty string for a string consisting only of whitespace. """ self.assertEqual(_normalizeWhitespace(u' \t\v'), u'') def test_leadingSpace(self): """ L{_normalizeWhitespace} should remove leading whitespace in its result. """ self.assertEqual(_normalizeWhitespace(u' x'), u'x') def test_trailingSpace(self): """ L{_normalizeWhitespace} should remove trailing whitespace in its result. """ self.assertEqual(_normalizeWhitespace(u'x '), u'x') def test_multipleSpace(self): """ L{_normalizeWhitespace} should replace occurrences of contiguous whitespace characters with a single space character. """ self.assertEqual(_normalizeWhitespace(u'x x'), u'x x') class BaseContactTests(unittest.TestCase): """ Tests for the utility base-class L{BaseContactType}. """ def test_uniqueIdentifier(self): """ L{BaseContactType.uniqueIdentifier} should return a unicode string giving the fully-qualified Python name of the class of the instance it is called on. """ class Dummy(BaseContactType): pass identifier = Dummy().uniqueIdentifier() self.assertTrue(isinstance(identifier, unicode)) self.assertEqual(identifier, __name__ + '.' + Dummy.__name__) def test_getEditFormForPerson(self): """ L{BaseContactType.getEditFormForPerson} should return C{None}. """ class Stub(BaseContactType): def getParameters(self, person): return [object()] self.assertIdentical(Stub().getEditFormForPerson(Person()), None) def test_getContactGroup(self): """ L{BaseContactType.getContactGroup} should return C{None}. """ self.assertIdentical( BaseContactType().getContactGroup(object()), None) class EmailAddressTests(unittest.TestCase): """ Tests for L{EmailAddress}. """ def test_deletedWithPerson(self): """ An L{EmailAddress} should be deleted when the L{Person} it is associated with is deleted. """ store = Store() person = Person(store=store) email = EmailAddress( store=store, person=person, address=u'testuser@example.com') person.deleteFromStore() self.assertEqual(store.query(EmailAddress).count(), 0) class PostalAddressTests(unittest.TestCase): """ Tests for L{PostalAddress}. """ def test_deletedWithPerson(self): """ A L{PostalAddress} should be deleted when the L{Person} it is associated with is deleted. """ store = Store() person = Person(store=store) address = PostalAddress( store=store, person=person, address=u'123 Street Rd') person.deleteFromStore() self.assertEqual(store.query(PostalAddress).count(), 0) class ContactTestsMixin(object): """ Define tests common to different L{IContactType} implementations. Mix this in to a L{unittest.TestCase} and bind C{self.contactType} to the L{IContactType} provider in C{setUp}. """ def test_providesContactType(self): """ C{self.contactType} should provide L{IContactType}. """ self.assertTrue(IContactType.providedBy(self.contactType)) # I would really like to use verifyObject here. However, the # **parameters in IContactType.editContactItem causes it to fail for # reasonably conformant implementations. # self.assertTrue(verifyObject(IContactType, self.contactType)) def test_organizerIncludesIt(self): """ L{Organizer.getContactTypes} should include an instance of our contact type in its return value. """ organizer = Organizer(store=self.store) self.assertTrue([ contactType for contactType in organizer.getContactTypes() if isinstance(contactType, self.contactType.__class__)]) class EmailContactTests(unittest.TestCase, ContactTestsMixin): """ Tests for the email address parameters defined by L{EmailContactType}. """ def setUp(self): self.store = Store() self.contactType = EmailContactType(self.store) def test_descriptiveIdentifier(self): """ L{EmailContactType.descriptiveIdentifier} should be "Email Address". """ self.assertEqual( self.contactType.descriptiveIdentifier(), u'Email Address') def test_allowsMultipleContactItems(self): """ L{EmailContactType.allowMultipleContactItems} should be C{True}. """ self.assertTrue(self.contactType.allowMultipleContactItems) def test_createContactItem(self): """ L{EmailContactType.createContactItem} should create an L{EmailAddress} instance with the supplied values. """ person = Person(store=self.store) contactItem = self.contactType.createContactItem( person, email=u'user@example.com') emails = list(self.store.query(EmailAddress)) self.assertEqual(emails, [contactItem]) self.assertEqual(contactItem.address, u'user@example.com') self.assertIdentical(contactItem.person, person) def test_createContactItemWithEmptyString(self): """ L{EmailContactType.createContactItem} shouldn't create an L{EmailAddress} instance if it is given an empty string for the address. """ person = Person(store=self.store) contactItem = self.contactType.createContactItem( person, email=u'') emails = list(self.store.query(EmailAddress)) self.assertIdentical(contactItem, None) self.assertEqual(len(emails), 0) def test_createContactItemRejectsDuplicate(self): """ L{EmailContactType.createContactItem} should raise an exception if it is given an email address already associated with an existing L{EmailAddress} item. """ email = u'user@example.com' person = Person(store=self.store) emailAddress = EmailAddress( store=self.store, person=person, address=email) self.assertRaises( ValueError, self.contactType.createContactItem, person, email=email) def test_editContactItem(self): """ L{EmailContactType.editContactItem} should update the address field of the L{EmailAddress} it is passed. """ person = Person(store=self.store) emailAddress = EmailAddress( store=self.store, person=person, address=u'wrong') self.contactType.editContactItem( emailAddress, email=u'user@example.com') self.assertEqual(emailAddress.address, u'user@example.com') def test_editContactItemAcceptsSame(self): """ L{EmailContactType.editContactItem} should update the address field of the L{EmailAddress} it is passed, even if it is passed the same value which is already set on the item. """ address = u'user@example.com' person = Person(store=self.store) emailAddress = EmailAddress( store=self.store, person=person, address=address) self.contactType.editContactItem( emailAddress, email=address) self.assertEqual(emailAddress.address, address) def test_editContactItemRejectsDuplicate(self): """ L{EmailContactType.editContactItem} should raise an exception if it is given an email address already associated with a different L{EmailAddress} item. """ person = Person(store=self.store) existing = EmailAddress( store=self.store, person=person, address=u'user@example.com') editing = EmailAddress( store=self.store, person=person, address=u'user@example.net') self.assertRaises( ValueError, self.contactType.editContactItem, editing, email=existing.address) # It should be possible to set an EmailAddress's address attribute to # its current value, though. address = editing.address self.contactType.editContactItem(editing, email=address) self.assertEqual(editing.address, address) def test_getParameters(self): """ L{EmailContactType.getParameters} should return a C{list} of L{LiveForm} parameters for an email address. """ (email,) = self.contactType.getParameters(None) self.assertEqual(email.name, 'email') self.assertEqual(email.default, '') def test_getParametersWithDefaults(self): """ L{EmailContactType.getParameters} should return a C{list} of L{LiveForm} parameters with default values supplied from the L{EmailAddress} item it is passed. """ person = Person(store=self.store) (email,) = self.contactType.getParameters( EmailAddress(store=self.store, person=person, address=u'user@example.com')) self.assertEqual(email.name, 'email') self.assertEqual(email.default, u'user@example.com') def test_coerce(self): """ L{EmailContactType.coerce} should return a dictionary mapping C{'email'} to the email address passed to it. """ self.assertEqual( self.contactType.coerce(email=u'user@example.com'), {'email': u'user@example.com'}) def test_getReadOnlyView(self): """ L{EmailContactType.getReadOnlyView} should return a L{SimpleReadOnlyView} wrapped around the given contact item. """ contact = EmailAddress(address=u'', person=Person()) view = self.contactType.getReadOnlyView(contact) self.assertTrue(isinstance(view, SimpleReadOnlyView)) self.assertIdentical(view.attribute, EmailAddress.address) self.assertIdentical(view.contactItem, contact) class VIPPersonContactTypeTestCase(unittest.TestCase): """ Tests for L{VIPPersonContactType}. """ def setUp(self): """ Create a L{Person} and a L{VIPPersonContactType}. """ self.person = Person(vip=False) self.contactType = VIPPersonContactType() def test_providesContactType(self): """ L{VIPPersonContactType} should provide L{IContactType}. """ self.assertTrue(IContactType.providedBy(self.contactType)) def test_createContactItem(self): """ L{VIPPersonContactType.createContactItem} should set the C{vip} attribute of the given person to the specified value, and return a L{_PersonVIPStatus} wrapping the person. """ contactItem = self.contactType.createContactItem( self.person, True) self.assertTrue(isinstance(contactItem, _PersonVIPStatus)) self.assertIdentical(contactItem.person, self.person) self.assertTrue(self.person.vip) contactItem = self.contactType.createContactItem( self.person, False) self.assertTrue(isinstance(contactItem, _PersonVIPStatus)) self.assertIdentical(contactItem.person, self.person) self.assertFalse(self.person.vip) def test_editContactItem(self): """ L{VIPPersonContactType.editContactItem} should set the C{vip} attribute of the wrapped person to the specified value. """ self.contactType.editContactItem( _PersonVIPStatus(self.person), True) self.assertTrue(self.person.vip) self.contactType.editContactItem( _PersonVIPStatus(self.person), False) self.assertFalse(self.person.vip) def test_getParametersNoPerson(self): """ L{VIPPersonContactType.getParameters} should return a parameter with a default of C{False} when it's passed C{None}. """ params = self.contactType.getParameters(None) self.assertEqual(len(params), 1) param = params[0] self.assertFalse(param.default) def test_getParametersPerson(self): """ L{VIPPersonContactType.getParameters} should return a parameter with the correct default when it's passed a L{_PersonVIPStatus} wrapping a person. """ params = self.contactType.getParameters( _PersonVIPStatus(self.person)) self.assertEqual(len(params), 1) param = params[0] self.assertFalse(param.default) self.person.vip = True params = self.contactType.getParameters( _PersonVIPStatus(self.person)) self.assertEqual(len(params), 1) param = params[0] self.assertTrue(param.default) def test_getReadOnlyView(self): """ L{VIPPersonContactType.getReadOnlyView} should return something which flattens to the empty string. """ view = self.contactType.getReadOnlyView( _PersonVIPStatus(self.person)) self.assertEqual(flatten(view), '') class PostalContactTests(unittest.TestCase, ContactTestsMixin): """ Tests for snail-mail address contact information represented by L{PostalContactType}. """ def setUp(self): """ Create a L{Store}, L{PostalContactType}, and L{Person} for use by tests. """ self.store = Store() self.person = Person(store=self.store) self.contactType = PostalContactType() def test_descriptiveIdentifier(self): """ L{PostalContactType.descriptiveIdentifier} should be "Postal Address". """ self.assertEqual( self.contactType.descriptiveIdentifier(), u'Postal Address') def test_allowsMultipleContactItems(self): """ L{PostalContactType.allowMultipleContactItems} should be C{True}. """ self.assertTrue(self.contactType.allowMultipleContactItems) def test_createContactItem(self): """ L{PostalContactType.createContactItem} should create a L{PostalAddress} instance with the supplied values. """ contactItem = self.contactType.createContactItem( self.person, address=u'123 Street Rd') addresses = list(self.store.query(PostalAddress)) self.assertEqual(addresses, [contactItem]) self.assertEqual(contactItem.address, u'123 Street Rd') self.assertIdentical(contactItem.person, self.person) def test_createContactItemWithEmptyString(self): """ L{PostalContactType.createContactItem} shouldn't create a L{PostalAddress} instance if it is given an empty string for the address. """ contactItem = self.contactType.createContactItem( self.person, address=u'') addresses = list(self.store.query(PostalAddress)) self.assertIdentical(contactItem, None) self.assertEqual(len(addresses), 0) def test_editContactItem(self): """ L{PostalContactType.editContactItem} should update the address field of the L{PostalAddress} it is passed. """ postalAddress = PostalAddress( store=self.store, person=self.person, address=u'wrong') self.contactType.editContactItem( postalAddress, address=u'123 Street Rd') self.assertEqual(postalAddress.address, u'123 Street Rd') def test_getParameters(self): """ L{PostalContactType.getParameters} should return a C{list} of L{LiveForm} parameters for a mailing address. """ (address,) = self.contactType.getParameters(None) self.assertEqual(address.name, 'address') self.assertEqual(address.default, '') def test_getParametersWithDefaults(self): """ L{PostalContactType.getParameters} should return a C{list} of L{LiveForm} parameters with default values supplied from the L{PostalAddress} item it is passed. """ (address,) = self.contactType.getParameters( PostalAddress(store=self.store, person=self.person, address=u'123 Street Rd')) self.assertEqual(address.name, 'address') self.assertEqual(address.default, u'123 Street Rd') def test_getContactItems(self): """ L{PostalContactType.getContactItems} should return a C{list} of all the L{PostalAddress} instances associated with the specified person. """ firstAddress = PostalAddress( store=self.store, person=self.person, address=u'123 Street Rd') secondAddress = PostalAddress( store=self.store, person=self.person, address=u'456 Street Rd') anotherPerson = Person(store=self.store) anotherAddress = PostalAddress( store=self.store, person=anotherPerson, address=u'789 Street Rd') self.assertEqual( list(self.contactType.getContactItems(self.person)), [firstAddress, secondAddress]) def test_coerce(self): """ L{PostalContactType.coerce} should return a dictionary mapping C{'address'} to the postal address passed to it. """ self.assertEqual( self.contactType.coerce(address=u'123 Street Rd'), {'address': u'123 Street Rd'}) def test_getReadOnlyView(self): """ L{PostalContactType.getReadOnlyView} should return a L{SimpleReadOnlyView} wrapped around the given contact item. """ contact = PostalAddress(address=u'', person=Person()) view = self.contactType.getReadOnlyView(contact) self.assertTrue(isinstance(view, SimpleReadOnlyView)) self.assertIdentical(view.contactItem, contact) self.assertIdentical(view.attribute, PostalAddress.address) class PhoneNumberContactTypeTestCase(unittest.TestCase, ContactTestsMixin): """ Tests for L{PhoneNumberContactType}. """ def setUp(self): """ Create a store, L{PhoneNumberContactType} and L{Person}. """ self.store = Store() self.person = Person(store=self.store) self.contactType = PhoneNumberContactType() def test_descriptiveIdentifier(self): """ L{PhoneNumberContactType.descriptiveIdentifier} should be "Phone Number". """ self.assertEqual( self.contactType.descriptiveIdentifier(), u'Phone Number') def test_allowsMultipleContactItems(self): """ L{PhoneNumberContactType.allowMultipleContactItems} should be C{True}. """ self.assertTrue(self.contactType.allowMultipleContactItems) def test_createContactItem(self): """ L{PhoneNumberContactType.createContactItem} should create a L{PhoneNumber} item with the supplied value. """ contactItem = self.contactType.createContactItem( self.person, label=PhoneNumber.LABELS.HOME, number=u'123456') numbers = list(self.store.query(PhoneNumber)) self.assertEqual(numbers, [contactItem]) self.assertEqual( contactItem.label, PhoneNumber.LABELS.HOME) self.assertEqual(contactItem.number, u'123456') self.assertIdentical(contactItem.person, self.person) def test_createContactItemWithEmptyString(self): """ L{PhoneNumberContactType.createContactItem} shouldn't create an item if it's passed an empty number. """ self.assertIdentical( self.contactType.createContactItem( self.person, label=PhoneNumber.LABELS.HOME, number=u''), None) self.assertEqual(self.store.query(PhoneNumber).count(), 0) def test_editContactItem(self): """ L{PhoneNumberContactType.editContactItem} should update the I{number} and I{label} attributes of the given item. """ contactItem = PhoneNumber( store=self.store, person=self.person, label=PhoneNumber.LABELS.HOME, number=u'123456') self.contactType.editContactItem( contactItem, label=PhoneNumber.LABELS.WORK, number=u'654321') self.assertEqual( contactItem.label, PhoneNumber.LABELS.WORK) self.assertEqual(contactItem.number, u'654321') def test_getParameters(self): """ L{PhoneNumberContactType.getParameters} should return a list containing two parameters. """ parameters = self.contactType.getParameters(None) self.assertEqual(len(parameters), 2) (labelParam, numberParam) = parameters self.assertTrue(isinstance(labelParam, ChoiceParameter)) self.assertEqual(labelParam.name, 'label') self.assertEqual( [c.value for c in labelParam.choices], PhoneNumber.LABELS.ALL_LABELS) self.assertTrue(isinstance(numberParam, Parameter)) self.assertEqual(numberParam.name, 'number') self.assertEqual(numberParam.default, '') self.assertEqual(numberParam.type, TEXT_INPUT) def test_getParametersWithDefault(self): """ L{PhoneNumberContactType.getParameters} should correctly default the returned parameter if its passed a contact item. """ contactItem = PhoneNumber( store=self.store, person=self.person, label=PhoneNumber.LABELS.HOME, number=u'123456') parameters = self.contactType.getParameters(contactItem) self.assertEqual(len(parameters), 2) (labelParam, numberParam) = parameters selectedOptions = [] for choice in labelParam.choices: if choice.selected: selectedOptions.append(choice.value) self.assertEqual(selectedOptions, [contactItem.label]) self.assertEqual(numberParam.default, contactItem.number) def test_getContactItems(self): """ L{PhoneNumberContactType.getContactItems} should return only L{PhoneNumber} items associated with the given person. """ otherPerson = Person(store=self.store) PhoneNumber( store=self.store, person=otherPerson, number=u'123455') expectedNumbers = [ PhoneNumber( store=self.store, person=self.person, number=u'123456'), PhoneNumber( store=self.store, person=self.person, number=u'123457')] self.assertEqual( list(self.contactType.getContactItems(self.person)), expectedNumbers) def test_getReadOnlyView(self): """ L{PhoneNumberContactType.getReadOnlyView} should return a correctly-initialized L{ReadOnlyPhoneNumberView}. """ contactItem = PhoneNumber( store=self.store, person=self.person, number=u'123456') view = self.contactType.getReadOnlyView(contactItem) self.assertTrue(isinstance(view, ReadOnlyPhoneNumberView)) self.assertIdentical(view.phoneNumber, contactItem) class NotesContactTypeTestCase(unittest.TestCase, ContactTestsMixin): """ Tests for L{NotesContactType}. """ def setUp(self): """ Create a store, L{NotesContactType} and L{Person}. """ self.store = Store() self.person = Person(store=self.store) self.contactType = NotesContactType() def test_descriptiveIdentifier(self): """ L{NotesContactType.descriptiveIdentifier} should be "Notes". """ self.assertEqual( self.contactType.descriptiveIdentifier(), u'Notes') def test_allowsMultipleContactItems(self): """ L{NotesContactType.allowMultipleContactItems} should be C{False}. """ self.assertFalse(self.contactType.allowMultipleContactItems) def test_createContactItem(self): """ L{NotesContactType.createContactItem} should create a L{Notes} item with the supplied value. """ contactItem = self.contactType.createContactItem( self.person, notes=u'some notes') notes = list(self.store.query(Notes)) self.assertEqual(notes, [contactItem]) self.assertEqual(contactItem.notes, u'some notes') self.assertIdentical(contactItem.person, self.person) def test_createContactItemWithEmptyString(self): """ L{NotesContactType.createContactItem} shouldn't create an item if it's passed an empty string. """ self.assertIdentical( self.contactType.createContactItem( self.person, notes=u''), None) self.assertEqual(self.store.query(Notes).count(), 0) def test_editContactItem(self): """ L{NotesContactType.editContactItem} should update the I{notes} attribute of the given item. """ contactItem = Notes( store=self.store, person=self.person, notes=u'some notes') self.contactType.editContactItem( contactItem, notes=u'revised notes') self.assertEqual(contactItem.notes, u'revised notes') def test_getParameters(self): """ L{NotesContactType.getParameters} should return a list containing a single parameter. """ parameters = self.contactType.getParameters(None) self.assertEqual(len(parameters), 1) param = parameters[0] self.assertTrue(isinstance(param, Parameter)) self.assertEqual(param.name, 'notes') self.assertEqual(param.default, '') self.assertEqual(param.type, TEXTAREA_INPUT) self.assertEqual(param.label, u'Notes') def test_getParametersWithDefault(self): """ L{NotesContactType.getParameters} should correctly default the returned parameter if it's passed a contact item. """ contactItem = Notes( store=self.store, person=self.person, notes=u'some notes') parameters = self.contactType.getParameters(contactItem) self.assertEqual(len(parameters), 1) self.assertEqual(parameters[0].default, contactItem.notes) def test_getContactItems(self): """ L{NotesContactType.getContactItems} should return only the L{Notes} item associated with the given person. """ Notes(store=self.store, person=Person(store=self.store), notes=u'notes') expectedNotes = [ Notes(store=self.store, person=self.person, notes=u'some notes')] self.assertEqual( list(self.contactType.getContactItems(self.person)), expectedNotes) def test_getContactItemsCreates(self): """ L{NotesContactType.getContactItems} should create a L{Notes} item for the given person, if one does not exist. """ # sanity check self.assertEqual(self.store.query(Notes).count(), 0) contactItems = self.contactType.getContactItems(self.person) self.assertEqual(len(contactItems), 1) self.assertEqual(contactItems, list(self.store.query(Notes))) self.assertEqual(contactItems[0].notes, u'') self.assertIdentical(contactItems[0].person, self.person) def test_getReadOnlyView(self): """ L{NotesContactType.getReadOnlyView} should return a correctly-initialized L{SimpleReadOnlyView}. """ contactItem = Notes( store=self.store, person=self.person, notes=u'notes') view = self.contactType.getReadOnlyView(contactItem) self.assertTrue(isinstance(view, SimpleReadOnlyView)) self.assertIdentical(view.attribute, Notes.notes) self.assertIdentical(view.contactItem, contactItem) class ReadOnlyPhoneNumberViewTestCase(unittest.TestCase, TagTestingMixin): """ Tests for L{ReadOnlyPhoneNumberView}. """ def test_number(self): """ The I{number} renderer of L{ReadOnlyPhoneNumberView} should return the value of the wrapped L{PhoneNumber}'s C{number} attribute. """ contactItem = PhoneNumber( person=Person(), number=u'123456') view = ReadOnlyPhoneNumberView(contactItem) value = renderer.get(view, 'number')(None, div) self.assertTag(value, 'div', {}, [contactItem.number]) def test_label(self): """ The I{label} renderer of L{ReadOnlyPhoneNumberView} should return the value of the wrapped L{PhoneNumber}'s C{label} attribute. """ contactItem = PhoneNumber( person=Person(), label=PhoneNumber.LABELS.WORK, number=u'123456') view = ReadOnlyPhoneNumberView(contactItem) value = renderer.get(view, 'label')(None, div) self.assertTag(value, 'div', {}, [contactItem.label]) class PeopleModelTestCase(unittest.TestCase): """ Tests for the model parts of the person organizer code. """ def setUp(self): """ Create a bunch of people with names beginning with various letters. """ self.store = Store() self.organizer = Organizer(store=self.store) installOn(self.organizer, self.store) letters = lowercase.decode('ascii') for firstPrefix, lastPrefix in zip(letters, reversed(letters)): name = u'Alice ' + lastPrefix + u'Jones' person = Person( store=self.store, organizer=self.organizer, created=Time(), name=name) def test_getPeopleFilters(self): """ L{Organizer.getPeopleFilters} should return an iterable of all of the L{IPeopleFilter} plugins available in the store. """ firstPeopleFilters = [object(), object()] firstContactPowerup = StubOrganizerPlugin( store=self.store, peopleFilters=firstPeopleFilters) self.store.powerUp( firstContactPowerup, IOrganizerPlugin, priority=1) secondPeopleFilters = [object()] secondContactPowerup = StubOrganizerPlugin( store=self.store, peopleFilters=secondPeopleFilters) self.store.powerUp( secondContactPowerup, IOrganizerPlugin, priority=0) self.assertEqual( list(self.organizer.getPeopleFilters())[ builtinPeopleFilterCount:], firstPeopleFilters + secondPeopleFilters) def test_getPeopleFiltersTags(self): """ L{Organizer.getPeopleFilters} should include one L{TaggedPeopleFilter} for each tag which has been applied to a person. """ personTags = list(u'xac') catalog = tags.Catalog(store=self.store) for personTag in personTags: catalog.tag(Person(store=self.store), personTag) peopleFilters = list(self.organizer.getPeopleFilters())[ builtinPeopleFilterCount:] self.assertEqual(len(peopleFilters), len(personTags)) for (peopleFilter, personTag) in zip(peopleFilters, sorted(personTags)): self.assertTrue(isinstance(peopleFilter, TaggedPeopleFilter)) self.assertEqual(peopleFilter.filterName, personTag) def test_createPerson(self): """ L{Organizer.createPerson} should instantiate and return a L{Person} item with the specified nickname, a reference to the creating L{Organizer}, and a creation timestamp set to the current time. """ nickname = u'test person' beforeCreation = extime.Time() person = self.organizer.createPerson(nickname) afterCreation = extime.Time() self.assertEqual(person.name, nickname) self.assertIdentical(person.organizer, self.organizer) self.assertTrue(beforeCreation <= person.created <= afterCreation) self.assertFalse(person.vip) def test_createPersonDuplicateNickname(self): """ L{Organizer.createPerson} raises an exception when passed a nickname which is already associated with a L{Person} in the database. """ nickname = u'test person' self.organizer.createPerson(nickname) self.assertRaises( ValueError, self.organizer.createPerson, nickname) def test_caseInsensitiveName(self): """ L{Person.name} should not be case-sensitive. """ name = u'alice' store = Store() person = Person(store=store, name=name.upper()) self.assertEqual( list(store.query(Person, Person.name == name.lower())), [person]) def test_editPersonChangesName(self): """ L{Organizer.editPerson} should change the I{name} of the given L{Person}. """ person = self.organizer.createPerson(u'alice') self.organizer.editPerson(person, u'bob', []) self.assertEqual(person.name, u'bob') def test_editPersonEditsContactInfo(self): """ L{Organizer.editPerson} should call I{editContactItem} on each element of the edits sequence it is passed. """ person = self.organizer.createPerson(u'alice') contactType = StubContactType((), None, None) contactItem = object() contactInfo = {u'foo': u'bar'} self.organizer.editPerson( person, u'alice', [(contactType, ListChanges( [], [EditObject(contactItem, contactInfo)], []))]) self.assertEqual( contactType.editedContacts, [(contactItem, contactInfo)]) def test_editPersonEditsUnrepeatableContactInfo(self): """ Like L{test_editPersonEditsContactInfo}, but for the case where the contact type doesn't support multiple contact items. """ person = self.organizer.createPerson(u'alice') contactItem = object() contactType = StubContactType( (), None, contactItems=[contactItem], allowMultipleContactItems=False) contactInfo = {u'foo': u'bar'} self.organizer.editPerson( person, u'alice', [(contactType, contactInfo)]) self.assertEqual( contactType.editedContacts, [(contactItem, contactInfo)]) def test_editPersonCreatesContactInfo(self): """ L{Organizer.editPerson} should call I{createContactItem} on each element in the create sequence it is passed. """ person = self.organizer.createPerson(u'alice') contactType = StubContactType((), None, None, createContactItems=True) contactInfo = {u'foo': u'bar'} createdObjects = [] def setter(createdObject): createdObjects.append(createdObject) self.organizer.editPerson( person, u'alice', [(contactType, ListChanges( [CreateObject(contactInfo, setter)], [], []))]) self.assertEqual( contactType.createdContacts, [(person, contactInfo)]) self.assertEqual(createdObjects, [(person, contactInfo)]) def test_editPersonContactCreationNotification(self): """ Contact items created through L{Organizer.editPerson} should be sent to L{IOrganizerPlugin.contactItemCreated} for all L{IOrganizerPlugin} powerups on the store. """ contactType = StubContactType((), None, None, createContactItems=True) contactInfo = {u'foo': u'bar'} observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(u'alice') self.organizer.editPerson( person, person.name, [(contactType, ListChanges([CreateObject(contactInfo, lambda obj: None)], [], []))]) self.assertEqual( observer.createdContactItems, [(person, contactInfo)]) def test_editPersonContactEditNotification(self): """ Contact items edit through L{Organizer.editPerson} should be sent to L{IOrganizerPlugin.contactItemEdited} for all L{IOrganizerPlugin} powerups on the store. """ contactType = StubContactType((), None, None) contactItem = object() observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(u'alice') self.organizer.editPerson( person, person.name, [(contactType, ListChanges([], [EditObject(contactItem, {})], []))]) self.assertEqual( observer.editedContactItems, [contactItem]) def test_editPersonDeletesContactInfo(self): """ L{Organizer.editPerson} should call L{deleteFromStore} on each element in the delete sequence it is passed. """ class DeletableObject(object): deleted = False def deleteFromStore(self): self.deleted = True person = self.organizer.createPerson(u'alice') contactType = StubContactType((), None, None) contactItem = DeletableObject() self.organizer.editPerson( person, u'alice', [(contactType, ListChanges([], [], [contactItem]))]) self.assertTrue(contactItem.deleted) def test_editPersonDuplicateNickname(self): """ L{Organizer.editPerson} raises an exception when passed a nickname which is already associated with a different L{Person} in the database. """ alice = self.organizer.createPerson(u'alice') bob = self.organizer.createPerson(u'bob') self.assertRaises(ValueError, self.organizer.editPerson, bob, alice.name, []) def test_editPersonSameName(self): """ L{Organizer.editPerson} allows the new nickname it is passed to be the same as the existing name for the given L{Person}. """ alice = self.organizer.createPerson(u'alice') self.organizer.editPerson(alice, alice.name, []) self.assertEqual(alice.name, u'alice') def test_editPersonNotifiesPlugins(self): """ L{Organizer.editPerson} should call C{personNameChanged} on all L{IOrganizerPlugin} powerups on the store. """ nickname = u'test person' newname = u'alice' observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(nickname) self.organizer.editPerson(person, newname, []) self.assertEqual( observer.renamedPeople, [(newname, nickname)]) def test_createVeryImportantPerson(self): """ L{Organizer.createPerson} should set L{Person.vip} to match the value it is passed for the C{vip} parameter, and issue a deprecation warning. """ self.assertWarns( DeprecationWarning, "Usage of Organizer.createPerson's 'vip' parameter is deprecated", people.__file__, lambda: self.organizer.createPerson(u'alice', True)) alice = self.store.findUnique(Person, Person.name == u'alice') self.assertTrue(alice.vip) def test_createPersonNoVIP(self): """ L{Organizer.createPerson} shouldn't issue a warning if no C{vip} argument is passed. """ originalWarnExplicit = warnings.warn_explicit def warnExplicit(*args): self.fail('Organizer.createPerson warned us: %r' % (args[0],)) try: warnings.warn_explicit = warnExplicit person = self.organizer.createPerson(u'alice') finally: warnings.warn_explicit = originalWarnExplicit def test_noMugshot(self): """ L{Person.getMugshot} should call L{Mugshot.placeholderForPerson} when called on a L{Person} without a stored L{Mugshot}. """ people = [] thePlaceholder = object() def placeholderForPerson(person): people.append(person) return thePlaceholder person = Person(store=self.store) originalPlaceholderForPerson = Mugshot.placeholderForPerson try: Mugshot.placeholderForPerson = staticmethod( placeholderForPerson) getMugshotResult = person.getMugshot() finally: Mugshot.placeholderForPerson = originalPlaceholderForPerson self.assertIdentical(getMugshotResult, thePlaceholder) self.assertEqual(people, [person]) def test_getMugshot(self): """ L{Person.getMugshot} should return the L{Mugshot} item which refers to the person on which it is called when one exists. """ store = Store(filesdir=self.mktemp()) person = Person(store=store) image = Mugshot( store=store, type=u'image/png', body=store.filesdir.child('a'), smallerBody=store.filesdir.child('b'), person=person) self.assertIdentical(person.getMugshot(), image) def test_deletePerson(self): """ L{Organizer.deletePerson} should delete the specified person from the store. """ person = Person(store=self.store) self.organizer.deletePerson(person) self.assertEqual(self.store.query(Person, Person.storeID == person.storeID).count(), 0) def test_getOrganizerPlugins(self): """ L{Organizer.getOrganizerPlugins} should return an iterator of the installed L{IOrganizerPlugin} powerups. """ observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) plugins = list(self.organizer.getOrganizerPlugins()) self.assertEqual(plugins[:-1], [observer]) self.assertTrue( isinstance(plugins[-1], ContactInfoOrganizerPlugin)) def test_createContactItemNotifiesPlugins(self): """ L{Organizer.createContactItem} should call L{contactItemCreated} on all L{IOrganizerPlugin} powerups on the store. """ nickname = u'test person' observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(nickname) contactType = StubContactType((), None, None) parameters = {'key': u'value'} contactItem = self.organizer.createContactItem( contactType, person, parameters) self.assertEqual(len(observer.createdContactItems), 1) [(observedPerson, observedParameters)] = observer.createdContactItems self.assertIdentical(person, observedPerson) self.assertEqual(parameters, observedParameters) def test_notificationSkippedForUncreatedContactItems(self): """ L{Organizer.createContactItem} should not call L{contactItemCreated} on any L{IOrganizerPlugin} powerups on the store if L{IContactType.createContactItem} returns C{None} to indicate that it is not creating a contact item. """ nickname = u'test person' observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(nickname) contactType = StubContactType((), None, None, False) parameters = {'key': u'value'} contactItem = self.organizer.createContactItem( contactType, person, parameters) self.assertEqual(observer.createdContactItems, []) def test_editContactItemNotifiesPlugins(self): """ L{Organizer.editContactItem} should call L{contactItemEdited} on all L{IOrganizerPlugin} powerups in the store. """ observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) contactType = StubContactType((), None, None) contactItem = object() self.organizer.editContactItem(contactType, contactItem, {}) self.assertEqual(observer.editedContactItems, [contactItem]) def test_createPersonNotifiesPlugins(self): """ L{Organizer.createPerson} should call L{personCreated} on all L{IOrganizerPlugin} powerups on the store. """ nickname = u'test person' observer = StubOrganizerPlugin(store=self.store) self.store.powerUp(observer, IOrganizerPlugin) person = self.organizer.createPerson(nickname) self.assertEqual(observer.createdPeople, [person]) def test_organizerPluginWithoutPersonCreated(self): """ L{IOrganizerPlugin} powerups which don't have the C{personCreated} method should not cause problems with L{Organizer.createPerson} (The method was added after the interface was initially defined so there may be implementations which have not yet been updated). """ store = Store() class OldOrganizerPlugin(object): """ An L{IOrganizerPlugin} which does not implement C{getContactTypes}. """ getOrganizerPlugins = Organizer.getOrganizerPlugins.im_func plugins = [OldOrganizerPlugin(), StubOrganizerPlugin(createdPeople=[])] Organizer.getOrganizerPlugins = lambda self: plugins try: organizer = Organizer(store=store) person = organizer.createPerson(u'nickname') finally: Organizer.getOrganizerPlugins = getOrganizerPlugins self.assertEqual(plugins[1].createdPeople, [organizer.storeOwnerPerson, person]) def test_getContactTypes(self): """ L{Organizer.getContactTypes} should return an iterable of all the L{IContactType} plugins available on the store. """ firstContactTypes = [object(), object()] firstContactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=firstContactTypes) self.store.powerUp( firstContactPowerup, IOrganizerPlugin, priority=1) secondContactTypes = [object()] secondContactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=secondContactTypes) self.store.powerUp( secondContactPowerup, IOrganizerPlugin, priority=0) self.assertEqual( list(self.organizer.getContactTypes())[builtinContactTypeCount:], firstContactTypes + secondContactTypes) def test_getContactTypesOldMethod(self): """ L{Organizer.getContactTypes} should emit a warning if it encounters an implementation which defines the C{getEditorialForm} method. """ contactType = StubContactType([], None, []) contactType.getEditorialForm = lambda _: None powerup = StubOrganizerPlugin( store=self.store, contactTypes=[contactType]) self.store.powerUp(powerup, IOrganizerPlugin) self.assertWarns( DeprecationWarning, "The IContactType %s defines the 'getEditorialForm'" " method, which is deprecated. 'getEditFormForPerson'" " does something vaguely similar." % (StubContactType,), people.__file__, lambda: list(self.organizer.getContactTypes())) def test_getContactTypesNewMethod(self): """ L{Organizer.getContactTypes} should emit a warning if it encounters an implementation which doesn't define the C{getEditFormForPerson} method. """ contactType = StubContactType([], None, []) contactType.getEditFormForPerson = None powerup = StubOrganizerPlugin( store=self.store, contactTypes=[contactType]) self.store.powerUp(powerup, IOrganizerPlugin) self.assertWarns( PendingDeprecationWarning, "IContactType now has the 'getEditFormForPerson'" " method, but %s did not implement it." % ( StubContactType,), people.__file__, lambda: list(self.organizer.getContactTypes())) def test_groupReadOnlyViews(self): """ L{Organizer.groupReadOnlyViews} should correctly group the read-only views of all available contact items. """ groupOneContactItems = [object(), object(), object()] groupOneContactTypes = [ StubContactType([], None, groupOneContactItems[:1], contactGroup=ContactGroup('One')), StubContactType([], None, groupOneContactItems[1:], contactGroup=ContactGroup('One'))] groupTwoContactItems = [object()] groupTwoContactTypes = [ StubContactType([], None, groupTwoContactItems, contactGroup=ContactGroup('Two'))] plugin = StubOrganizerPlugin( store=self.store, contactTypes=groupTwoContactTypes + groupOneContactTypes) self.store.powerUp(plugin, IOrganizerPlugin) person = Person(store=self.store) grouped = self.organizer.groupReadOnlyViews(person) for contactType in groupOneContactTypes + groupTwoContactTypes: self.assertEqual(contactType.queriedPeople, [person]) self.assertEqual(sorted(grouped.keys()), [None, 'One', 'Two']) self.assertEqual( [view.item for view in grouped['One']], groupOneContactItems) self.assertEqual( [view.item for view in grouped['Two']], groupTwoContactItems) # builtin (groupless) contact type stuff. builtinContactTypes = list(self.organizer.getContactTypes())[ :builtinContactTypeCount] self.assertEqual( len(grouped[None]), sum(len(list(contactType.getContactItems(person))) for contactType in builtinContactTypes)) def test_organizerPluginWithoutContactTypes(self): """ L{IOrganizerPlugin} powerups which don't have the C{getContactTypes} method should not cause problems with L{Organizer.getContactTypes} (The method was added after the interface was initially defined so there may be implementations which have not yet been updated). """ class OldOrganizerPlugin(object): """ An L{IOrganizerPlugin} which does not implement C{getContactTypes}. """ getOrganizerPlugins = Organizer.getOrganizerPlugins.im_func Organizer.getOrganizerPlugins = lambda self: [OldOrganizerPlugin()] try: organizer = Organizer() contactTypes = list(organizer.getContactTypes()) finally: Organizer.getOrganizerPlugins = getOrganizerPlugins self.assertEqual(contactTypes[builtinContactTypeCount:], []) def test_getContactCreationParameters(self): """ L{Organizer.getContactCreationParameters} should return a list containing a L{ListChangeParameter} for each contact type available in the system which allows multiple contact items. """ contactTypes = [StubContactType( (), None, None, allowMultipleContactItems=True, theDescriptiveIdentifier=u'Very Descriptive')] contactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=contactTypes) self.store.powerUp(contactPowerup, IOrganizerPlugin) parameters = list(self.organizer.getContactCreationParameters()) self.assertEqual(len(parameters), builtinContactTypeCount + 1) self.assertTrue( isinstance(parameters[builtinContactTypeCount], ListChangeParameter)) self.assertEqual( parameters[builtinContactTypeCount].modelObjectDescription, u'Very Descriptive') self.assertEqual( parameters[builtinContactTypeCount].name, qual(StubContactType)) def test_getContactCreationParametersUnrepeatable(self): """ L{Organizer.getContactCreationParameters} should return a list containing a L{FormParameter} for each contact type which doesn't support multiple contact items. """ contactTypeParameters = [Parameter('foo', TEXT_INPUT, lambda x: None)] contactTypes = [StubContactType( contactTypeParameters, None, None, allowMultipleContactItems=False)] contactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=contactTypes) self.store.powerUp(contactPowerup, IOrganizerPlugin) parameters = list(self.organizer.getContactCreationParameters()) liveFormParameter = parameters[builtinContactTypeCount] self.assertTrue(isinstance(liveFormParameter, FormParameter)) self.assertEqual(liveFormParameter.name, qual(StubContactType)) liveForm = liveFormParameter.form self.assertTrue(isinstance(liveForm, LiveForm)) self.assertEqual(liveForm.parameters, contactTypeParameters) def test_getContactEditorialParameters(self): """ L{Organizer.getContactEditorialParameters} should return a list containing a L{ListChangeParameter} for each contact type available in the system which supports multiple contact items. """ contactTypes = [StubContactType( (), None, [], theDescriptiveIdentifier=u'So Descriptive')] contactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=contactTypes) self.store.powerUp(contactPowerup, IOrganizerPlugin) person = self.organizer.createPerson(u'nickname') parameters = list(self.organizer.getContactEditorialParameters(person)) self.assertIdentical( parameters[builtinContactTypeCount][0], contactTypes[0]) self.failUnless( isinstance( parameters[builtinContactTypeCount][1], ListChangeParameter)) self.assertEqual( parameters[builtinContactTypeCount][1].modelObjectDescription, u'So Descriptive') def test_getContactEditorialParametersNone(self): """ The L{ListChangeParameter} returned by L{Organizer.getContactEditorialParameters} for a particular L{IContactType} should not have a model object or defaults dict if the L{IContactType} indicates that the contact item is immutable (by returning C{None} from its C{getParameters} implementation). """ class PickyContactType(StubContactType): def getParameters(self, contactItem): return self.parameters[contactItem] mutableContactItem = object() immutableContactItem = object() makeParam = lambda default=None: Parameter( 'foo', TEXT_INPUT, lambda x: None, default=default) contactType = PickyContactType( {mutableContactItem: [makeParam('the default')], None: [makeParam(None)], immutableContactItem: None}, None, [mutableContactItem, immutableContactItem]) contactTypes = [contactType] contactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=contactTypes) self.store.powerUp(contactPowerup, IOrganizerPlugin) person = self.organizer.createPerson(u'nickname') parameters = list( self.organizer.getContactEditorialParameters(person)) (gotContactType, parameter) = parameters[builtinContactTypeCount] self.assertEqual(parameter.modelObjects, [mutableContactItem]) self.assertEqual(parameter.defaults, [{'foo': 'the default'}]) def test_getContactEditorialParametersUnrepeatable(self): """ L{Organizer.getContactEditorialParameters} should return a list containing a L{FormParameter} for each contact type available in the system which doesn't support multiple contact items. """ contactTypeParameters = [Parameter('foo', TEXT_INPUT, lambda x: x)] contactTypes = [StubContactType( contactTypeParameters, None, [None], allowMultipleContactItems=False)] contactPowerup = StubOrganizerPlugin( store=self.store, contactTypes=contactTypes) self.store.powerUp(contactPowerup, IOrganizerPlugin) person = self.organizer.createPerson(u'nickname') parameters = list(self.organizer.getContactEditorialParameters(person)) (contactType, liveFormParameter) = parameters[builtinContactTypeCount] self.assertIdentical(contactType, contactTypes[0]) self.assertTrue(isinstance(liveFormParameter, FormParameter)) self.assertEqual(liveFormParameter.name, qual(StubContactType)) liveForm = liveFormParameter.form self.assertTrue(isinstance(liveForm, LiveForm)) self.assertEqual(liveForm.parameters, contactTypeParameters) def test_getContactEditorialParametersDefaults(self): """ L{Organizer.getContactEditorialParameters} should return some parameters with correctly initialized lists of defaults and model objects. """ person = self.organizer.createPerson(u'nickname') contactItems = [PostalAddress(store=self.store, person=person, address=u'1'), PostalAddress(store=self.store, person=person, address=u'2')] editParameters = list(self.organizer.getContactEditorialParameters(person)) (editType, editParameter) = editParameters[2] self.assertEqual( editParameter.defaults, [{u'address': u'1'}, {u'address': u'2'}]) self.assertEqual( editParameter.modelObjects, contactItems) def test_navigation(self): """ L{Organizer.getTabs} should return a single tab, 'People', that points to itself. """ tabs = self.organizer.getTabs() self.assertEqual(len(tabs), 1) tab = tabs[0] self.assertEqual(tab.name, "People") self.assertEqual(tab.storeID, self.organizer.storeID) self.assertEqual(tab.children, ()) self.assertEqual(tab.authoritative, True) self.assertEqual(tab.linkURL, None) def test_getPeopleTags(self): """ L{Organizer.getPeopleTags} should return a set containing each tag which has been applied to a L{Person}. """ alice = self.organizer.createPerson(u'Alice') frank = self.organizer.createPerson(u'Frank') catalog = tags.Catalog(store=self.store) catalog.tag(alice, u'person') catalog.tag(frank, u'person') catalog.tag(alice, u'girl') catalog.tag(frank, u'boy') # tag the organizer for laughs catalog.tag(self.organizer, u'organizer') self.assertEqual( self.organizer.getPeopleTags(), set(('person', 'girl', 'boy'))) class POBox(Item): number = text() def _keyword(contactType): return contactType.uniqueIdentifier().encode('ascii') CONTACT_EMAIL = u'jlp@starship.enterprise' CONTACT_ADDRESS = u'123 Street Rd' def createAddPersonContactInfo(store): """ Create a structure suitable to be passed to AddPersonFragment.addPerson. Since the structure keeps changing slightly, this lets some tests be independent of those details and so avoids requiring them to change every time the structure does. """ return { _keyword(EmailContactType(store)): ListChanges( [CreateObject({u'email': CONTACT_EMAIL}, lambda x: None)], [], []), _keyword(PostalContactType()): ListChanges( [CreateObject({u'address': CONTACT_ADDRESS}, lambda x: None)], [], [])} class PeopleTests(unittest.TestCase): def setUp(self): """ Create an in-memory store and organizer. """ self.user = emptyMantissaUserStore() self.organizer = Organizer(store=self.user) installOn(self.organizer, self.user) def testPersonCreation(self): beforeCreation = extime.Time() p = self.organizer.personByName(u'testuser') afterCreation = extime.Time() self.assertEquals(p.name, u'testuser') self.failUnless( beforeCreation <= p.created <= afterCreation, "not (%r <= %r <= %r)" % (beforeCreation, p.created, afterCreation)) # Make sure people from that organizer don't collide with # people from a different organizer another = Organizer(store=self.user) q = another.personByName(u'testuser') self.failIfIdentical(p, q) self.assertEquals(q.name, u'testuser') # And make sure people within a single Organizer don't trample # on each other. notQ = another.personByName(u'nottestuser') self.failIfIdentical(q, notQ) self.assertEquals(q.name, u'testuser') self.assertEquals(notQ.name, u'nottestuser') def test_getEmailAddresses(self): """ Verify that getEmailAddresses yields the associated email address strings for a person. """ p = Person(store=self.user) EmailAddress(store=self.user, person=p, address=u'a@b.c') EmailAddress(store=self.user, person=p, address=u'c@d.e') # Ordering is undefined, so let's use a set. self.assertEquals(set(p.getEmailAddresses()), set([u'a@b.c', u'c@d.e'])) def test_getEmailAddress(self): """ Verify that getEmailAddress yields the only associated email address for a person if it is the only one. """ p = Person(store=self.user) EmailAddress(store=self.user, person=p, address=u'a@b.c') self.assertEquals(p.getEmailAddress(), u'a@b.c') def testPersonRetrieval(self): name = u'testuser' firstPerson = self.organizer.personByName(name) self.assertIdentical(firstPerson, self.organizer.personByName(name)) def test_docFactory(self): """ L{AddPersonFragment.docFactory.load} should not return C{None}. """ self.assertNotIdentical( AddPersonFragment(self.organizer).docFactory.load(), None) def test_addPerson(self): """ L{AddPersonFragment.addPerson} should add the person. """ name = u'Billy Spade' addPerson = AddPersonFragment(self.organizer) addPerson.addPerson(name) self.assertEqual( self.user.query(Person, Person.name == name).count(), 1) def test_addPersonParameters(self): """ L{AddPersonFragment.render_addPersonForm} should return a L{LiveForm} with several fixed parameters. """ addPersonFrag = AddPersonFragment(self.organizer) # Whatever is in _baseParameters should end up in the resulting form's # parameters. Explicitly define _baseParameters here so that changes # to the actual value don't affect this test. The actual value is # effectively a declaration, so the only thing one could test about it # is that it is equal to itself, anyway. addPersonFrag._baseParameters = baseParameters = [ Parameter('foo', TEXT_INPUT, unicode, 'Foo')] addPersonForm = addPersonFrag.render_addPersonForm(None, None) self.assertEqual(addPersonForm.parameters, baseParameters) def test_addPersonValueError(self): """ L{AddPersonFragment.addPerson} raises L{InputError} if L{Organizer.createPerson} raises a L{ValueError}. """ addPersonFragment = AddPersonFragment(self.organizer) def stubCreatePerson(*a, **kw): raise ValueError("Stub nickname rejection") object.__setattr__(self.organizer, 'createPerson', stubCreatePerson) exception = self.assertRaises( InputError, addPersonFragment.addPerson, u'nickname') self.assertEqual(exception.args, ("Stub nickname rejection",)) self.assertTrue(isinstance(exception.args[0], unicode)) def test_linkToPerson(self): """ L{Organizer.linkToPerson} generates an URL that is the same as linking to the private person item. """ privapp = self.user.findUnique(PrivateApplication) p = Person(store=self.user) self.assertEqual(self.organizer.linkToPerson(p), privapp.linkTo(p.storeID)) def test_urlForViewState(self): """ L{Organizer.urlForViewState} should generate a valid, correctly quoted url. """ organizerURL = IWebTranslator(self.user).linkTo( self.organizer.storeID) person = self.organizer.createPerson(u'A Person') self.assertEqual( str(self.organizer.urlForViewState( person, ORGANIZER_VIEW_STATES.EDIT)), organizerURL + '?initial-person=A%20Person&initial-state=edit') class PersonDetailFragmentTests(unittest.TestCase): """ Tests for L{xmantissa.people.PersonDetailFragment}. """ def test_mugshotUploadForm(self): """ L{PersonDetailFragment}'s I{mugshotUploadForm} child should return a L{MugshotUploadForm}. """ person = StubPerson([]) person.organizer = StubOrganizer() fragment = PersonDetailFragment(person) (resource, segments) = fragment.locateChild( None, ('mugshotUploadForm',)) self.assertTrue(isinstance(resource, MugshotUploadForm)) self.assertIdentical(resource.person, person) def test_getPersonURL(self): """ Test that L{getPersonURL} returns the URL for the Person. """ person = StubPerson([]) person.organizer = StubOrganizer() self.assertEqual(getPersonURL(person), "/person/Alice") def test_mugshotChild(self): """ L{PersonDetailFragment}'s I{mugshot} child should return a L{MugshotResource} wrapping the result of calling L{Person.getMugshot}. """ theMugshot = object() class StubMugshotPerson(StubPerson): organizer = StubOrganizer() def getMugshot(self): return theMugshot fragment = PersonDetailFragment(StubMugshotPerson([])) (res, segments) = fragment.locateChild(None, ('mugshot',)) self.assertTrue(isinstance(res, MugshotResource)) self.assertIdentical(res.mugshot, theMugshot) self.assertEqual(segments, ()) class PersonScrollingFragmentTests(unittest.TestCase): """ Tests for L{PersonScrollingFragment}. """ def setUp(self): """ Make an L{Organizer}. """ self.store = Store() self.organizer = Organizer(store=self.store) installOn(self.organizer, self.store) def test_scrollingAttributes(self): """ L{PersonScrollingFragment} should have the attributes its base class wants to use. """ baseConstraint = object() fragment = PersonScrollingFragment( self.organizer, baseConstraint, Person.name, StubTranslator(None, None)) self.assertIdentical(fragment.baseConstraint, baseConstraint) self.assertIdentical( fragment.currentSortColumn.sortAttribute(), Person.name) self.assertIdentical(fragment.itemType, Person) self.assertEqual(len(fragment.columns), 3) self.assertEqual(fragment.columns['name'], Person.name) self.assertTrue(isinstance(fragment.columns['vip'], UnsortableColumn)) self.assertEqual(fragment.columns['vip'].attribute, Person.vip) self.assertTrue( isinstance(fragment.columns['mugshotURL'], MugshotURLColumn)) self.assertIdentical( fragment.columns['mugshotURL'].organizer, self.organizer) def test_initialArguments(self): """ L{PersonScrollingFragment.getInitialArguments} should include the store owner person's name in its result. """ storeOwnerPersonName = u'Store Owner' self.organizer.storeOwnerPerson.name = storeOwnerPersonName fragment = PersonScrollingFragment( self.organizer, object(), Person.name, StubTranslator(None, None)) self.assertEqual( fragment.getInitialArguments(), (ScrollingElement.getInitialArguments(fragment) + [storeOwnerPersonName])) def test_filterByFilter(self): """ L{PersonScrollingFragment.filterByFilter} should change the scrolltable's base constraint to the query comparison of the named filter. """ queryComparison = object() class MockPeopleFilter: def getPeopleQueryComparison(_self, store): self.assertIdentical(store, self.store) return queryComparison fragment = PersonScrollingFragment( self.organizer, object(), Person.name, StubTranslator(None, None)) fragment.filters = { u'test_filterByFilter': MockPeopleFilter()} filterByFilter = expose.get(fragment, 'filterByFilter') filterByFilter(u'test_filterByFilter') self.assertIdentical( fragment.baseConstraint, queryComparison) class OrganizerFragmentTests(unittest.TestCase): """ Tests for L{OrganizerFragment}. @ivar contactTypes: A list of L{StubContactType} instances which will be returned by the C{getContactTypes} method of the stub organizer used by these tests. @ivar organizer: The L{StubOrganizer} which is used by these tests. @ivar fragment: An L{OrganizerFragment} to test. @ivar deletedPeople: A list of the arguments which have been passed to the C{deletePerson} method of L{organizer}. """ def setUp(self): """ Create an L{OrganizerFragment} wrapped around a double for L{Organizer}. """ deletedPeople = [] contactTypes = [] self.store = Store() self.contactTypes = contactTypes self.organizer = StubOrganizer( self.store, contactTypes, deletedPeople) self.fragment = OrganizerFragment(self.organizer) self.deletedPeople = deletedPeople def test_head(self): """ L{OrganizerFragment.head} should return C{None}. """ self.assertIdentical(self.fragment.head(), None) def test_peopleTable(self): """ L{OrganizerFragment}'s I{peopleTable} renderer should return a L{PersonScrollingFragment}. """ peopleTableRenderer = renderer.get(self.fragment, 'peopleTable') scroller = peopleTableRenderer(None, None) self.assertTrue(isinstance(scroller, PersonScrollingFragment)) def test_peopleFilters(self): """ L{OrganizerFragment}'s I{peopleFilters} renderer should return an instance of its tag's I{filter} pattern for each filter, except the first, which should use the I{selected-filter} pattern. """ filterNames = list('acyx') peopleFilters = [record('filterName')(name) for name in filterNames] self.organizer.peopleFilters = peopleFilters peopleFiltersRenderer = renderer.get(self.fragment, 'peopleFilters') tag = div[ div(usedpattern='filter', pattern='filter')[slot('name')], div(usedpattern='selected-filter', pattern='selected-filter')[slot('name')]] patterns = list(peopleFiltersRenderer(None, tag)) self.assertEqual(len(patterns), len(peopleFilters)) selectedPattern = patterns.pop(0) selectedFilterName = filterNames.pop(0) self.assertEqual( selectedPattern.slotData, {'name': selectedFilterName}) self.assertEqual( selectedPattern.attributes['usedpattern'], 'selected-filter') for (pattern, filterName) in zip(patterns, filterNames): self.assertEqual(pattern.slotData, {'name': filterName}) self.assertEqual(pattern.attributes['usedpattern'], 'filter') def test_getAddPerson(self): """ L{OrganizerFragment.getAddPerson} should return an L{AddPersonFragment}. """ addPersonFragment = expose.get(self.fragment, 'getAddPerson')() self.assertTrue(isinstance(addPersonFragment, AddPersonFragment)) self.assertIdentical(addPersonFragment.organizer, self.organizer) self.assertIdentical(addPersonFragment.fragmentParent, self.fragment) def test_getImportPeople(self): """ L{OrganizerFragment.getImportPeople} should return an L{ImportPeopleWidget}. """ widget = expose.get(self.fragment, 'getImportPeople')() self.assertTrue(isinstance(widget, ImportPeopleWidget)) self.assertIdentical(widget.organizer, self.organizer) self.assertIdentical(widget.fragmentParent, self.fragment) def test_getEditPerson(self): """ L{OrganizerFragment.getEditPerson} should return an L{EditPersonView}. """ name = u'testuser' person = Person() self.organizer.people[name] = person editPersonFragment = expose.get( self.fragment, 'getEditPerson')(name) self.assertTrue(isinstance(editPersonFragment, EditPersonView)) self.assertIdentical(editPersonFragment.person, person) self.assertIdentical(editPersonFragment.fragmentParent, self.fragment) def test_deletePerson(self): """ L{OrganizerFragment.deletePerson} should call L{Organizer.deletePerson}. """ name = u'testuser' person = Person() self.organizer.people[name] = person expose.get(self.fragment, 'deletePerson', None)(name) self.assertEqual(self.fragment.organizer.deletedPeople, [person]) def test_getPersonPluginWidget(self): """ L{OrganizerFragment.getPersonPluginWidget} should return a L{PersonPluginView} for the named person. """ name = u'testuser' person = Person() self.organizer.people[name] = person self.organizer.organizerPlugins = plugins = [object()] widget = expose.get( self.fragment, 'getPersonPluginWidget')(name) self.assertTrue(isinstance(widget, PersonPluginView)) self.assertEqual(widget.plugins, plugins) self.assertIdentical(widget.person, person) self.assertIdentical(widget.fragmentParent, self.fragment) def test_initialArgumentsNoInitialPerson(self): """ When L{Organizer.initialPerson} is C{None}, L{Organizer.getInitialArguments} should be a one-element tuple containing the name of the store owner person. """ storeOwnerPersonName = u'Alice' self.organizer.storeOwnerPerson = Person( name=storeOwnerPersonName) self.assertEqual( self.fragment.getInitialArguments(), (storeOwnerPersonName,)) def test_initialArgumentsInitialPerson(self): """ When L{Organizer.initialPerson} is not C{None}, L{Organizer.getInitialArguments} should be a three-element tuple containing the name of the store owner person, the name of the initial person, and the initial view state. """ storeOwnerPersonName = u'Alice' initialPersonName = u'Bob' initialState = ORGANIZER_VIEW_STATES.EDIT self.organizer.storeOwnerPerson = Person( name=storeOwnerPersonName) initialPerson = Person(name=initialPersonName) fragment = OrganizerFragment( self.organizer, initialPerson, initialState) self.assertEqual( fragment.getInitialArguments(), (storeOwnerPersonName, initialPersonName, initialState)) class OrganizerFragmentBeforeRenderTestCase(unittest.TestCase): """ Tests for L{OrganizerFragment.beforeRender}. These tests require more expensive setup than is provided by L{OrganizerFragmentTests}. """ def setUp(self): """ Make a substore with a L{PrivateApplication} and an L{Organizer}. """ self.siteStore = Store(filesdir=self.mktemp()) def siteStoreTxn(): Mantissa().installSite(self.siteStore, u"example.com", u"", False) userAccount = Create().addAccount( self.siteStore, u'testuser', u'example.com', u'password') self.userStore = userAccount.avatars.open() self.siteStore.transact(siteStoreTxn) def userStoreTxn(): self.organizer = Organizer(store=self.userStore) installOn(self.organizer, self.userStore) self.fragment = OrganizerFragment(self.organizer) self.userStore.transact(userStoreTxn) def _makeContextWithRequestArgs(self, args): """ Make a context which contains a request with args C{args}. """ request = FakeRequest() request.args = args return context.PageContext( tag=None, parent=context.RequestContext( tag=request)) def test_validPersonAndValidState(self): """ L{OrganizerFragment.beforeRender} should correctly initialize the L{OrganizerFragment} if a valid person name and valid initial view state are present in the query args. """ person = self.organizer.createPerson(u'Andr\xe9') self.fragment.beforeRender( self._makeContextWithRequestArgs( {'initial-person': [person.name.encode('utf-8')], 'initial-state': [ORGANIZER_VIEW_STATES.EDIT.encode('utf-8')]})) self.assertIdentical(self.fragment.initialPerson, person) self.assertEqual(self.fragment.initialState, ORGANIZER_VIEW_STATES.EDIT) def test_invalidPersonAndValidState(self): """ L{OrganizerFragment.beforeRender} shouldn't modify the L{OrganizerFragment} if an invalid person name and valid view state are present in the query args. """ self.fragment.beforeRender( self._makeContextWithRequestArgs( {'initial-person': ['Alice'], 'initial-state': [ORGANIZER_VIEW_STATES.EDIT.encode('utf-8')]})) self.assertIdentical(self.fragment.initialPerson, None) self.assertIdentical(self.fragment.initialState, None) def test_validPersonAndInvalidState(self): """ Similar to L{test_invalidPersonAndValidState}, but for a valid person name and invalid initial view state. """ person = self.organizer.createPerson(u'Alice') for args in [{'initial-person': ['Alice']}, {'initial-person': ['Alice'], 'initial-state': [u'\xe9dit'.encode('utf-8')]}]: self.fragment.beforeRender(self._makeContextWithRequestArgs(args)) self.assertIdentical(self.fragment.initialPerson, None) self.assertIdentical(self.fragment.initialState, None) class AddPersonFragmentTests(unittest.TestCase): """ Tests for L{AddPersonFragment}. """ def test_jsClass(self): """ L{AddPersonFragment} should have a customized C{jsClass} in order to expose methods on its L{LiveForm}. """ self.assertEqual(AddPersonFragment.jsClass, u'Mantissa.People.AddPerson') def test_renders(self): """ An L{AddPersonFragment} should be renderable. """ user = emptyMantissaUserStore() installOn(PrivateApplication(store=user), user) organizer = Organizer(store=user) fragment = AddPersonFragment(organizer) result = renderLiveFragment(fragment) self.assertTrue(isinstance(result, str)) def test_addPersonFormRenderer(self): """ L{AddPersonFragment.render_addPersonForm} should return a L{LiveForm} with a customized I{jsClass} attribute. """ store = Store() organizer = Organizer(store=store) fragment = AddPersonFragment(organizer) form = fragment.render_addPersonForm(None, None) self.assertTrue(isinstance(form, LiveForm)) self.assertEqual(form.jsClass, u'Mantissa.People.AddPersonForm') class ImportPeopleWidgetTests(unittest.TestCase): """ Tests for L{ImportPeopleWidget}. """ def test_parseAddresses(self): """ L{_parseAddresses} should extract valid-looking names and addresses. """ def _assert(input, expected): self.assertEqual(ImportPeopleWidget._parseAddresses(input), expected) # Empty for s in [u'', u' ', u'<>', u',', u'<>, <>']: _assert(s, []) # Name defaulting to local-part _assert(u'alice@example.com', [(u'alice', u'alice@example.com')]) _assert(u' alice@example.com, ', [(u'alice', u'alice@example.com')]) # Separators and display names for sep in u', ', u'\n', u', foo <>, ': _assert(sep.join([u'alice@example.com', u'bob@example.com']), [(u'alice', u'alice@example.com'), (u'bob', u'bob@example.com')]) _assert(sep.join([u'', u'Alice Allison ', u'"Bob Boberton" ']), [(u'Alice.Allison', u'Alice.Allison@example.com'), (u'Alice Allison', u'alice@example.com'), (u'Bob Boberton', u'bob@example.com')]) def test_importAddresses(self): """ L{ImportPeopleWidget.importAddresses} should create entries for the given addresses (ignoring names/addresses that exist already). """ store = Store() organizer = Organizer(store=store) owner = organizer.storeOwnerPerson importFragment = ImportPeopleWidget(organizer) self.assertEqual(list(store.query(Person)), [owner]) importFragment.importAddresses([]) self.assertEqual(list(store.query(Person)), [owner]) addresses = [(u'Alice', u'alice@example.com'), (u'Bob', u'bob@example.com')] # Import twice to check idempotency, and make sure both the name and # address are checked. for input in [addresses, addresses, [(u'Alice', u'chaff'), (u'chaff', u'bob@example.com')]]: importFragment.importAddresses(input) self.assertEqual(set((p.name, p.getEmailAddress()) for p in store.query(Person) if p is not owner), set(addresses)) class ReadOnlyContactInfoViewTestCase(unittest.TestCase): """ Tests for L{ReadOnlyContactInfoView}. """ def test_personSummary(self): """ The I{personSummary} renderer should return a L{PersonSummaryView} for the wrapped person. """ person = Person() personSummary = renderer.get( ReadOnlyContactInfoView(person), 'personSummary', None) fragment = personSummary(None, None) self.assertTrue(isinstance(fragment, PersonSummaryView)) self.assertIdentical(fragment.person, person) def test_contactInfo(self): """ The I{contactInfo} renderer should return the suitiably-transformed result of calling L{Organizer.groupReadOnlyViews}. """ person = StubPerson([]) contactItems = [object(), object(), object()] readOnlyViews = [div(), div(), div()] person.organizer = StubOrganizer( groupedReadOnlyViews={ 'One': readOnlyViews[:1], None: readOnlyViews[1:]}) contactInfo = renderer.get( ReadOnlyContactInfoView(person), 'contactInfo', None) tag = div[ div(pattern='contact-group')[ slot('name'), slot('views')]] result = list(contactInfo(None, tag)) self.assertEqual( person.organizer.groupedReadOnlyViewPeople, [person]) self.assertEqual(len(result), 2) grouplessReadOnlyViews = result[0] self.assertEqual(len(grouplessReadOnlyViews), 2) self.assertEqual(grouplessReadOnlyViews, readOnlyViews[1:]) contactGroupPattern = result[1] self.assertEqual( contactGroupPattern.slotData['name'], 'One') self.assertEqual( contactGroupPattern.slotData['views'], readOnlyViews[:1]) class PersonSummaryViewTestCase(unittest.TestCase): """ Tests for L{PersonSummaryView}. """ def test_mugshotURL(self): """ The I{mugshotURL} renderer should return the correct URL if the person has a mugshot. """ store = Store(self.mktemp()) organizer = Organizer(store=store) installOn(organizer, store) person = Person(store=store, organizer=organizer) Mugshot( store=store, person=person, body=store.newFilePath(u'body'), smallerBody=store.newFilePath(u'smallerBody'), type=u'image/jpeg') mugshotURL = renderer.get( PersonSummaryView(person), 'mugshotURL', None) self.assertEqual( mugshotURL(None, None), organizer.linkToPerson(person) + '/mugshot/smaller') def test_mugshotURLNoMugshot(self): """ The I{mugshotURL} renderer should return the correct URL if the person has no mugshot. """ store = Store() organizer = Organizer(store=store) installOn(organizer, store) person = Person(store=store, organizer=organizer) mugshotURL = renderer.get( PersonSummaryView(person), 'mugshotURL', None) self.assertEqual( mugshotURL(None, None), organizer.linkToPerson(person) + '/mugshot/smaller') def test_personName(self): """ The I{personName} renderer should return the display name of the wrapped person. """ name = u'A Person Name' personName = renderer.get( PersonSummaryView(Person(store=Store(), name=name)), 'personName', None) self.assertEqual(personName(None, None), name) def test_vipStatus(self): """ The I{vipStatus} renderer should return its tag if the wrapped person is a VIP. """ vipStatus = renderer.get( PersonSummaryView(Person(store=Store(), vip=True)), 'vipStatus', None) tag = object() self.assertIdentical(vipStatus(None, tag), tag) def test_vipStatusNoVip(self): """ The I{vipStatus} renderer should return the empty string if the wrapped person is not a VIP. """ vipStatus = renderer.get( PersonSummaryView(Person(store=Store(), vip=False)), 'vipStatus', None) self.assertEqual(vipStatus(None, None), '') class EditPersonViewTests(unittest.TestCase): """ Tests for L{EditPersonView}. """ def setUp(self): """ Create an L{EditPersonView} wrapped around a stub person and stub organizer. """ self.contactType = StubContactType((), None, None) self.contactParameter = ListChangeParameter( u'blah', [], [], modelObjects=[]) self.person = StubPerson(None) self.person.organizer = self.organizer = StubOrganizer( contactTypes=[self.contactType], contactEditorialParameters={self.person: [ (self.contactType, self.contactParameter)]}) self.view = EditPersonView(self.person) def test_editContactItems(self): """ L{EditPersonView.editContactItems} should take a dictionary mapping parameter names to values and update its person's contact information in a transaction. """ transactions = [] transaction = record('function args kwargs') class StubStore(object): def transact(self, f, *a, **kw): transactions.append(transaction(f, a, kw)) self.person.store = StubStore() contactType = StubContactType((), None, None) self.view.contactTypes = {'contactTypeName': contactType} MODEL_OBJECT = object() # Submit the form submission = object() self.view.editContactItems(u'nick', contactTypeName=submission) # A transaction should happen, and nothing should change until it's # run. self.assertEqual(len(transactions), 1) self.assertEqual(self.person.name, StubPerson.name) self.assertEqual(contactType.editedContacts, []) # Okay run it. transactions[0].function( *transactions[0].args, **transactions[0].kwargs) self.assertEqual( self.person.organizer.editedPeople, [(self.person, u'nick', [(contactType, submission)])]) def test_editorialContactForms(self): """ L{EditPersonView.editorialContactForms} should return an instance of L{EditorialContactForms} for the wrapped L{Person} as a child of the tag it is passed. """ editorialContactForms = renderer.get( self.view, 'editorialContactForms') tag = div() forms = editorialContactForms(None, tag) self.assertEqual(forms.tagName, 'div') self.assertEqual(forms.attributes, {}) self.assertEqual(len(forms.children), 1) form = forms.children[0] self.assertTrue(isinstance(form, LiveForm)) self.assertEqual(form.callable, self.view.editContactItems) self.assertEqual(form.parameters[1:], [self.contactParameter]) self.assertIdentical(form.fragmentParent, self.view) self.assertEqual( self.view.contactTypes[form.parameters[1].name], self.contactType) def test_mugshotFormURL(self): """ The I{mugshotFormURL} renderer of L{EditPersonView} should return the correct URL. """ mugshotFormURLRenderer = renderer.get( self.view, 'mugshotFormURL') self.assertEqual( mugshotFormURLRenderer(None, None), '/person/Alice/mugshotUploadForm') def test_renderable(self): """ L{EditPersonView} should be renderable in the typical manner. """ # XXX I have no hope of asserting anything meaningful about the return # value of renderLiveFragment. However, even calling it at all pointed # out that: there was no docFactory; the fragmentName didn't reference # an extant template; the LiveForm had no fragment parent (for which I # also updated test_editorialContactForms to do a direct # assertion). -exarkun user = emptyMantissaUserStore() installOn(PrivateApplication(store=user), user) organizer = Organizer(store=user) installOn(organizer, user) person = organizer.createPerson(u'Alice') markup = renderLiveFragment(EditPersonView(person)) self.assertIn(self.view.jsClass, markup) def test_makeEditorialLiveForms(self): """ L{EditPersonView.makeEditorialLiveForms} should make a single liveform with the correct parameters if no contact types specify custom edit forms. """ liveForms = self.view.makeEditorialLiveForms() self.assertEqual(len(liveForms), 1) liveForm = liveForms[0] self.assertEqual(len(liveForm.parameters), 2) nameParam = liveForm.parameters[0] self.assertEqual(nameParam.name, 'nickname') self.assertEqual(nameParam.default, self.person.name) self.assertEqual(nameParam.type, TEXT_INPUT) contactParam = liveForm.parameters[1] self.assertIdentical(contactParam, self.contactParameter) def test_makeEditorialLiveFormsCustom(self): """ Contact types with custom forms should have their forms included in the result of L{EditPersonView.makeEditorialLiveForms}. """ theEditorialForm = LiveForm(lambda: None, ()) self.contactType.editorialForm = theEditorialForm liveForms = self.view.makeEditorialLiveForms() self.assertEqual(len(liveForms), 2) liveForm = liveForms[1] self.assertIdentical(liveForm, theEditorialForm) self.assertEqual(self.contactType.editedContacts, [self.person]) def test_makeEditorialLiveFormsNoMethod(self): """ L{EditPersonView.makeEditorialLiveForms} should work with contact types which don't define a C{getEditFormForPerson}. """ self.contactType.getEditFormForPerson = None (form,) = self.view.makeEditorialLiveForms() self.assertIdentical( form.parameters[1], self.contactParameter) class StoreOwnerPersonTestCase(unittest.TestCase): """ Tests for L{Organizer._makeStoreOwnerPerson} and related functionality. """ def test_noStore(self): """ L{Organizer.storeOwnerPerson} should be C{None} if the L{Organizer} doesn't live in a store. """ self.assertIdentical(Organizer().storeOwnerPerson, None) def test_emptyStore(self): """ Test that when an L{Organizer} is inserted into an empty store, L{Organizer.storeOwnerPerson} is set to a L{Person} with an empty string for a name. """ store = Store() organizer = Organizer(store=store) self.failUnless(organizer.storeOwnerPerson) self.assertIdentical(organizer.storeOwnerPerson.organizer, organizer) self.assertEqual(organizer.storeOwnerPerson.name, u'') def test_differentStoreOwner(self): """ Test that when an L{Organizer} is passed a C{storeOwnerPerson} explicitly, it does not create any additional L{Person} items. """ store = Store() person = Person(store=store) organizer = Organizer(store=store, storeOwnerPerson=person) self.assertIdentical(store.findUnique(Person), person) self.assertIdentical(organizer.storeOwnerPerson, person) def test_storeOwnerDeletion(self): """ Verify that we fail if we attempt to delete L{Organizer.storeOwnerPerson}. """ store = Store() organizer = Organizer(store=store) self.assertRaises( DeletionDisallowed, organizer.storeOwnerPerson.deleteFromStore) def test_personNameFromUserInfo(self): """ The L{Person} created to be the store owner by L{Organizer} should have its I{name} attribute set to a string computed from the L{UserInfo} item. """ name = u'Joe Rogers' store = Store() UserInfo(store=store, realName=name) organizer = Organizer(store=store) self.assertEqual(organizer.storeOwnerPerson.name, name) def test_personEmailFromUserInfo(self): """ The L{Person} created to be the store owner by L{Organizer} should have an L{EmailAddress} item created with an address computed from the available 'email' login methods. (In the course of doing so, make sure that it creates them correctly and notifies the organizer plugins of the L{EmailAddress} item's existence.) """ siteStore = Store() ls = LoginSystem(store=siteStore) # It should NOT consider the login method created implicitly as a # result of the signup process. Too bad that actually defaults to the # 'email' protocol! acct = ls.addAccount(u'jim.bean', u'service.example.com', u'nevermind', internal=True) userStore = acct.avatars.open() acct.addLoginMethod(localpart=u'jim', domain=u'bean.example.com', protocol=u'email', verified=False, internal=False) stub = StubOrganizerPlugin(store=userStore) # This is _slightly_ unrealistic for real-world usage, because # generally L{IOrganizerPlugin} providers will also just happen to # depend on the organizer (and therefore won't get notified of this # first item). However, nothing says they *need* to depend on it, and # if they don't, the contact items should be created the proper, # suggested way. userStore.powerUp(stub, IOrganizerPlugin) organizer = Organizer(store=userStore) person = organizer.storeOwnerPerson self.assertEqual(list(person.getEmailAddresses()), [u'jim@bean.example.com']) self.assertEqual(stub.createdPeople, [organizer.storeOwnerPerson]) self.assertEqual(stub.createdContactItems, [userStore.findUnique(EmailAddress)]) class MugshotURLColumnTestCase(unittest.TestCase): """ Tests for L{MugshotURLColumn}. """ def test_interface(self): """ L{MugshotURLColumn} should provide L{IColumn}. """ self.assertNotIdentical( IColumn(MugshotURLColumn(None, None), None), None) def test_extractValue(self): """ L{MugshotURLColumn.extractValue} should return the correct URL. """ organizer = StubOrganizer() person = Person(name=u'test_extractValue') self.assertEqual( MugshotURLColumn(organizer, None).extractValue(None, person), organizer.linkToPerson(person) + u'/mugshot/smaller') def test_sortAttribute(self): """ L{MugshotURLColumn.sortAttribute} should return C{None}. """ self.assertIdentical( MugshotURLColumn(None, None).sortAttribute(), None) def test_getType(self): """ L{MugshotURLColumn.getType} should return C{text}. """ self.assertEqual(MugshotURLColumn(None, None).getType(), 'text') def test_toComparableValue(self): """ L{MugshotURLColumn.toComparableValue} should throw L{NotImplementedError}. """ self.assertRaises( NotImplementedError, MugshotURLColumn(None, None).toComparableValue, u'/person/xyz/mugshot/smaller') class ContactInfoOrganizerPluginTestCase(unittest.TestCase): """ Tests for L{ContactInfoOrganizerPlugin}. """ def test_name(self): """ L{ContactInfoOrganizerPlugin.name} should be set. """ self.assertEqual(ContactInfoOrganizerPlugin.name, u'Contact') def test_personalize(self): """ L{ContactInfoOrganizerPlugin.personalize} should return a L{ReadOnlyContactInfoView}. """ plugin = ContactInfoOrganizerPlugin() person = Person() result = plugin.personalize(person) self.assertTrue(isinstance(result, ReadOnlyContactInfoView)) self.assertIdentical(result.person, person) def test_getContactTypes(self): """ L{ContactInfoOrganizerPlugin} shouldn't supply any contact types. """ plugin = ContactInfoOrganizerPlugin() self.assertEqual(plugin.getContactTypes(), ()) def test_getPeopleFilters(self): """ L{ContactInfoOrganizerPlugin} shouldn't supply any people filters. """ plugin = ContactInfoOrganizerPlugin() self.assertEqual(plugin.getPeopleFilters(), ()) class PersonPluginViewTestCase(unittest.TestCase): """ Tests for L{PersonPluginView}. """ def _doGetPluginWidgetTest(self, personalization): """ Set up a L{PersonPluginView} and try to request the given personalized view from it using I{getPluginWidget}, returning the result. """ person = Person() thePlugin = StubOrganizerPlugin( store=Store(), name=u'test_getPluginWidget2') thePlugin.personalization = personalization plugins = [StubOrganizerPlugin(name=u'test_getPluginWidget1'), thePlugin] view = PersonPluginView(plugins, person) getPluginWidget = expose.get(view, 'getPluginWidget') result = getPluginWidget('test_getPluginWidget2') self.assertEqual(thePlugin.personalizedPeople, [person]) return result def test_getPluginWidget(self): """ L{PersonPluginView}'s I{getPluginWidget} remote method should return the appropriate view. """ personalization = LiveElement() self.assertIdentical( self._doGetPluginWidgetTest(personalization), personalization) def test_getPluginWidgetLegacy(self): """ L{PersonPluginView}'s I{getPluginWidget} remote method should wrap the view with L{_ElementWrapper} if it's not a L{LiveElement}. """ personalization = Element() result = self._doGetPluginWidgetTest(personalization) self.assertTrue(isinstance(result, _ElementWrapper)) self.assertIdentical(result.wrapped, personalization) def test_pluginTabbbedPane(self): """ L{PersonPluginView}'s I{pluginTabbedPane} renderer return a correctly-configured L{tabbedPane.TabbedPaneFragment}. """ store = Store() pluginNames = [ u'test_pluginTabbbedPane1', u'test_pluginTabbbedPane2'] view = PersonPluginView( [StubOrganizerPlugin( store=store, name=name) for name in pluginNames], Person()) view.plugins[0].personalization = personalization = LiveElement() pluginTabbedPaneRenderer = renderer.get( view, 'pluginTabbedPane', None) tag = div[div(pattern='pane-body', secret='test_pluginTabbbedPane')] frag = pluginTabbedPaneRenderer(None, tag) self.assertTrue(isinstance(frag, tabbedPane.TabbedPaneFragment)) self.assertEqual(frag.jsClass, u'Mantissa.People.PluginTabbedPane') (tabNames, paneBodies) = zip(*frag.pages) self.assertEqual(list(tabNames), pluginNames) self.assertIdentical(paneBodies[0], personalization) self.assertEqual( paneBodies[1].attributes['secret'], 'test_pluginTabbbedPane') class ElementWrapperTestCase(unittest.TestCase): """ Tests for L{_ElementWrapper}. """ def test_element(self): """ L{_ElementWrapper}'s I{element} renderer should render the wrapped element. """ elem = Element() live = _ElementWrapper(elem) elementRenderer = renderer.get(live, 'element', None) self.assertIdentical(elementRenderer(None, None), elem) class SimpleReadOnlyViewTestCase(unittest.TestCase): """ Tests for L{SimpleReadOnlyView}. """ def test_attributeName(self): """ L{SimpleReadOnlyView}'s C{attributeName} renderer should return the correct value. """ view = SimpleReadOnlyView(Person.name, Person()) attributeNameRenderer = renderer.get(view, 'attributeName') self.assertEqual( attributeNameRenderer(None, None), nameToLabel('Person')) def test_attributeValue(self): """ L{SimpleReadOnlyView}'s C[attributeValue} renderer should return the correct value. """ name = u'test_attributeValue' view = SimpleReadOnlyView(Person.name, Person(name=name)) attributeValueRenderer = renderer.get(view, 'attributeValue') self.assertEqual(attributeValueRenderer(None, None), name) PK,eFoA xmantissa/test/test_publicweb.py# Copyright 2008 Divmod, Inc. See LICENSE file for details from zope.interface import implements from twisted.trial.unittest import TestCase from twisted.trial.util import suppress as SUPPRESS from twisted.python.usage import UsageError from twisted.python.components import registerAdapter from axiom.store import Store from axiom.item import Item from axiom.substore import SubStore from axiom.userbase import LoginSystem from axiom.attributes import boolean, integer, inmemory from axiom.plugins.axiom_plugins import Create from axiom.plugins.mantissacmd import Mantissa from axiom.plugins.offeringcmd import SetFrontPage from axiom.dependency import installOn from axiom.test.util import CommandStubMixin from nevow import rend, context, inevow from nevow.inevow import IResource from nevow.page import Element from nevow.rend import NotFound from nevow.flat import flatten from nevow.tags import title, div, span, h1, h2 from nevow.testutil import FakeRequest from xmantissa.ixmantissa import ( IPublicPage, ITemplateNameResolver, INavigableElement, ISiteURLGenerator, IOfferingTechnician, INavigableFragment, ISiteRootPlugin, IWebViewer) from xmantissa import signup from xmantissa.website import APIKey, WebSite from xmantissa.webapp import (PrivateApplication, _AuthenticatedWebViewer) from xmantissa.prefs import PreferenceAggregator from xmantissa.port import SSLPort from xmantissa.webnav import Tab from xmantissa.offering import Offering, InstalledOffering, installOffering from xmantissa.webtheme import theThemeCache from xmantissa.sharing import shareItem, getEveryoneRole from xmantissa.websharing import getDefaultShareID, UserIndexPage from xmantissa.publicweb import ( _AnonymousWebViewer, FrontPage, PublicAthenaLivePage, PublicNavAthenaLivePage, _PublicFrontPage, getLoader, AnonymousSite, _OfferingsFragment, _CustomizingResource, PublicPage, LoginPage) from xmantissa.signup import PasswordResetResource from xmantissa.test.test_offering import FakeOfferingTechnician from xmantissa.test.test_websharing import TestAppPowerup, ITest from xmantissa.test.test_webshell import WebViewerTestMixin from xmantissa.test.test_website import SiteTestsMixin class TestAppElement(Element): """ View class for TestAppPowerup. """ docFactory = object() # masquerade as a valid Element, for the # purposes of theme lookup. def __init__(self, original): self.original = original Element.__init__(self) registerAdapter(TestAppElement, ITest, INavigableFragment) class FakeTheme(object): """ Trivial implementation of L{ITemplateNameResolver} which returns document factories from an in-memory dictionary. @ivar docFactories: C{dict} mapping fragment names to document factory objects. """ def __init__(self, docFactories): self.docFactories = docFactories def getDocFactory(self, fragmentName, default=None): """ Return the document factory for the given name, or the default value if the given name is unknown. """ return self.docFactories.get(fragmentName, default) class AnonymousWebViewerTests(WebViewerTestMixin, TestCase): """ Tests for L{_AnonymousWebViewer}. """ def setupPageFactory(self): """ Create the page factory used by the tests. """ self.pageFactory = _AnonymousWebViewer(self.siteStore) def test_roleIn(self): """ L{_AnonymousWebViewer} should provide the Everyone role in the store it is asked about. """ theRole = getEveryoneRole(self.adminStore) self.assertIdentical( self.pageFactory.roleIn(self.adminStore), theRole) class FakeNavigableElement(Item): """ Navigation contributing powerup tests can use to verify the behavior of the navigation renderers. """ powerupInterfaces = (INavigableElement,) implements(*powerupInterfaces) dummy = integer() tabs = inmemory( doc=""" The object which will be returned by L{getTabs}. """) def getTabs(self): """ Return whatever tabs object has been set. """ return self.tabs class FakeTemplateNameResolver(object): """ Template name resolver which knows about one template. @ivar correctName: The name of the template this resolver knows about. @ivar correctFactory: The template which will be returned for C{correctName}. """ implements(ITemplateNameResolver) def __init__(self, correctName, correctFactory): self.correctName = correctName self.correctFactory = correctFactory def getDocFactory(self, name, default=None): """ Return the default for all names other than C{self.correctName}. Return C{self.correctFactory} for that. """ if name == self.correctName: return self.correctFactory return default class FakePublicItem(Item): """ Some item that is to be shared on an app store. """ dummy = integer() class FakeApplication(Item): """ Fake implementation of an application installed by an offering. """ implements(IPublicPage) index = boolean(doc=""" Flag indicating whether this application wants to be included on the front page. """) class TestHonorInstalledThemes(TestCase): """ Various classes should be using template resolvers to determine which theme to use based on a site store. """ def setUp(self): self.correctDocumentFactory = object() self.store = Store() self.fakeResolver = FakeTemplateNameResolver( None, self.correctDocumentFactory) def fakeConform(interface): if interface is ITemplateNameResolver: return self.fakeResolver return None self.store.__conform__ = fakeConform def test_offeringsFragmentLoader(self): """ L{_OfferingsFragment.docFactory} is the I{front-page} template loaded from the store's ITemplateNameResolver. """ self.fakeResolver.correctName = 'front-page' frontPage = FrontPage(store=self.store) offeringsFragment = _OfferingsFragment(frontPage) self.assertIdentical( offeringsFragment.docFactory, self.correctDocumentFactory) def test_loginPageLoader(self): """ L{LoginPage.fragment} is the I{login} template loaded from the store's ITemplateNameResolver. """ self.fakeResolver.correctName = 'login' page = LoginPage(self.store) self.assertIdentical( page.fragment, self.correctDocumentFactory) def test_passwordResetLoader(self): """ L{PasswordResetResource.fragment} is the I{login} template loaded from the store's ITemplateNameResolver. """ self.fakeResolver.correctName = 'reset' resetPage = PasswordResetResource(self.store) self.assertIdentical( resetPage.fragment, self.correctDocumentFactory) class OfferingsFragmentTestCase(TestCase): """ Tests for L{_OfferingsFragment}. """ def setUp(self): """ Set up stores and an offering. """ store = Store(dbdir=self.mktemp()) appStore1 = SubStore.createNew(store, ("app", "test1.axiom")) appStore2 = SubStore.createNew(store, ("app", "test2.axiom")) self.firstOffering = Offering(u'first offering', None, None, None, None, None, []) firstInstalledOffering = InstalledOffering( store=store, application=appStore1, offeringName=self.firstOffering.name) ss1 = appStore1.open() self.installApp(ss1) # (bypass Item.__setattr__) object.__setattr__( firstInstalledOffering, 'getOffering', lambda: self.firstOffering) secondOffering = Offering(u'second offering', None, None, None, None, None, []) secondInstalledOffering = InstalledOffering( store=store, application=appStore2, offeringName=secondOffering.name) # (bypass Item.__setattr__) object.__setattr__(secondInstalledOffering, 'getOffering', lambda: secondOffering) self.fragment = _OfferingsFragment(FrontPage(store=store)) def installApp(self, ss1): """ Create a public item and share it as the default. """ fpi = FakePublicItem(store=ss1) shareItem(fpi, toRole=getEveryoneRole(ss1), shareID=getDefaultShareID(ss1)) def test_offerings(self): """ L{_OfferingsFragment.data_offerings} returns a generator of C{dict} mapping C{'name'} to the name of an installed offering with a shared item that requests a link on the default public page. """ self.assertEqual( list(self.fragment.data_offerings(None, None)), [{'name': self.firstOffering.name}]) class OldOfferingsFragmentTestCase(OfferingsFragmentTestCase): """ Test for deprecated behaviour of L{_OfferingsFragment}. """ def installApp(self, ss1): """ Install an L{IPublicPage} powerup. """ fa = FakeApplication(store=ss1, index=True) ss1.powerUp(fa, IPublicPage) def test_offerings(self): """ Test the deprecated case for rendering the offering list. """ self.assertWarns( DeprecationWarning, "Use the sharing system to provide public pages, not IPublicPage", __file__, OfferingsFragmentTestCase.test_offerings, self) class PublicFrontPageTests(TestCase, CommandStubMixin): """ Tests for Mantissa's top-level web resource. """ def setUp(self): """ Set up a store with an installed offering. """ self.siteStore = Store(dbdir=self.mktemp()) Mantissa().installSite(self.siteStore, u"localhost", u"", False) off = Offering( name=u'test_offering', description=u'Offering for creating a sample app store', siteRequirements=[], appPowerups=[TestAppPowerup], installablePowerups=[], loginInterfaces=[], themes=[], ) self.installedOffering = installOffering(self.siteStore, off, None) self.app = self.installedOffering.application self.substore = self.app.open() sharedItem = getEveryoneRole(self.substore).getShare( getDefaultShareID(self.substore)) self.frontPage = self.siteStore.findUnique(FrontPage) self.webViewer = IWebViewer(self.siteStore) def test_offeringChild(self): """ Installing an offering makes its shared items accessible under a child of L{_PublicFrontPage} with the offering's name. """ frontPage = FrontPage(store=self.siteStore) resource = _PublicFrontPage(frontPage, self.webViewer) request = FakeRequest() result, segments = resource.locateChild(request, ('test_offering',)) self.assertIdentical(result.userStore, self.substore) self.assertTrue(IWebViewer.providedBy(result.webViewer)) def test_nonExistentChild(self): """ L{_PublicFrontPage.locateChild} returns L{rend.NotFound} for a child segment which does not exist. """ store = Store() frontPage = FrontPage(store=store) resource = _PublicFrontPage(frontPage, IWebViewer(self.siteStore)) request = FakeRequest() ctx = context.WebContext() ctx.remember(request, inevow.IRequest) result = resource.locateChild(ctx, ('foo',)) self.assertIdentical(result, rend.NotFound) def test_rootChild(self): """ When no default offering has been selected, L{PublicFrontPage.locateChild} returns an L{_OfferingsFragment} wrapped by the L{IWebViewer}. """ frontPage = FrontPage(store=self.siteStore) resource = _PublicFrontPage(frontPage, self.webViewer) request = FakeRequest() ctx = context.WebContext() ctx.remember(request, inevow.IRequest) result, segments = resource.locateChild(ctx, ('',)) self.assertIsInstance(result, PublicPage) self.assertIsInstance(result.fragment, _OfferingsFragment) def test_rootChildWithDefaultApp(self): """ The root resource provided by L{_PublicFrontPage} when a primary application has been selected is that application's L{SharingIndex}. """ resource, segments = self.frontPage.produceResource( None, ('',), IWebViewer(self.siteStore)) self.assertEqual(segments, ('',)) self.frontPage.defaultApplication = self.app result, segments = resource.locateChild(None, ('',)) self.assertIsInstance(result, PublicPage) self.assertIsInstance(result.fragment, TestAppElement) def getStore(self): return self.siteStore def test_switchFrontPage(self): """ 'axiomatic frontpage ' switches the primary application (i.e., the one whose front page will be displayed on the site's root resource) to the one belonging to the named offering. """ off2 = Offering( name=u'test_offering2', description=u'Offering for creating a sample app store', siteRequirements=[], appPowerups=[TestAppPowerup], installablePowerups=[], loginInterfaces=[], themes=[]) installedOffering2 = installOffering(self.siteStore, off2, None) sfp = SetFrontPage() sfp.parent = self sfp.parseOptions(["test_offering"]) resource, segments = self.frontPage.produceResource( None, ('',), self.webViewer) result, segs = resource.locateChild(None, ['']) self.assertIdentical(result.fragment.original.store, self.substore) self.assertEqual(segs, []) sfp.parseOptions(["test_offering2"]) resource, moreSegs = self.frontPage.produceResource(None, ('',), self.webViewer) result, segs = resource.locateChild(None, ['']) self.assertEqual(segs, []) self.assertIdentical(result.fragment.original.store, installedOffering2.application.open()) self.assertRaises(UsageError, sfp.parseOptions, []) self.assertRaises(UsageError, sfp.parseOptions, ["nonexistent"]) class AuthenticatedNavigationTestMixin: """ Mixin defining test methods for the authenticated navigation view. @ivar siteStore: The site Store with which the page returned by L{createPage} will be associated (probably by way of a user store and an item). """ userinfo = (u'testuser', u'example.com') username = u'@'.join(userinfo) def createPage(self): """ Create a subclass of L{_PublicPageMixin} to be used by tests. """ raise NotImplementedError("%r did not implement createPage" % (self,)) def rootURL(self, request): """ Return the root URL for the website associated with the page returned by L{createPage}. """ raise NotImplementedError("%r did not implement rootURL" % (self,)) def test_headRenderer(self): """ The I{head} renderer gets the head section for each installed theme by calling their C{head} method with the request being rendered and adds the result to the tag it is passed. """ headCalls = [] customHead = object() class CustomHeadTheme(object): priority = 0 themeName = "custom" def head(self, request, site): headCalls.append((request, site)) return customHead def getDocFactory(self, name, default): return default tech = FakeOfferingTechnician() tech.installOffering(Offering( name=u'fake', description=u'', siteRequirements=[], appPowerups=[], installablePowerups=[], loginInterfaces=[], themes=[CustomHeadTheme()])) self.siteStore.inMemoryPowerUp(tech, IOfferingTechnician) # Flush the cache, which is global, else the above fake-out is # completely ignored. theThemeCache.emptyCache() page = self.createPage(self.username) tag = div() ctx = context.WebContext(tag=tag) request = FakeRequest() ctx.remember(request, inevow.IRequest) result = page.render_head(ctx, None) self.assertEqual(result.tagName, 'div') self.assertEqual(result.attributes, {}) self.assertIn(customHead, result.children) self.assertEqual( headCalls, [(request, ISiteURLGenerator(self.siteStore))]) def test_authenticatedAuthenticateLinks(self): """ The I{authenticateLinks} renderer should remove the tag it is passed from the output if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ page = self.createPage(self.username) authenticateLinksPattern = div() ctx = context.WebContext(tag=authenticateLinksPattern) tag = page.render_authenticateLinks(ctx, None) self.assertEqual(tag, '') def test_authenticatedStartmenu(self): """ The I{startmenu} renderer should add navigation elements to the tag it is passed if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ navigable = FakeNavigableElement(store=self.userStore) installOn(navigable, self.userStore) navigable.tabs = [Tab('foo', 123, 0, [Tab('bar', 432, 0)])] page = self.createPage(self.username) startMenuTag = div[ h1(pattern='tab'), h2(pattern='subtabs')] ctx = context.WebContext(tag=startMenuTag) tag = page.render_startmenu(ctx, None) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) children = [child for child in tag.children if child.pattern is None] self.assertEqual(len(children), 0) # This structure seems overly complex. tabs = list(tag.slotData.pop('tabs')) self.assertEqual(len(tabs), 1) fooTab = tabs[0] self.assertEqual(fooTab.tagName, 'h1') self.assertEqual(fooTab.attributes, {}) self.assertEqual(fooTab.children, []) self.assertEqual(fooTab.slotData['href'], self.privateApp.linkTo(123)) self.assertEqual(fooTab.slotData['name'], 'foo') self.assertEqual(fooTab.slotData['kids'].tagName, 'h2') subtabs = list(fooTab.slotData['kids'].slotData['kids']) self.assertEqual(len(subtabs), 1) barTab = subtabs[0] self.assertEqual(barTab.tagName, 'h1') self.assertEqual(barTab.attributes, {}) self.assertEqual(barTab.children, []) self.assertEqual(barTab.slotData['href'], self.privateApp.linkTo(432)) self.assertEqual(barTab.slotData['name'], 'bar') self.assertEqual(barTab.slotData['kids'], '') def test_authenticatedSettingsLink(self): """ The I{settingsLink} renderer should add the URL of the settings item to the tag it is passed if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ page = self.createPage(self.username) settingsLinkPattern = div() ctx = context.WebContext(tag=settingsLinkPattern) tag = page.render_settingsLink(ctx, None) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) self.assertEqual( tag.children, [self.privateApp.linkTo( self.userStore.findUnique(PreferenceAggregator).storeID)]) def test_authenticatedLogout(self): """ The I{logout} renderer should return the tag it is passed if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ page = self.createPage(self.username) logoutPattern = div() ctx = context.WebContext(tag=logoutPattern) tag = page.render_logout(ctx, None) self.assertIdentical(logoutPattern, tag) def test_authenticatedApplicationNavigation(self): """ The I{applicationNavigation} renderer should add primary navigation elements to the tag it is passed if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ navigable = FakeNavigableElement(store=self.userStore) installOn(navigable, self.userStore) navigable.tabs = [Tab('foo', 123, 0, [Tab('bar', 432, 0)])] request = FakeRequest() page = self.createPage(self.username) navigationPattern = div[ span(id='app-tab', pattern='app-tab'), span(id='tab-contents', pattern='tab-contents')] ctx = context.WebContext(tag=navigationPattern) ctx.remember(request) tag = page.render_applicationNavigation(ctx, None) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) children = [child for child in tag.children if child.pattern is None] self.assertEqual(children, []) self.assertEqual(len(tag.slotData['tabs']), 1) fooTab = tag.slotData['tabs'][0] self.assertEqual(fooTab.attributes, {'id': 'app-tab'}) self.assertEqual(fooTab.slotData['name'], 'foo') fooContent = fooTab.slotData['tab-contents'] self.assertEqual(fooContent.attributes, {'id': 'tab-contents'}) self.assertEqual( fooContent.slotData['href'], self.privateApp.linkTo(123)) def test_title(self): """ The I{title} renderer should add the wrapped fragment's title attribute, if any, or the default "Divmod". """ page = self.createPage(self.username) titleTag = title() tag = page.render_title(context.WebContext(tag=titleTag), None) self.assertIdentical(tag, titleTag) flattened = flatten(tag) self.assertSubstring(flatten(getattr(page.fragment, 'title', 'Divmod')), flattened) def test_rootURL(self): """ The I{base} renderer should add the website's root URL to the tag it is passed. """ page = self.createPage(self.username) baseTag = div() request = FakeRequest(headers={'host': 'example.com'}) ctx = context.WebContext(tag=baseTag) ctx.remember(request, inevow.IRequest) tag = page.render_rootURL(ctx, None) self.assertIdentical(tag, baseTag) self.assertEqual(tag.attributes, {}) self.assertEqual(tag.children, [self.rootURL(request)]) def test_noUsername(self): """ The I{username} renderer should remove its node from the output when presented with a None username. """ page = self.createPage(None) result = page.render_username(None, None) self.assertEqual(result, "") def test_noUrchin(self): """ When there's no Urchin API key installed, the I{urchin} renderer should remove its node from the output. """ page = self.createPage(None) result = page.render_urchin(None, None) self.assertEqual(result, "") def test_urchin(self): """ When an Urchin API key is present, the code for enabling Google Analytics tracking should be inserted into the shell template. """ keyString = u"UA-99018-11" APIKey.setKeyForAPI(self.siteStore, APIKey.URCHIN, keyString) page = self.createPage(None) t = div() result = page.render_urchin(context.WebContext(tag=t), None) self.assertEqual(result.slotData['urchin-key'], keyString) def usernameRenderingTest(self, username, hostHeader, expectedUsername): """ Verify that the username will be rendered appropriately given the host of the HTTP request. @param username: the user's full login identifier. @param hostHeader: the value of the 'host' header. @param expectedUsername: the expected value of the rendered username. """ page = self.createPage(username) userTag = span() req = FakeRequest(headers={'host': hostHeader}) ctx = context.WebContext(tag=userTag) ctx.remember(req, inevow.IRequest) tag = page.render_username(ctx, None) self.assertEqual(tag.tagName, 'span') self.assertEqual(tag.children, [expectedUsername]) def test_localUsername(self): """ The I{username} renderer should render just the username when the username domain is the same as the HTTP request's domain. otherwise it should render the full username complete with domain. """ domainUser = self.username.split('@')[0] return self.usernameRenderingTest( self.username, 'example.com', domainUser) def test_remoteUsername(self): """ The I{username} renderer should render username with the domain when the username domain is different than the HTTP request's domain. """ return self.usernameRenderingTest( self.username, 'not-example.com', self.username) def test_usernameWithHostPort(self): """ The I{username} renderer should respect ports in the host headers. """ domainUser = self.username.split('@')[0] return self.usernameRenderingTest( self.username, 'example.com:8080', domainUser) def test_prefixedDomainUsername(self): """ The I{username} renderer should render just the username in the case where you are viewing a subdomain as well; if bob is viewing 'jethro.divmod.com' or 'www.divmod.com', he should still see the username 'bob'. """ domainUser = self.username.split('@')[0] return self.usernameRenderingTest( self.username, 'www.example.com', domainUser) class _PublicAthenaLivePageTestMixin(AuthenticatedNavigationTestMixin): """ Mixin which defines test methods which exercise functionality provided by the various L{xmantissa.publicweb._PublicPageMixin} subclasses, like L{PublicAthenaLivePage} and L{PublicNavAthenaLivePage}. """ signupURL = u'sign/up' signupPrompt = u'sign up now' def setUp(self): self.siteStore = Store(filesdir=self.mktemp()) def siteStoreTxn(): Mantissa().installSite( self.siteStore, self.userinfo[1], u"", False) ticketed = signup.FreeTicketSignup( store=self.siteStore, prefixURL=self.signupURL, prompt=self.signupPrompt) signup._SignupTracker(store=self.siteStore, signupItem=ticketed) return Create().addAccount( self.siteStore, self.userinfo[0], self.userinfo[1], u'password').avatars.open() self.userStore = self.siteStore.transact(siteStoreTxn) def userStoreTxn(): self.privateApp = PrivateApplication(store=self.userStore) installOn(self.privateApp, self.userStore) self.userStore.transact(userStoreTxn) def rootURL(self, request): """ Return the root URL as reported by C{self.website}. """ return ISiteURLGenerator(self.siteStore).rootURL(request) def test_unauthenticatedAuthenticateLinks(self): """ The I{authenticateLinks} renderer should add login and signup links to the tag it is passed, if it is called on a L{_PublicPageMixin} being rendered for an unauthenticated user. """ page = self.createPage(None) authenticateLinksPattern = div[span(pattern='signup-link')] ctx = context.WebContext(tag=authenticateLinksPattern) tag = page.render_authenticateLinks(ctx, None) self.assertEqual(tag.tagName, 'div') self.assertEqual(tag.attributes, {}) children = [child for child in tag.children if child.pattern is None] self.assertEqual(len(children), 1) self.assertEqual( children[0].slotData, {'prompt': self.signupPrompt, 'url': '/' + self.signupURL}) def test_unauthenticatedStartmenu(self): """ The I{startmenu} renderer should remove the tag it is passed from the output if it is called on a L{_PublicPageMixin} being rendered for an unauthenticated user. """ page = self.createPage(None) startMenuTag = div() ctx = context.WebContext(tag=startMenuTag) tag = page.render_startmenu(ctx, None) self.assertEqual(tag, '') def test_unauthenticatedSettingsLink(self): """ The I{settingsLink} renderer should remove the tag it is passed from the output if it is called on a L{_PublicPageMixin} being rendered for an unauthenticated user. """ page = self.createPage(None) settingsLinkPattern = div() ctx = context.WebContext(tag=settingsLinkPattern) tag = page.render_settingsLink(ctx, None) self.assertEqual(tag, '') def test_unauthenticatedLogout(self): """ The I{logout} renderer should remove the tag it is passed from the output if it is called on a L{_PublicPageMixin} being rendered for an authenticated user. """ page = self.createPage(None) logoutPattern = div() ctx = context.WebContext(tag=logoutPattern) tag = page.render_logout(ctx, None) self.assertEqual(tag, '') def test_unauthenticatedApplicationNavigation(self): """ The I{applicationNavigation} renderer should remove the tag it is passed from the output if it is called on a L{_PublicPageMixin} being rendered for an unauthenticated user. """ page = self.createPage(None) navigationPattern = div() ctx = context.WebContext(tag=navigationPattern) tag = page.render_applicationNavigation(ctx, None) self.assertEqual(tag, '') class TestFragment(rend.Fragment): title = u'a test fragment' class PublicAthenaLivePageTestCase(_PublicAthenaLivePageTestMixin, TestCase): """ Tests for L{PublicAthenaLivePage}. """ def createPage(self, forUser): return PublicAthenaLivePage( self.siteStore, TestFragment(), forUser=forUser) class PublicNavAthenaLivePageTestCase(_PublicAthenaLivePageTestMixin, TestCase): """ Tests for L{PublicNavAthenaLivePage}. """ suppress = [SUPPRESS(category=DeprecationWarning)] def createPage(self, forUser): return PublicNavAthenaLivePage( self.siteStore, TestFragment(), forUser=forUser) class GetLoaderTests(TestCase): """ Tests for L{xmantissa.publicweb.getLoader}. """ def test_deprecated(self): """ Calling L{getLoader} emits a deprecation warning. """ self.assertWarns( DeprecationWarning, "xmantissa.publicweb.getLoader is deprecated, use " "PrivateApplication.getDocFactory or SiteTemplateResolver." "getDocFactory.", __file__, lambda: getLoader("shell")) class CustomizedPublicPageTests(TestCase): """ Tests for L{CustomizedPublicPage}. Let's say you've got a normal Mantissa database. There's an L{AnonymousSite} in the site store, powering it up for L{IResource}. There's a user, that has a user store, which has a L{WebSite} as their L{IResource} avatar, plus a L{PrivateApplication} and a L{CustomizedPublicPage} as L{ISiteRootPlugin}s. L{CustomizedPublicPage}'s purpose is to make sure that when the user views the public site, their L{IWebViewer} is propagated to children of the global L{AnonymousSite}. """ def setUp(self): """ Create a store as described in the test case docstring. """ site = Store(self.mktemp()) Mantissa().installSite(site, u"example.com", u"", False) Mantissa().installAdmin(site, u'admin', u'example.com', u'asdf') anonsite = site.findUnique(AnonymousSite) user = site.findUnique(LoginSystem).accountByAddress( u"admin",u"example.com").avatars.open() self.website = user.findUnique(WebSite) self.privapp = user.findUnique(PrivateApplication) self.site = site def test_propagateNavigationToSlashUsers(self): """ When the 'users' child is requested through a CustomizedPublicPage, L{AnonymousSite.rootChild_users} method should be invoked to produce a L{UserIndexPage} for the given user's L{PrivateApplication}. """ wrapper, resultSegs = self.website.locateChild( FakeRequest(headers={"host": "example.com"}), ('users',)) self.assertIsInstance(wrapper, _CustomizingResource) self.assertIsInstance(wrapper.currentResource, UserIndexPage) self.assertIsInstance(wrapper.currentResource.webViewer, _AuthenticatedWebViewer) self.assertIdentical(wrapper.currentResource.webViewer._privateApplication, self.privapp) def test_propagateNavigationToPlugins(self): """ The site store has an L{ISiteRootPlugin} which provides some other application-defined resource - we'll call that 'AppSitePlugin'. L{CustomizedPublicPage}'s whole purpose is to make sure that when 'AppSitePlugin' wants to return a resource, that resource is appropriately decorated so that its shell template will appear appropriate to the logged-in user. In order to do that, 'AppSitePlugin' must receive as its C{webViewer} argument the same one that L{CustomizedPublicPage} does. """ # Stock configuration now set up, let's introduce a site plugin... result = object() calledWith = [] class AppSitePlugin(object): implements(ISiteRootPlugin) def produceResource(self, request, segments, webViewer): calledWith.append([request, segments, webViewer]) return result, () self.site.inMemoryPowerUp(AppSitePlugin(), ISiteRootPlugin) req = FakeRequest(headers={"host": "example.com"}) wrapper, resultSegs = self.website.locateChild(req, ("foo", "bar")) [(inreq, segs, webViewer)] = calledWith self.assertIdentical(inreq, req) self.assertEqual(segs, ('foo', 'bar')) self.assertIsInstance(webViewer, _AuthenticatedWebViewer) self.assertEqual(webViewer._privateApplication, self.privapp) self.assertIsInstance(wrapper, _CustomizingResource) self.assertIdentical(wrapper.currentResource, result) self.assertEqual(resultSegs, ()) class AnonymousSiteTests(SiteTestsMixin, TestCase): """ Tests for L{AnonymousSite}. """ def setUp(self): """ Set up a store with a valid offering to test against. """ SiteTestsMixin.setUp(self) self.store = self.siteStore self.site = ISiteURLGenerator(self.store) self.resource = IResource(self.store) def test_powersUpWebViewer(self): """ L{AnonymousSite} provides an indirected L{IWebViewer} powerup, and its indirected powerup should be the default provider of that interface. """ webViewer = IWebViewer(self.store) self.assertIsInstance(webViewer, _AnonymousWebViewer) self.assertIdentical(webViewer._siteStore, self.store) def test_login(self): """ L{AnonymousSite} has a I{login} child which returns a L{LoginPage} instance. """ host = 'example.org' port = 1234 netloc = '%s:%d' % (host, port) request = FakeRequest( headers={'host': netloc}, uri='/login/foo', currentSegments=[], isSecure=False) self.site.hostname = host.decode('ascii') SSLPort(store=self.store, portNumber=port, factory=self.site) resource, segments = self.resource.locateChild(request, ("login",)) self.assertTrue(isinstance(resource, LoginPage)) self.assertIdentical(resource.store, self.store) self.assertEqual(resource.segments, ()) self.assertEqual(resource.arguments, {}) self.assertEqual(segments, ()) def test_resetPassword(self): """ L{AnonymousSite} has a I{resetPassword} child which returns a L{PasswordResetResource} instance. """ resource, segments = self.resource.locateChild( FakeRequest(headers={"host": "example.com"}), ("resetPassword",)) self.assertTrue(isinstance(resource, PasswordResetResource)) self.assertIdentical(resource.store, self.store) self.assertEqual(segments, ()) def test_users(self): """ L{AnonymousSite} has a I{users} child which returns a L{UserIndexPage} instance. """ resource, segments = self.resource.locateChild( FakeRequest(headers={"host": "example.com"}), ("users",)) self.assertTrue(isinstance(resource, UserIndexPage)) self.assertIdentical( resource.loginSystem, self.store.findUnique(LoginSystem)) self.assertEqual(segments, ()) def test_notFound(self): """ L{AnonymousSite.locateChild} returns L{NotFound} for requests it cannot find another response for. """ result = self.resource.locateChild( FakeRequest(headers={"host": "example.com"}), ("foo", "bar")) self.assertIdentical(result, NotFound) PKCmF Nhxmantissa/test/test_stats.py# Copyright 2008 Divmod, Inc. See LICENSE for details. """ Tests for L{xmantissa.stats}. """ from twisted.python import log, failure from twisted.trial import unittest from twisted.protocols.amp import ASK, COMMAND, Command, parseString from axiom.iaxiom import IStatEvent from xmantissa.stats import RemoteStatsCollectorFactory, RemoteStatsCollector from xmantissa.test.test_ampserver import ( BoxReceiverFactoryPowerupTestMixin, CollectingSender) class RemoteStatsCollectorTest(BoxReceiverFactoryPowerupTestMixin, unittest.TestCase): """ Tests for L{RemoteStatsCollectorFactory} and L{RemoteStatsCollector}. """ factoryClass = RemoteStatsCollectorFactory protocolClass = RemoteStatsCollector def setUp(self): """ Create and start a L{RemoteStatsCollector}. """ self.receiver = RemoteStatsCollectorFactory().getBoxReceiver() self.sender = CollectingSender() self.receiver.startReceivingBoxes(self.sender) def tearDown(self): """ Ensure the log observer added by L{setUp} is removed. """ try: log.removeObserver(self.receiver._emit) except ValueError: # The test removed it already. pass def test_deliverStatEvents(self): """ When a L{RemoteStatsCollector} is active, it sends AMP boxes to its client when L{IStatEvent}s are logged. """ log.msg(interface=IStatEvent, foo="bar", baz=12, quux=u'\N{SNOWMAN}') self.assertEqual(len(self.sender.boxes), 1) stat = self.sender.boxes[0] self.assertNotIn(ASK, stat) self.assertEqual(stat[COMMAND], 'StatUpdate') # Skip testing the timestamp, another test which can control its value # will do that. data = set([ (d['key'], d['value']) for d in parseString(stat['data']) if d['key'] != 'time']) self.assertIn(('foo', 'bar'), data) self.assertIn(('baz', '12'), data) self.assertIn(('quux', u'\N{SNOWMAN}'.encode('utf-8')), data) def test_ignoreOtherEvents(self): """ L{RemoteStatsCollection} does not send any boxes for events which don't have L{IStatEvent} as the value for their C{'interface'} key. """ log.msg(interface="test log event") self.assertEqual(self.sender.boxes, []) def test_deliveryStopsAfterDisconnect(self): """ After L{RemoteStatsCollection.stopReceivingBoxes} is called, it no longer observes L{IStatEvent} log messages. """ self.receiver.stopReceivingBoxes( failure.Failure(Exception("test exception"))) log.msg(interface=IStatEvent, foo="bar") self.assertEqual(self.sender.boxes, []) def test_disconnectFailsOutgoing(self): """ L{RemoteStatsCollection.stopReceivingBoxes} causes the L{Deferred} associated with any outstanding command to fail with the reason given. """ class DummyCommand(Command): pass d = self.receiver.callRemote(DummyCommand) self.receiver.stopReceivingBoxes(RuntimeError("test exception")) return self.assertFailure(d, RuntimeError) def test_timestamp(self): """ L{RemoteStatsCollector._emit} sends a I{StatUpdate} command with a timestamp taken from the log event passed to it. """ self.receiver._emit({ 'time': 123456789.0, 'interface': IStatEvent}) self.assertEqual(len(self.sender.boxes), 1) timestamp = [ d['value'] for d in parseString(self.sender.boxes[0]['data']) if d['key'] == 'time'] self.assertEqual(timestamp, ['123456789.0']) def test_athena(self): """ L{RemoteStatsCollector._emit} sends a I{StatUpdate} command with Athena transport data for Athena message send and receive events. """ self.receiver._emit({ 'athena_send_messages': True, 'count': 17}) self.receiver._emit({ 'athena_received_messages': True, 'count': 12}) self.assertEqual(len(self.sender.boxes), 2) send = set([(d['key'], d['value']) for d in parseString(self.sender.boxes[0]['data'])]) self.assertEqual( send, set([('count', '17'), ('athena_send_messages', 'True')])) received = set([(d['key'], d['value']) for d in parseString(self.sender.boxes[1]['data'])]) self.assertEqual( received, set([('count', '12'), ('athena_received_messages', 'True')])) PKF %%xmantissa/test/test_port.py# Copyright (c) 2008 Divmod, Inc. # See LICENSE for details. """ Tests for L{xmantissa.port}. """ import sys import os from StringIO import StringIO from zope.interface import implements from zope.interface.verify import verifyObject from twisted.trial.unittest import TestCase from twisted.python.filepath import FilePath from twisted.application.service import IService, IServiceCollection from twisted.internet.protocol import ServerFactory from twisted.internet.defer import Deferred from twisted.internet.ssl import CertificateOptions from axiom.iaxiom import IAxiomaticCommand from axiom.store import Store from axiom.item import Item from axiom.attributes import inmemory, integer from axiom.dependency import installOn from axiom.scripts.axiomatic import Options as AxiomaticOptions from axiom.test.util import CommandStub from xmantissa.ixmantissa import IProtocolFactoryFactory from xmantissa.port import TCPPort, SSLPort, StringEndpointPort from xmantissa.port import PortConfiguration CERTIFICATE_DATA = """ -----BEGIN CERTIFICATE----- MIICmTCCAgICAQEwDQYJKoZIhvcNAQEEBQAwgZQxCzAJBgNVBAYTAlVTMRQwEgYD VQQDEwtleGFtcGxlLmNvbTERMA8GA1UEBxMITmV3IFlvcmsxEzARBgNVBAoTCkRp dm1vZCBMTEMxETAPBgNVBAgTCE5ldyBZb3JrMSIwIAYJKoZIhvcNAQkBFhNzdXBw b3J0QGV4YW1wbGUuY29tMRAwDgYDVQQLEwdUZXN0aW5nMB4XDTA2MTIzMDE5MDEx NloXDTA3MTIzMDE5MDExNlowgZQxCzAJBgNVBAYTAlVTMRQwEgYDVQQDEwtleGFt cGxlLmNvbTERMA8GA1UEBxMITmV3IFlvcmsxEzARBgNVBAoTCkRpdm1vZCBMTEMx ETAPBgNVBAgTCE5ldyBZb3JrMSIwIAYJKoZIhvcNAQkBFhNzdXBwb3J0QGV4YW1w bGUuY29tMRAwDgYDVQQLEwdUZXN0aW5nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQCrmNNyXLHAETcDH8Uxhmbo8IhFFMx1C4i7oTHTKsmD84E3YFj/RdByrWrG TL4XskALpfmw1+LxQmMO8n4sIsN3QmjkAWhFhMEquKv6NNN+sRo6vF+ytEasuYn/ 7gY/iT7LYqUmKWckBsPYzT9elyOXi6miI0tFdeyfXRSxOslKewIDAQABMA0GCSqG SIb3DQEBBAUAA4GBABotNizqPoGWIG5BMsl8lxseqiw/8AwvoiQNpYTrC8W+Umsg oZEaMuVkf/NDJEa3TXdYcAzkFwGN9Cn/WCgHEkLxIZ66aHV0bfcE7YJjHRDrrLiY chPndOGGrD3iTuWaGnauUcsjJ+RsxqHMBu6NRQYgkoYNsOr0UA1ek7O1vjMy -----END CERTIFICATE----- """ PRIVATEKEY_DATA = """ -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCrmNNyXLHAETcDH8Uxhmbo8IhFFMx1C4i7oTHTKsmD84E3YFj/ RdByrWrGTL4XskALpfmw1+LxQmMO8n4sIsN3QmjkAWhFhMEquKv6NNN+sRo6vF+y tEasuYn/7gY/iT7LYqUmKWckBsPYzT9elyOXi6miI0tFdeyfXRSxOslKewIDAQAB AoGAHd9YCBOs+gPFMO0J9iowpiKhhm0tfr7ISemw89MCC8+LUimatK3hsOURrn3T peppDd4SDsA2iMuG1SZP4r0Wi9ZncZ+uj6KfVHg6rJZRDW2cPsGNyBw2HO8pFxnh NsfxioutzCqJ9A0KwqSNQsBpOAlRWzP13+/W5wYAGK+yrLECQQDYgOhVR+1KOhty CI0NVITNFL5IOZ254Eu46qbEGwPNJvkzdp+Wx5gsfCip9aiZgw3LMEeGXu9P1C4N AqDM4uozAkEAyua0F0nCRLzjLAAw4odC+vA6jnq6K4M7QT6cQVwmrxgOj6jGEOGu eaoWbXi2bKcxOGBNDZW0PVKmpq4hZblmmQJBALwFP0AIxg+HZRxkRrMD6oz77cBl oQ+ytbAywH9ggq2gohzKcRAN6J8BeIMZn8EpqkoCdKtCOQyX1SJhXOpySjcCQDds mZka7tQz/KISU0gtxqAhav1sjNpB+Lez0J8R+wctPR0E70XBQBW/3mx84uf/K7TI qYOidx+hKiCxxDGzWVECQHNVutQ1ABjmv6EDJTo28QQsm5hNbfS+tVY3bSihNjLM Y+O7ib90LsqfQ8r0GUphQVi4EA4QMJqaF7ZxKms79qA= -----END RSA PRIVATE KEY----- """ class DummyPort(object): """ Stub class used to track what reactor listen calls have been made and what created ports have been stopped. """ stopping = None def __init__(self, portNumber, factory, contextFactory=None, interface=''): self.portNumber = portNumber self.factory = factory self.contextFactory = contextFactory self.interface = interface def stopListening(self): assert self.stopping is None self.stopping = Deferred() return self.stopping class DummyFactory(Item): """ Helper class used as a stand-in for a real protocol factory by the unit tests. """ implements(IProtocolFactoryFactory) dummyAttribute = integer(doc=""" Meaningless attribute which serves only to make this a valid Item subclass. """) realFactory = inmemory(doc=""" A reference to the protocol factory which L{getFactory} will return. """) def getFactory(self): return self.realFactory class PortTestsMixin: """ Test method-defining mixin class for port types with C{portNumber} and C{factory} attributes. Included are tests for various persistence-related behaviors as well as the L{IService} implementation which all ports should have. @ivar portType: The L{Item} subclass which will be tested. @ivar lowPortNumber: A port number which requires privileges to bind on POSIX. Used to test L{privilegedStartService}. @ivar highPortNumber: A port number which does not require privileges to bind on POSIX. Used to test the interaction between L{privilegedStartService} and L{startService}. @ivar dbdir: The path at which to create the test L{Store}. This must be bound before L{setUp} is run, since that is the only method which examines its value. @ivar ports: A list of ports which have been bound using L{listen}. created in L{setUp}. """ portType = None lowPortNumber = 123 highPortNumber = 1234 someInterface = u'127.0.0.1' def port(self, **kw): """ Create and return a new port instance with the given attribute values. """ return self.portType(**kw) def listen(self, *a, **kw): """ Pretend to bind a port. Used as a stub implementation of a reactor listen method. Subclasses should override and implement to append useful information to C{self.ports}. """ raise NotImplementedError() def checkPort(self, port, alternatePort=None): """ Assert that the given port has been properly created. @type port: L{DummyPort} @param port: A port which has been created by the code being tested. @type alternatePort: C{int} @param alternatePort: If not C{None}, the port number on which C{port} should be listening. """ raise NotImplementedError() def setUp(self): self.filesdir = self.mktemp() self.store = Store(filesdir=self.filesdir) self.realFactory = ServerFactory() self.factory = DummyFactory(store=self.store, realFactory=self.realFactory) self.ports = [] def test_portNumberAttribute(self): """ Test that C{self.portType} remembers the port number it is told to listen on. """ port = self.port(store=self.store, portNumber=self.lowPortNumber) self.assertEqual(port.portNumber, self.lowPortNumber) def test_interfaceAttribute(self): """ Test that C{self.portType} remembers the interface it is told to listen on. """ port = self.port(store=self.store, interface=self.someInterface) self.assertEqual(port.interface, self.someInterface) def test_factoryAttribute(self): """ Test that C{self.portType} remembers the factory it is given to associate with its port. """ port = self.port(store=self.store, factory=self.factory) self.assertIdentical(port.factory, self.factory) def test_service(self): """ Test that C{self.portType} becomes a service on the store it is installed on. """ port = self.port(store=self.store) installOn(port, self.store) self.assertEqual( list(self.store.powerupsFor(IService)), [port]) def test_setServiceParent(self): """ Test that the C{self.portType.setServiceParent} method adds the C{self.portType} to the Axiom Store Service as a child. """ port = self.port(store=self.store) port.setServiceParent(self.store) self.failUnlessIn(port, list(IService(self.store))) def test_disownServiceParent(self): """ Test that the C{self.portType.disownServiceParent} method removes the C{self.portType} from the Axiom Store Service. """ port = self.port(store=self.store) port.setServiceParent(self.store) port.disownServiceParent() self.failIfIn(port, list(IService(self.store))) def test_serviceParent(self): """ Test that C{self.portType} is a child of the store service after it is installed. """ port = self.port(store=self.store) installOn(port, self.store) service = IServiceCollection(self.store) self.failUnlessIn(port, list(service)) def _start(self, portNumber, methodName): port = self.port(store=self.store, portNumber=portNumber, factory=self.factory) port._listen = self.listen getattr(port, methodName)() return self.ports def _privilegedStartService(self, portNumber): return self._start(portNumber, 'privilegedStartService') def _startService(self, portNumber): return self._start(portNumber, 'startService') def test_startPrivilegedService(self): """ Test that C{self.portType} binds a low-numbered port with the reactor when it is started with privilege. """ ports = self._privilegedStartService(self.lowPortNumber) self.assertEqual(len(ports), 1) self.checkPort(ports[0]) def test_dontStartPrivilegedService(self): """ Test that C{self.portType} doesn't bind a high-numbered port with the reactor when it is started with privilege. """ ports = self._privilegedStartService(self.highPortNumber) self.assertEqual(ports, []) def test_startServiceLow(self): """ Test that C{self.portType} binds a low-numbered port with the reactor when it is started without privilege. """ ports = self._startService(self.lowPortNumber) self.assertEqual(len(ports), 1) self.checkPort(ports[0]) def test_startServiceHigh(self): """ Test that C{self.portType} binds a high-numbered port with the reactor when it is started without privilege. """ ports = self._startService(self.highPortNumber) self.assertEqual(len(ports), 1) self.checkPort(ports[0], self.highPortNumber) def test_startServiceNoInterface(self): """ Test that C{self.portType} binds to all interfaces if no interface is explicitly specified. """ port = self.port(store=self.store, portNumber=self.highPortNumber, factory=self.factory) port._listen = self.listen port.startService() self.assertEqual(self.ports[0].interface, '') def test_startServiceInterface(self): """ Test that C{self.portType} binds to only the specified interface when instructed to. """ port = self.port(store=self.store, portNumber=self.highPortNumber, factory=self.factory, interface=self.someInterface) port._listen = self.listen port.startService() self.assertEqual(self.ports[0].interface, self.someInterface) def test_startedOnce(self): """ Test that C{self.portType} only binds one network port when C{privilegedStartService} and C{startService} are both called. """ port = self.port(store=self.store, portNumber=self.lowPortNumber, factory=self.factory) port._listen = self.listen port.privilegedStartService() self.assertEqual(len(self.ports), 1) self.checkPort(self.ports[0]) port.startService() self.assertEqual(len(self.ports), 1) def test_stopService(self): """ Test that C{self.portType} cleans up its listening port when it is stopped. """ port = self.port(store=self.store, portNumber=self.lowPortNumber, factory=self.factory) port._listen = self.listen port.startService() stopped = port.stopService() stopping = self.ports[0].stopping self.failIfIdentical(stopping, None) self.assertIdentical(stopped, stopping) def test_deletedFactory(self): """ Test that the deletion of a C{self.portType}'s factory item results in the C{self.portType} being deleted. """ port = self.port(store=self.store, portNumber=self.lowPortNumber, factory=self.factory) self.factory.deleteFromStore() self.assertEqual(list(self.store.query(self.portType)), []) def test_deletionDisownsParent(self): """ Test that a deleted C{self.portType} no longer shows up in the children list of the service which used to be its parent. """ port = self.port(store=self.store, portNumber=self.lowPortNumber, factory=self.factory) port.setServiceParent(self.store) port.deleteFromStore() service = IServiceCollection(self.store) self.failIfIn(port, list(service)) class TCPPortTests(PortTestsMixin, TestCase): """ Tests for L{xmantissa.port.TCPPort}. """ portType = TCPPort def checkPort(self, port, alternatePort=None): if alternatePort is None: alternatePort = self.lowPortNumber self.assertEqual(port.portNumber, alternatePort) self.assertEqual(port.factory, self.realFactory) def listen(self, port, factory, interface=''): self.ports.append(DummyPort(port, factory, interface=interface)) return self.ports[-1] class SSLPortTests(PortTestsMixin, TestCase): """ Tests for L{xmantissa.port.SSLPort}. """ portType = SSLPort def checkPort(self, port, alternatePort=None): if alternatePort is None: alternatePort = self.lowPortNumber self.assertEqual(port.portNumber, alternatePort) self.assertEqual(port.factory, self.realFactory) self.failUnless(isinstance(port.contextFactory, CertificateOptions)) def port(self, certificatePath=None, **kw): if certificatePath is None: certificatePath = self.store.newFilePath('certificate.pem') assert not certificatePath.exists() certificatePath.setContent(CERTIFICATE_DATA + PRIVATEKEY_DATA) return self.portType(certificatePath=certificatePath, **kw) def listen(self, port, factory, contextFactory, interface=''): self.ports.append(DummyPort(port, factory, contextFactory, interface=interface)) return self.ports[-1] def test_certificatePathAttribute(self): """ Test that L{SSLPort} remembers the certificate filename it is given. """ certificatePath = self.store.newFilePath('foo', 'bar') port = self.port(store=self.store, certificatePath=certificatePath) self.assertEqual(port.certificatePath, certificatePath) class _FakeService(object): """ Fake L{twisted.application.service.IService} implementation for testing L{xmantissa.port.StringEndpointPort}'s wrapping behaviour. """ def __init__(self, description, factory): self.description = description self.factory = factory self.privilegedStarted = False self.started = False self.stopped = False def privilegedStartService(self): self.privilegedStarted = True def startService(self): self.started = True def stopService(self): self.stopped = True class StringEndpointPortTests(TestCase): """ Tests for L{xmantissa.port.StringEndpointPort}. """ def _fakeService(self, description, factory): """ A fake for L{twisted.application.strports.service} that just constructs our fake service object. """ self._service = _FakeService(description, factory) return self._service def port(self, **kw): port = StringEndpointPort(store=Store(), **kw) port._endpointService = self._fakeService return port def test_startService(self): """ The underlying endpoint service is started when L{xmantissa.port.StringEndpointPort} is started. """ port = self.port(description=u'foo') port.privilegedStartService() self.assertTrue(self._service.privilegedStarted) port.startService() self.assertTrue(self._service.started) def test_description(self): """ The underlying endpoint service is created with the description specified by the L{xmantissa.port.StringEndpointPort}. """ port = self.port(description=u'foo') port.startService() self.assertEqual(u'foo', self._service.description) def test_stopService(self): """ The underlying endpoint service is stopped when L{xmantissa.port.StringEndpointPort} is stopped. """ port = self.port(description=u'foo') port.startService() port.stopService() self.assertTrue(self._service.stopped) class PortConfigurationCommandTests(TestCase): """ Tests for the I{axiomatic port} command. """ def setUp(self): """ Override C{sys.stdout} to capture anything written by the port subcommand. """ self.oldColumns = os.environ.get('COLUMNS') os.environ['COLUMNS'] = '80' self.stdout = sys.stdout sys.stdout = StringIO() def tearDown(self): """ Restore the original value of C{sys.stdout}. """ sys.stdout = self.stdout if self.oldColumns is not None: os.environ['COLUMNS'] = self.oldColumns def _makeConfig(self, store): """ Create a L{PortConfiguration} instance with a properly set C{parent} attribute. """ config = PortConfiguration() config.parent = CommandStub(store, "port") return config def assertSuccessStatus(self, options, arguments): """ Parse the given arguments with the given options object and assert that L{SystemExit} is raised with an exit code of C{0}. """ self.assertFailStatus(0, options, arguments) def assertFailStatus(self, code, options, arguments): """ Parse the given arguments with the given options object and assert that L{SystemExit} is raised with the specified exit code. """ exc = self.assertRaises(SystemExit, options.parseOptions, arguments) self.assertEqual(exc.args, (code,)) def assertSpacelessEqual(self, first, second): """ Assert the equality of two strings without respect to their whitespace. """ self.assertEqual(' '.join(first.split()), ' '.join(second.split())) def test_providesCommandInterface(self): """ L{PortConfiguration} provides L{IAxiomaticCommand}. """ verifyObject(IAxiomaticCommand, PortConfiguration) def test_axiomaticSubcommand(self): """ L{PortConfiguration} is available as a subcommand of I{axiomatic}. """ subCommands = AxiomaticOptions().subCommands [options] = [cmd[2] for cmd in subCommands if cmd[0] == 'port'] self.assertIdentical(options, PortConfiguration) _portHelpText = ( # This is a bit unfortunate. It depends on what Options in Twisted # decides to spit out. Note particularly the seemingly random amount # of trailing whitespace included on some lines. The intent of tests # using this isn't really to ensure byte-identical results, but simply # to verify that help text is going to be shown to a user. -exarkun "Usage: axiomatic [options] port [options]\n" "Options:\n" " --version Display Twisted version and exit.\n" " --help Display this help and exit.\n" "\n" "This command allows for the inspection and modification of the " "configuration of\n" "network services in an Axiom store.\n" "Commands:\n" " list Show existing ports and factories.\n" " delete Delete existing ports.\n" " create Create new ports.\n" "\n") def test_implicitPortHelp(self): """ When I{axiomatic port} is invoked with no arguments, usage information is written to standard out and the process exits successfully. """ self.assertSuccessStatus(self._makeConfig(None), []) self.assertSpacelessEqual(self._portHelpText, sys.stdout.getvalue()) def test_explicitPortHelp(self): """ When I{axiomatic port} is invoked with I{--help}, usage information is written to standard out. """ self.assertSuccessStatus(self._makeConfig(None), ["--help"]) self.assertSpacelessEqual(self._portHelpText, sys.stdout.getvalue()) _listHelpText = ( "Usage: axiomatic [options] port [options] list [options]\n" "Options:\n" " --version Display Twisted version and exit.\n" " --help Display this help and exit.\n" "\n" "Show the port/factory bindings in an Axiom store.\n" "\n") def test_explicitListHelp(self): """ When I{axiomatic port list} is invoked with I{--help}, usage information for the C{list} subcommand is written to standard out. """ self.assertSuccessStatus(self._makeConfig(None), ["list", "--help"]) self.assertSpacelessEqual(self._listHelpText, sys.stdout.getvalue()) def test_listEmpty(self): """ When I{axiomatic port list} is invoked, the ports which are currently configured in the system are displayed. """ store = Store() self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertIn("There are no ports configured.", sys.stdout.getvalue()) def test_listTCPPort(self): """ When I{axiomatic port list} is invoked for a L{Store} which has a L{TCPPort} in it, the details of that port, including its factory, are written to stdout. """ store = Store() factory = DummyFactory(store=store) port = TCPPort( store=store, factory=factory, portNumber=1234, interface=u"foo") self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factory.storeID, factory) + " %d) TCP, interface %s, port %d\n" % ( port.storeID, port.interface, port.portNumber), sys.stdout.getvalue()) def test_listSSLPort(self): """ When I{axiomatic port list} is invoked for a L{Store} which has an L{SSLPort} in it, the details of that port, including its factory, are written to stdout. """ store = Store(filesdir=self.mktemp()) factory = DummyFactory(store=store) port = SSLPort( store=store, factory=factory, portNumber=1234, interface=u"foo", certificatePath=store.filesdir.child("bar")) self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factory.storeID, factory) + " %d) SSL, interface %s, port %d, certificate %s\n" % ( port.storeID, port.interface, port.portNumber, port.certificatePath.path), sys.stdout.getvalue()) def test_listStringEndpointPort(self): """ When I{axiomatic port list} is invoked for a L{Store} which has an L{StringEndpointPort} in it, the endpoint description and factory are written to stdout. """ store = Store() factory = DummyFactory(store=store) port = StringEndpointPort( store=store, factory=factory, description=u'tcp:1234') self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "{:d}) {!r} listening on:\n".format(factory.storeID, factory) + " {:d}) Endpoint {!r}\n".format(port.storeID, port.description), sys.stdout.getvalue()) def test_listAnyInterface(self): """ I{axiomatic port list} displays a special string for a port bound to C{INADDR_ANY}. """ store = Store() factory = DummyFactory(store=store) port = TCPPort( store=store, factory=factory, portNumber=1234, interface=u"") self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factory.storeID, factory) + " %d) TCP, any interface, port %d\n" % (port.storeID, port.portNumber), sys.stdout.getvalue()) def test_listSSLPortWithoutAttributes(self): """ If there is an L{SSLPort} with no certificate or no port number (a rather invalid configuration), I{axiomatic port list} should show this in its output without producing an error. """ store = Store() factory = DummyFactory(store=store) port = SSLPort(store=store, factory=factory) self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factory.storeID, factory) + " %d) SSL, any interface, NO PORT, NO CERTIFICATE\n" % ( port.storeID,), sys.stdout.getvalue()) def test_listTwoPorts(self): """ I{axiomatic port list} displays two different ports bound to the same factory together beneath that factory. """ store = Store() factory = DummyFactory(store=store) portOne = TCPPort( store=store, factory=factory, portNumber=1234, interface=u"foo") portTwo = TCPPort( store=store, factory=factory, portNumber=2345, interface=u"bar") self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factory.storeID, factory) + " %d) TCP, interface %s, port %d\n" % ( portOne.storeID, portOne.interface, portOne.portNumber) + " %d) TCP, interface %s, port %d\n" % ( portTwo.storeID, portTwo.interface, portTwo.portNumber), sys.stdout.getvalue()) def test_listTwoFactories(self): """ I{axiomatic port list} displays two different factories separately from each other. """ store = Store() factoryOne = DummyFactory(store=store) factoryTwo = DummyFactory(store=store) portOne = TCPPort( store=store, factory=factoryOne, portNumber=10, interface=u"foo") portTwo = TCPPort( store=store, factory=factoryTwo, portNumber=20, interface=u"bar") self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r listening on:\n" % (factoryOne.storeID, factoryOne) + " %d) TCP, interface %s, port %d\n" % ( portOne.storeID, portOne.interface, portOne.portNumber) + "%d) %r listening on:\n" % (factoryTwo.storeID, factoryTwo) + " %d) TCP, interface %s, port %d\n" % ( portTwo.storeID, portTwo.interface, portTwo.portNumber), sys.stdout.getvalue()) def test_listUnlisteningFactory(self): """ I{axiomatic port list} displays factories even if they aren't associate with any port. """ store = Store() factory = DummyFactory(store=store) store.powerUp(factory, IProtocolFactoryFactory) self.assertSuccessStatus(self._makeConfig(store), ["list"]) self.assertEqual( "%d) %r is not listening.\n" % (factory.storeID, factory), sys.stdout.getvalue()) _deleteHelpText = ( "Usage: axiomatic [options] port [options] delete [options]\n" "Options:\n" " --port-identifier= Identify a port for deletion.\n" " --version Display Twisted version and exit.\n" " --help Display this help and exit.\n" "\n" "Delete an existing port binding from a factory. If a server is " "currently running using the database from which the port is deleted, " "the factory will *not* stop listening on that port until the server " "is restarted.") def test_explicitDeleteHelp(self): """ If I{axiomatic port delete} is invoked with I{--help}, usage information for the C{delete} subcommand is written to standard out. """ store = Store() self.assertSuccessStatus(self._makeConfig(store), ["delete", "--help"]) self.assertSpacelessEqual(self._deleteHelpText, sys.stdout.getvalue()) def test_implicitDeleteHelp(self): """ If I{axiomatic port delete} is invoked with no arguments, usage information for the C{delete} subcommand is written to standard out. """ store = Store() self.assertSuccessStatus(self._makeConfig(store), ["delete"]) self.assertSpacelessEqual(self._deleteHelpText, sys.stdout.getvalue()) def test_deletePorts(self): """ I{axiomatic port delete} deletes each ports with a C{storeID} which is specified. """ store = Store(filesdir=self.mktemp()) factory = DummyFactory(store=store) deleteTCP = TCPPort( store=store, factory=factory, portNumber=10, interface=u"foo") keepTCP = TCPPort( store=store, factory=factory, portNumber=10, interface=u"bar") deleteSSL = SSLPort( store=store, factory=factory, portNumber=10, interface=u"baz", certificatePath=store.filesdir.child("baz")) keepSSL = SSLPort( store=store, factory=factory, portNumber=10, interface=u"quux", certificatePath=store.filesdir.child("quux")) self.assertSuccessStatus( self._makeConfig(store), ["delete", "--port-identifier", str(deleteTCP.storeID), "--port-identifier", str(deleteSSL.storeID)]) self.assertEqual("Deleted.\n", sys.stdout.getvalue()) self.assertEqual(list(store.query(TCPPort)), [keepTCP]) self.assertEqual(list(store.query(SSLPort)), [keepSSL]) def test_cannotDeleteOtherStuff(self): """ I{axiomatic port delete} will not delete something which is neither a L{TCPPort} nor an L{SSLPort} and does not delete anything if an invalid port identifier is present in the command. """ store = Store() factory = DummyFactory(store=store) tcp = TCPPort( store=store, factory=factory, interface=u"foo", portNumber=1234) self.assertFailStatus( 1, self._makeConfig(store), ["delete", "--port-identifier", str(tcp.storeID), "--port-identifier", str(factory.storeID)]) self.assertEqual( "%d does not identify a port.\n" % (factory.storeID,), sys.stdout.getvalue()) self.assertEqual(list(store.query(DummyFactory)), [factory]) self.assertEqual(list(store.query(TCPPort)), [tcp]) def test_cannotDeleteNonExistent(self): """ I{axiomatic port delete} writes a short error to standard output when a port-identifier is specified for which there is no corresponding store ID. """ store = Store() self.assertFailStatus( 1, self._makeConfig(store), ["delete", "--port-identifier", "12345"]) self.assertEqual( "12345 does not identify an item.\n", sys.stdout.getvalue()) _createHelpText = ( "Usage: axiomatic [options] port [options] create [options]\n" "Options:\n" " --strport= A Twisted strports description of a " "port to add.\n" " --factory-identifier= Identifier for a protocol factory to " "associate with\n" " the new port.\n" " --version Display Twisted version and exit.\n" " --help Display this help and exit.\n" "\n" "Create a new port binding for an existing factory. If a server is " "currently\n" "running using the database in which the port is created, the " "factory will *not*\n" "be started on that port until the server is restarted.\n" "\n") def test_createImplicitHelp(self): """ If I{axiomatic port create} is invoked with no arguments, usage information for the C{create} subcommand is written to standard out. """ self.assertSuccessStatus(self._makeConfig(None), ["create"]) self.assertSpacelessEqual(self._createHelpText, sys.stdout.getvalue()) def test_createExplicitHelp(self): """ If I{axiomatic port create} is invoked with C{--help} as an argument, usage information for the C{add} subcommand is written to standard out. """ self.assertSuccessStatus(self._makeConfig(None), ["create", "--help"]) self.assertSpacelessEqual(self._createHelpText, sys.stdout.getvalue()) def test_createInvalidPortDescription(self): """ If an invalid string is given for the C{strport} option of I{axiomatic port create}, a short error is written to standard output. """ store = Store() factory = DummyFactory(store=store) self.assertFailStatus( 1, self._makeConfig(store), ["create", "--strport", "xyz", "--factory-identifier", str(factory.storeID)]) self.assertEqual( "'xyz' is not a valid port description.\n", sys.stdout.getvalue()) def test_createNonExistentFactoryIdentifier(self): """ If a storeID which is not associated with any item is given for the C{factory-identifier} option of I{axiomatic port create}, a short error is written to standard output. """ store = Store() self.assertFailStatus( 1, self._makeConfig(store), ["create", "--strport", "tcp:8080", "--factory-identifier", "123"]) self.assertEqual( "123 does not identify an item.\n", sys.stdout.getvalue()) def test_createNonFactoryIdentifier(self): """ If a storeID which is associated with an item which does not provide L{IProtocolFactoryFactory} is given for the C{factory-identifier} option of I{axiomatic port create}, a short error is written to standard output. """ store = Store() storeID = TCPPort(store=store).storeID self.assertFailStatus( 1, self._makeConfig(store), ["create", "--strport", "tcp:8080", "--factory-identifier", str(storeID)]) self.assertEqual( "%d does not identify a factory.\n" % (storeID,), sys.stdout.getvalue()) def test_createPort(self): """ I{axiomatic port create} creates a new L{xmantissa.port.StringEndpointPort} with the specified description, referring to the specified factory. The port is also powered up on the store for L{IService}. """ store = Store() factory = DummyFactory(store=store) self.assertSuccessStatus( self._makeConfig(store), ["create", "--strport", "tcp:8080", "--factory-identifier", str(factory.storeID)]) self.assertEqual("Created.\n", sys.stdout.getvalue()) [port] = list(store.query(StringEndpointPort)) self.assertEqual(u'tcp:8080', port.description) self.assertEqual(list(store.interfacesFor(port)), [IService]) PK;5$Gcc$xmantissa/test/test_howtolistings.py# Copyright 2008 Divmod, Inc. See LICENSE file for details """ This module tests the code listings used in the Mantissa lore documentation, in the top-level 'doc/' directory. See also L{nevow.test.test_howtolistings}. """ import sys from zope.interface import implements from zope.interface.verify import verifyObject from twisted.python.filepath import FilePath from twisted.trial.unittest import TestCase, SkipTest from epsilon.structlike import record from nevow.testutil import FakeRequest from nevow.flat import flatten from nevow.url import URL from axiom.store import Store from xmantissa.ixmantissa import ISiteRootPlugin, IMantissaSite, IWebViewer from xmantissa.ixmantissa import IMessageReceiver from xmantissa.publicweb import AnonymousSite from axiom.plugins.mantissacmd import Mantissa class StubShellPage(record('model')): """ A stub implementation of the shell-page object that should be returned from wrapModel. """ class StubViewer(object): """ A stub implementation of IWebViewer. """ implements(IWebViewer) def wrapModel(self, thingy): """ Wrap a model and return myself, with the model set. """ self.shell = StubShellPage(thingy) return self.shell class ExampleTestBase(object): """ This mixin provides setUp and tearDown methods for adding a specific example to the path. See ticket #2713 for certain issues that this test has with operating in an installed environment. """ def setUp(self): """ Add the example dictated by C{self.examplePath} to L{sys.path}. """ here = FilePath(__file__).parent().parent().parent().child('doc') for childName in self.examplePath: here = here.child(childName) if not here.exists(): raise SkipTest('Docs not found at %r' % (here,)) sys.path.append(here.path) self.addCleanup(sys.path.remove, here.path) class SiteRootDocumentationTest(ExampleTestBase, TestCase): """ Tests for doc/siteroot.xhtml and friends. """ examplePath = ['listings', 'siteroot'] def setUp(self): """ Do the example setup and import the module. """ ExampleTestBase.setUp(self) import aboutpage import adminpage self.aboutpage = aboutpage self.adminpage = adminpage def test_powerItUp(self): """ Powering up a store with an C{AboutPlugin} results in it being installed as an L{ISiteRootPlugin} powerup. """ s = Store() ap = self.aboutpage.AboutPlugin(store=s) s.powerUp(ap) self.assertEquals([ap], list(s.powerupsFor(ISiteRootPlugin))) def test_interface(self): """ C{AboutPlugin} implements L{ISiteRootPlugin}. """ self.assertTrue(verifyObject(ISiteRootPlugin, self.aboutpage.AboutPlugin())) def test_produceAboutResource(self): """ When C{AboutPlugin} is installed on a site store created by 'axiomatic mantissa', requests for 'about.php' will be responded to by a helpful message wrapped in a shell page. """ s = Store(self.mktemp()) s.powerUp(self.aboutpage.AboutPlugin(store=s)) m = Mantissa() m.installSite(s, u"localhost", u"", False) root = IMantissaSite(s) viewer = StubViewer() result, segments = root.siteProduceResource(FakeRequest(), tuple(['about.php']), viewer) self.assertIdentical(result, viewer.shell) self.assertIsInstance(result.model, self.aboutpage.AboutText) def test_notOtherResources(self): """ C{AboutPlugin} will only respond to about.php, not every page on the site. """ s = Store(self.mktemp()) s.powerUp(self.aboutpage.AboutPlugin(store=s)) s.powerUp(AnonymousSite(store=s)) root = IMantissaSite(s) viewer = StubViewer() result = root.siteProduceResource(FakeRequest(), tuple(['undefined']), viewer) self.assertIdentical(result, None) def test_rendering(self): """ C{AboutText} should render a
with a string in it. """ self.assertEquals("
Hello, world!
", flatten(self.aboutpage.AboutText(u'Hello, world!'))) def test_adminRedirect(self): """ When the admin redirect is installed on a store, it should return an URL which should redirect to /private. """ s = Store(self.mktemp()) s.powerUp(self.adminpage.RedirectPlugin(store=s)) m = Mantissa() m.installSite(s, u'localhost', u'', False) root = IMantissaSite(s) viewer = StubViewer() result, segments = root.siteProduceResource(FakeRequest(), tuple(['admin.php']), viewer) self.assertEquals(result, URL.fromString("http://localhost/private")) class InterstoreMessagingDocumentationTests(ExampleTestBase, TestCase): """ Tests for doc/interstore.xhtml and related files. """ examplePath = ['listings', 'interstore'] def setUp(self): """ Import the interstore messaging example code. """ ExampleTestBase.setUp(self) import cal self.cal = cal def test_powerUp(self): """ L{Calendar} is an L{IMessageReceiver} powerup. """ store = Store() calendar = self.cal.Calendar(store=store) self.assertTrue(verifyObject(IMessageReceiver, calendar)) store.powerUp(calendar) self.assertEquals( list(store.powerupsFor(IMessageReceiver)), [calendar]) PK9FSX $ $#xmantissa/test/test_autocomplete.js// Copyright (c) 2006 Divmod. // See LICENSE for details. // import Mantissa.AutoComplete /** * Make a L{Mantissa.AutoComplete.Model} with some completions */ function makeModel() { return Mantissa.AutoComplete.Model( ['a', 'aaa', 'aaab', 'abba', 'abracadabra', 'zoop']); } var _KEYCODE_TAB = 9, _KEYCODE_ENTER = 13, _KEYCODE_UP = 38, _KEYCODE_DOWN = 40, _KEYCODE_ALNUM = 0; /** * Make something which looks a little bit like an event object a browser * might construct in response to a keypress event * * @param keyCode: code of the key that was pressed. C{_KEYCODE_ALNUM}, * C{_KEYCODE_UP}, C{_KEYCODE_DOWN}, C{_KEYCODE_TAB} and C{_KEYCODE_ENTER} are * the interesting ones * @type keyCode: C{Number} * * @rtype: C{Object} */ function makeKeypressEvent(keyCode) { return {keyCode: keyCode}; } /** * L{Mantissa.AutoComplete.View} subclass which doesn't depend on the presence * of any DOM functionality * * @ivar visibleCompletions: list of completions that would be being presented * to the user, if this was a real thing * * @ivar theSelectedCompletion: offset of the currently selected completion in * the completions list. might be C{null} if nothing is selected * * @ivar keypressListener: function which the controller asked us to hook up * to keypress events */ var MockAutoCompleteView = Mantissa.AutoComplete.View.subclass('MockAutoCompleteView'); MockAutoCompleteView.methods( function __init__(self) { self._displayingCompletions = false; self._value = null; self.visibleCompletions = []; self.theSelectedCompletion = null; MockAutoCompleteView.upcall(self, '__init__', null, null); }, /** * Override default implementation to store the function we get in an * instance variable, because there isn't any DOM node to attach it do */ function hookupKeypressListener(self, f) { self.keypressListener = f; }, /** * Override default implementation to store the value in an instance * variable, because there isn't any DOM node to stick it in */ function setValue(self, v) { self._value = v; }, /** * Override default implementation to return whatever the last thing * passed to L{setValue} was */ function getValue(self) { return self._value; }, /** * Override the default implementation to count the number of entries in * C{self.visibleCompletions}, instead of doing DOM stuff */ function completionCount(self) { return self.visibleCompletions.length; }, /** * Override default implementation to instead inspect our explicitly * managed display state */ function displayingCompletions(self) { return self._displayingCompletions; }, /** * Override default implementation to fiddle the appropriate instance * variables */ function showCompletions(self, completions) { self.visibleCompletions = completions; self.theSelectedCompletion = 0; self._displayingCompletions = true; }, /** * Again, override default implementation to store the value we get in an * instance variable for inspection */ function _selectCompletion(self, offset) { self.theSelectedCompletion = offset; }, /** * Override default implementation to remove DOM dependencies */ function selectedCompletion(self) { return {offset: self.theSelectedCompletion, value: self.visibleCompletions[self.theSelectedCompletion]}; }, /** * Override default implementation to remove DOM dependencies */ function emptyAndHideCompletions(self) { self.visibleCompletions = []; self.theSelectedCompletion = null; self._displayingCompletions = false; }); /** * Make a L{Mantissa.AutoComplete.Controller} with a * L{Mantissa.AutoComplete.Model}, as obtained from L{makeModel}, and a * L{MockAutoCompleteView} * * @rype: L{Mantissa.AutoComplete.Controller} */ function makeController() { var view = MockAutoCompleteView(), model = makeModel(); return Mantissa.AutoComplete.Controller(model, view, function(f, when) { f(); }); } runTests([ /** * Test L{Mantissa.AutoComplete.Model.complete} */ function test_modelCompletion() { var model = makeModel(); assertArraysEqual(model.complete('ab'), ['abba', 'abracadabra']); assertArraysEqual(model.complete('zoop'), ['zoop']); assertArraysEqual(model.complete('zap!'), []); }, /** * Test that L{Mantissa.AutoComplete.Model.complete} doesn't think the * empty string has any completions */ function test_modelCompletionEmpty() { var model = makeModel(); assertArraysEqual(model.complete(''), []); }, /** * Test L{Mantissa.AutoComplete.Model.complete} when the string it is * passed is comma-separated */ function test_modelCompletionCSV() { var model = makeModel(); assertArraysEqual(model.complete('zz,yy, aa'), ['aaa', 'aaab']); assertArraysEqual(model.complete(', aa'), ['aaa', 'aaab']); assertArraysEqual(model.complete('zoop, zoo'), ['zoop']); }, /** * Test L{Mantissa.AutoComplete.Model.appendCompletion} */ function test_modelAppendCompletion() { var model = makeModel(); assertEqual(model.appendCompletion('a, b, cra', 'crabapple'), 'a, b, crabapple, '); assertEqual(model.appendCompletion('cra', 'crab!'), 'crab!, '); }, /** * Test L{Mantissa.AutoComplete.Model.isCompletion} */ function test_modelIsCompletion() { var model = makeModel(); assert(model.isCompletion('foo', 'foooo')); assert(model.isCompletion('f', 'f')); assert(model.isCompletion('ab', 'abba')); }, /** * Test L{Mantissa.AutoComplete.Model.isCompletion}, when the things we * pass it are not completions */ function test_modelIsCompletionNeg() { var model = makeModel(); assert(!model.isCompletion('foobar', 'foo')); assert(!model.isCompletion('f', 'g')); assert(!model.isCompletion('zao', 'zo')); }, /** * Test L{Mantissa.AutoComplete.Controller} and its model/view * interactions by telling it to find out about completions for the * contents of our imaginary textbox. */ function test_alnumKeypressWithCompletions() { var controller = makeController(), view = controller.view, keypressListener = view.keypressListener, keypressEvent = makeKeypressEvent(_KEYCODE_ALNUM); view.setValue('abb'); keypressListener(keypressEvent); assertArraysEqual(view.visibleCompletions, ['abba']); assertEqual(view.theSelectedCompletion, 0); }, /** * Test that L{Mantissa.AutoComplete.Controller}'s keypress event handler * correctly interprets up/down keypresses as hints that the selection * should be moved up/down */ function test_completionNavigation() { var controller = makeController(), view = controller.view, keypressListener = view.keypressListener; view.setValue('a'); keypressListener(makeKeypressEvent(_KEYCODE_ALNUM)); assertEqual(view.visibleCompletions.length, 5); assertEqual(view.theSelectedCompletion, 0); keypressListener(makeKeypressEvent(_KEYCODE_DOWN)); assertEqual(view.theSelectedCompletion, 1); keypressListener(makeKeypressEvent(_KEYCODE_UP)); assertEqual(view.theSelectedCompletion, 0); }, /** * Test that L{Mantissa.AutoComplete.View.moveSelectionUp} and * L{Mantissa.AutoComplete.View.moveSelectionDown} correctly wrap the * selection around */ function test_completionNavigationWraparound() { var controller = makeController(), view = controller.view, keypressListener = view.keypressListener; view.setValue('a'); keypressListener(makeKeypressEvent(_KEYCODE_ALNUM)); keypressListener(makeKeypressEvent(_KEYCODE_UP)); assertEqual(view.theSelectedCompletion, 4); keypressListener(makeKeypressEvent(_KEYCODE_DOWN)); assertEqual(view.theSelectedCompletion, 0); }, /** * Test that L{Mantissa.AutoComplete.Controller} understands that ENTER * means we'd like to have the currently selected completion spliced onto * the current value of the view's imaginary textbox */ function test_completionSelection() { var controller = makeController(), view = controller.view, keypressListener = view.keypressListener; view.setValue('z'); keypressListener(makeKeypressEvent(_KEYCODE_ALNUM)); assertArraysEqual(view.visibleCompletions, ['zoop']); keypressListener(makeKeypressEvent(_KEYCODE_ENTER)); assertEqual(view.getValue(), 'zoop, '); assertArraysEqual(view.visibleCompletions, []); }]); PK9F%xmantissa/test/acceptance/__init__.py """ This package contains various interactive demonstrations of functionality that can be used as individualized acceptance tests for particular subsystems. """ PK9F@= %xmantissa/test/acceptance/liveform.py """ An interactive demonstration of L{xmantissa.liveform.LiveForm} and L{xmantissa.liveform.ListChangeParameter}. Run this test like this:: $ twistd -n athena-widget --element=xmantissa.test.acceptance.liveform.testname $ firefox http://localhost:8080/ (where testname is one of "coerce", "inputerrors", "listChangeParameter", "listChangeParameterCompact", "listChangeParameterNoDefaults", "choiceParameter", "choiceParameterCompact") This will display a form which rejects most inputs. """ from xmantissa.liveform import (TEXT_INPUT, InputError, Parameter, LiveForm, ListChangeParameter, ChoiceParameter, Option) def coerce(theText): """ Reject all values of C{theText} except C{'hello, world'}. """ if theText != u'hello, world': raise InputError(u"Try entering 'hello, world'") def inputerrors(): """ Create a L{LiveForm} which rejects most inputs in order to demonstrate how L{InputError} is handled in the browser. """ form = LiveForm( lambda theText: None, [Parameter(u'theText', TEXT_INPUT, coerce, 'Some Text')], u'LiveForm input errors acceptance test', ) return form _parameterDefaults = [{u'foo': 1, u'bar': 2, u'baz': ['1']}, {u'foo': 10, u'bar': 20, u'baz': ['2']}] def _listChangeParameter(**parameterKwargs): counter = [0] def theCallable(repeatableFoo): for create in repeatableFoo.create: create.setter(u'other thing %d' % (counter[0],)) counter[0] += 1 return u'Created %s, edited %s, deleted %s' % (repeatableFoo.create, repeatableFoo.edit, repeatableFoo.delete) form = LiveForm( theCallable, [ListChangeParameter( u'repeatableFoo', [Parameter('foo', TEXT_INPUT, int, 'Enter a number'), Parameter('bar', TEXT_INPUT, int, 'And another'), ChoiceParameter( 'baz', [Option('Value 1', '1', True), Option('Value 2', '2', False)], 'Pick something')], modelObjectDescription=u'Repeatable Foo', **parameterKwargs)]) form.jsClass = u'Mantissa.Test.EchoingFormWidget' return form def listChangeParameter(): """ Create a L{LiveForm} with a L{ListChangeParameter}. """ return _listChangeParameter( defaults=_parameterDefaults, modelObjects=(u'the first thing', u'the second thing')) def listChangeParameterCompact(): """ Create a compact L{LiveForm} with a L{ListChangeParameter}. """ liveForm = listChangeParameter() liveForm.compact() return liveForm def listChangeParameterNoDefaults(): """ Create a L{LiveForm} with a L{ListChangeParameter} and no defaults. """ return _listChangeParameter(defaults=[], modelObjects=[]) def choiceParameter(): """ Create a L{LiveForm} with a L{ChoiceParameter}. """ return LiveForm( lambda **k: unicode(k), [ChoiceParameter( 'choice', [Option('Thing 1', 'thing-one', False), Option('Thing 2', 'thing-two', True), Option('Thing 3', 'thing-three', False)], 'This is a choice between things')]) def choiceParameterCompact(): """ Compact version of the form returned by L{choiceParameter}. """ liveForm = choiceParameter() liveForm.compact() return liveForm PK9Fb1__#xmantissa/test/acceptance/people.py""" An interactive demonstration of various people-related functionality. Run this test like this:: $ twistd -n athena-widget --element=xmantissa.test.acceptance.people.{editperson,addperson} $ firefox http://localhost:8080/ """ from axiom.store import Store from axiom.dependency import installOn from xmantissa.people import Organizer, EditPersonView, AddPersonFragment store = Store() organizer = Organizer(store=store) installOn(organizer, store) person = organizer.createPerson(u'alice') def editperson(): """ Create a database with a Person in it and return the L{EditPersonView} for that person. """ return EditPersonView(person) def addperson(): """ Create a database with an L{Organizer} in it and return a L{AddPersonFragment} wrapped around the organizer. """ return AddPersonFragment(organizer) PK9Fk44(xmantissa/test/acceptance/scrolltable.py """ An interactive demonstration of L{xmantissa.test.scrolltable} Run this test like this:: $ twistd -n athena-widget --element=xmantissa.test.acceptance.scrolltable.scroller $ firefox http://localhost:8080/ This will display a large scrollable table, containing 5000 rows. """ from zope.interface import implements import time from epsilon.extime import Time from axiom.store import Store from axiom.item import Item from axiom.attributes import integer, text, timestamp from nevow.athena import LiveElement from nevow import loaders, tags from xmantissa.scrolltable import ScrollingElement, TYPE_WIDGET from xmantissa.webtheme import getLoader from xmantissa import ixmantissa class Sample(Item): """ A sample item which will be used as the rows in the table displayed. """ quantity = integer(indexed=True) title = text() date = timestamp(indexed=True) color = text(allowNone=False) class SampleColorWidget(LiveElement): """ Trivial L{LiveElement} which renders a square with the background color of L{sampleItem}'s C{color} attribute. @ivar sampleItem: The sample item. @type sampleItem: L{Sample} """ def __init__(self, sampleItem): super(SampleColorWidget, self).__init__( docFactory=loaders.stan( tags.div(style='background-color: %s' % (sampleItem.color,), render=tags.directive('liveElement'))['hi'])) self.sampleItem = sampleItem class SampleColorWidgetColumn(object): """ A widget column which renders a L{SampleColorWidget}. """ implements(ixmantissa.IColumn) attributeID = 'color' def sortAttribute(self): """ L{SampleColorWidgetColumn} objects cannot be sorted. """ return None def extractValue(self, model, sampleItem): """ Make a L{SampleColorWidget} out of C{sampleItem}. """ fragment = SampleColorWidget(sampleItem) fragment.setFragmentParent(model) return fragment def getType(self): """ Return L{TYPE_WIDGET} """ return TYPE_WIDGET def populate(aStore, itemCount=5000): """ Populate the store. @param aStore: an Axiom store to populate @param itemCount: the number of L{Sample} items to create. """ now = time.time() colors = [u'red', u'blue'] def fxn(): for x in xrange(itemCount): yield (x * 2, u"Number %d" % (x,), Time.fromPOSIXTimestamp(now - x), colors[x % 2]) aStore.batchInsert(Sample, [Sample.quantity, Sample.title, Sample.date, Sample.color], fxn()) class FakeTranslator(object): """ This is a fake implementation of IWebTranslator to simplify testing. """ def __init__(self, aStore): """ Create a FakeTranslator from a store. """ self.store = aStore def toWebID(self, theItem): """ Generate a simple webID for a given row. """ return str(theItem.storeID) def fromWebID(self, webID): """ Retrieve a given item from an ID generated by FakeTranslator.toWebID. """ return self.store.getItemByID(int(webID)) def scroller(): """ Create a scrolling element with a large number of rows for use in an interactive demonstration. """ aStore = Store() populate(aStore) se = ScrollingElement(aStore, Sample, None, [Sample.quantity, Sample.date, Sample.title, SampleColorWidgetColumn()], webTranslator=FakeTranslator(aStore), defaultSortAscending=False) se.docFactory = getLoader(se.fragmentName) return se PK9FR~Y22#xmantissa/test/historic/__init__.py# -*- test-case-name: xmantissa.test.historic -*- PK9F-xmantissa/test/historic/stub_addPerson1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.people import AddPerson def createDatabase(s): AddPerson(store=s) if __name__ == '__main__': saveStub(createDatabase, 10664) PK9FNy9xmantissa/test/historic/stub_adminstatsapplication1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.webadmin import AdminStatsApplication def createDatabase(s): AdminStatsApplication(store=s) if __name__ == '__main__': saveStub(createDatabase, 11254) PK9FfP[[4xmantissa/test/historic/stub_ampConfiguration1to2.py# -*- test-case-name: xmantissa.test.historic.test_ampConfiguration1to2 -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Generate a stub for the tests for the upgrade from schema version 1 to 2 of the AMPConfiguration item. """ from axiom.dependency import installOn from axiom.test.historic.stubloader import saveStub from xmantissa.ampserver import AMPConfiguration def createDatabase(store): """ Make an L{AMPConfiguration} in the given store. """ installOn(AMPConfiguration(store=store), store) if __name__ == '__main__': saveStub(createDatabase, 16892) PK9FǬII,xmantissa/test/historic/stub_anonsite1to2.py """ Create an L{AnonymousSite} in a database by itself. """ from axiom.test.historic.stubloader import saveStub from axiom.dependency import installOn from xmantissa.publicweb import AnonymousSite def createDatabase(s): installOn(AnonymousSite(store=s), s) if __name__ == '__main__': saveStub(createDatabase, 16560) PK9FxT ?xmantissa/test/historic/stub_defaultPreferenceCollection1to2.py from xmantissa import prefs def createDatabase(s): prefs.DefaultPreferenceCollection(store=s) from axiom.test.historic.stubloader import saveStub if __name__ == '__main__': saveStub(createDatabase) PK9Fds;8xmantissa/test/historic/stub_developerapplication1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.webadmin import DeveloperApplication def createDatabase(s): DeveloperApplication(store=s) if __name__ == '__main__': saveStub(createDatabase, 10876) PK9Fzz0xmantissa/test/historic/stub_emailAddress1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.people import EmailAddress, Person def createDatabase(s): EmailAddress(store=s, address=u'bob@divmod.com', type=u'default', person=Person(store=s, name=u'Bob')) if __name__ == '__main__': saveStub(createDatabase, 7052) PK9FJrr4xmantissa/test/historic/stub_freeTicketSignup3to4.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.signup import FreeTicketSignup def createDatabase(s): FreeTicketSignup(store=s, prefixURL=u'/a/b', booth=s, benefactor=s, emailTemplate=u'TEMPLATE!') if __name__ == '__main__': saveStub(createDatabase, 5809) PK9Fɑ~444xmantissa/test/historic/stub_freeTicketSignup4to5.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.signup import FreeTicketSignup, Multifactor from xmantissa.webadmin import AdministrativeBenefactor def createDatabase(s): ab = AdministrativeBenefactor(store=s) mf = Multifactor(store=s) mf.add(ab) FreeTicketSignup(store=s, prefixURL=u'/a/b', booth=s, benefactor=mf, emailTemplate=u'TEMPLATE!', prompt=u'OK?') if __name__ == '__main__': saveStub(createDatabase, 6290) PK9F0UŒ554xmantissa/test/historic/stub_freeTicketSignup5to6.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.signup import FreeTicketSignup, Multifactor from xmantissa.webadmin import AdministrativeBenefactor def createDatabase(s): ab = AdministrativeBenefactor(store=s) mf = Multifactor(store=s) mf.add(ab) FreeTicketSignup(store=s, prefixURL=u'/a/b', booth=s, benefactor=mf, emailTemplate=u'TEMPLATE!', prompt=u'OK?') if __name__ == '__main__': saveStub(createDatabase, 10664) PK9F E-xmantissa/test/historic/stub_frontpage1to2.py""" Database-creation script for testing the version 1 to version 2 upgrader of L{xmantissa.publicweb.FrontPage}. """ from axiom.test.historic.stubloader import saveStub from xmantissa.publicweb import FrontPage def createDatabase(s): """ Create a L{FrontPage} item. """ FrontPage(store=s, publicViews=17, privateViews=42) if __name__ == '__main__': saveStub(createDatabase, 16183) PK9FX0xmantissa/test/historic/stub_messagequeue1to2.py# -*- test-case-name: xmantissa.test.historic.test_messagequeue1to2 -*- from axiom.test.historic.stubloader import saveStub from axiom.dependency import installOn from xmantissa.interstore import MessageQueue MESSAGE_COUNT = 17 def createDatabase(store): installOn(MessageQueue(store=store, messageCounter=MESSAGE_COUNT), store) if __name__ == '__main__': saveStub(createDatabase, 17606) PK9F+++xmantissa/test/historic/stub_mugshot1to2.pyfrom twisted.python.filepath import FilePath from axiom.test.historic.stubloader import saveStub from xmantissa.people import Mugshot, Person def createDatabase(s): imgfile = FilePath(__file__).parent().parent().child('resources').child('square.png') outfile = s.newFile('the-image') outfile.write(imgfile.getContent()) outfile.close() Mugshot(store=s, person=Person(store=s, name=u'Bob'), body=outfile.finalpath, type=u'image/png') if __name__ == '__main__': saveStub(createDatabase, 7671) PK9F966+xmantissa/test/historic/stub_mugshot2to3.py""" Database-creation script for testing the version 2 to version 3 upgrader of L{Mugshot}. """ from twisted.python.filepath import FilePath from axiom.test.historic.stubloader import saveStub from xmantissa.people import Mugshot, Person MUGSHOT_TYPE = u'image/png' MUGSHOT_BODY_PATH_SEGMENTS = ('mugshot',) def createDatabase(store): """ Make L{Person} and L{Mugshot} items. Set the C{body} and C{smallerBody} attributes of the L{Mugshot} item to point at a copy of I{xmantissa/test/resources/square.png} beneath the store's directory. """ atomicImageFile = store.newFile(*MUGSHOT_BODY_PATH_SEGMENTS) imageFilePath = FilePath(__file__).parent().parent().child( 'resources').child('square.png') atomicImageFile.write(imageFilePath.getContent()) atomicImageFile.close() Mugshot(store=store, person=Person(store=store), body=atomicImageFile.finalpath, smallerBody=atomicImageFile.finalpath, type=MUGSHOT_TYPE) if __name__ == '__main__': saveStub(createDatabase, 13812) PK9F -xmantissa/test/historic/stub_organizer2to3.pyfrom axiom.test.historic.stubloader import saveStub from axiom.dependency import installOn from xmantissa.people import Organizer def createDatabase(s): installOn(Organizer(store=s), s) if __name__ == '__main__': saveStub(createDatabase, 13142) PK9F'1xmantissa/test/historic/stub_passwordReset1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.signup import PasswordReset def createDatabase(s): PasswordReset(store=s, installedOn=s) if __name__ == '__main__': saveStub(createDatabase, 8991) PK9Fl*xmantissa/test/historic/stub_people1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.people import Organizer def createDatabase(s): Organizer(store=s) if __name__ == '__main__': saveStub(createDatabase, 10664) PK9F*bb*xmantissa/test/historic/stub_person1to2.py """ Generate a stub for the tests for the upgrade from schema version 1 to 2 of the Person item. """ from epsilon.extime import Time from axiom.test.historic.stubloader import saveStub from xmantissa.people import Organizer, Person NAME = u'testuser' CREATED = Time.fromPOSIXTimestamp(1234567890) def createDatabase(store): """ Make a L{Person} in the given store. """ organizer = Organizer(store=store) person = Person( store=store, organizer=organizer, name=NAME) person.created = CREATED if __name__ == '__main__': saveStub(createDatabase, 13023) PK9Fcc*xmantissa/test/historic/stub_person2to3.py """ Generate a stub for the tests for the upgrade from schema version 2 to 3 of the Person item. """ from epsilon.extime import Time from axiom.test.historic.stubloader import saveStub from xmantissa.people import Organizer, Person NAME = u'Test User' CREATED = Time.fromPOSIXTimestamp(1234567890) def createDatabase(store): """ Make a L{Person} in the given store. """ organizer = Organizer(store=store) person = Person( store=store, organizer=organizer, name=NAME) person.created = CREATED if __name__ == '__main__': saveStub(createDatabase, 13344) PK9F&.mm/xmantissa/test/historic/stub_phoneNumber1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.people import PhoneNumber, Person def createDatabase(s): PhoneNumber(store=s, number=u'555-1212', type=u'default', person=Person(store=s, name=u'Bob')) if __name__ == '__main__': saveStub(createDatabase, 7052) PK9FT(xmantissa/test/historic/stub_port1to2.py from OpenSSL.crypto import FILETYPE_PEM from twisted.internet.ssl import PrivateCertificate, KeyPair from axiom.item import Item from axiom.attributes import text from axiom.dependency import installOn from axiom.test.historic.stubloader import saveStub from xmantissa.port import TCPPort, SSLPort from xmantissa.website import WebSite # Unfortunately, the test module for this store binds ports. So pick some # improbably port numbers and hope they aren't bound. If they are, the test # will fail. Hooray! -exarkun TCP_PORT = 29415 SSL_PORT = 19224 def createDatabase(siteStore): """ Populate the given Store with a TCPPort and SSLPort. """ factory = WebSite(store=siteStore) installOn(factory, siteStore) installOn( TCPPort(store=siteStore, portNumber=TCP_PORT, factory=factory), siteStore) certificatePath = siteStore.newFilePath('certificate') key = KeyPair.generate() cert = key.selfSignedCert(1) certificatePath.setContent( cert.dump(FILETYPE_PEM) + key.dump(FILETYPE_PEM)) installOn( SSLPort(store=siteStore, portNumber=SSL_PORT, certificatePath=certificatePath, factory=factory), siteStore) if __name__ == '__main__': saveStub(createDatabase, 12731) PK9F(B6xmantissa/test/historic/stub_privateApplication2to3.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.webapp import PrivateApplication def createDatabase(s): PrivateApplication(store=s) if __name__ == '__main__': saveStub(createDatabase, 10876) PK9FnPh''6xmantissa/test/historic/stub_privateApplication3to4.py# -*- test-case-name: xmantissa.test.historic.test_privateApplication3to4 -*- """ Generate a test database containing a L{PrivateApplication} installed on its store without powering it up for L{ITemplateNameResolver}. """ from axiom.test.historic.stubloader import saveStub from axiom.dependency import installOn from axiom.userbase import LoginSystem from xmantissa.webapp import PrivateApplication USERNAME = u'testuser' DOMAIN = u'localhost' PREFERRED_THEME = u'theme-preference' HIT_COUNT = 8765 PRIVATE_KEY = 123456 def createDatabase(store): """ Instantiate a L{PrivateApplication} in C{store} and install it. """ loginSystem = LoginSystem(store=store) installOn(loginSystem, store) account = loginSystem.addAccount(USERNAME, DOMAIN, None) subStore = account.avatars.open() app = PrivateApplication( store=subStore, preferredTheme=PREFERRED_THEME, hitCount=HIT_COUNT, privateKey=PRIVATE_KEY) installOn(app, subStore) if __name__ == '__main__': saveStub(createDatabase, 12759) PK9F3  6xmantissa/test/historic/stub_privateApplication4to5.py# -*- test-case-name: xmantissa.test.historic.test_privateApplication3to4 -*- """ Generate a test database containing a L{PrivateApplication} installed on its store without powering it up for L{ITemplateNameResolver}. """ from axiom.test.historic.stubloader import saveStub from axiom.dependency import installOn from axiom.userbase import LoginSystem from xmantissa.webapp import PrivateApplication USERNAME = u'testuser' DOMAIN = u'localhost' PREFERRED_THEME = u'theme-preference' HIT_COUNT = 8765 PRIVATE_KEY = 123456 def createDatabase(store): """ Instantiate a L{PrivateApplication} in C{store} and install it. """ loginSystem = LoginSystem(store=store) installOn(loginSystem, store) account = loginSystem.addAccount(USERNAME, DOMAIN, None) subStore = account.avatars.open() app = PrivateApplication( store=subStore, preferredTheme=PREFERRED_THEME, privateKey=PRIVATE_KEY) installOn(app, subStore) if __name__ == '__main__': saveStub(createDatabase, 16534) PK9Fr[3xmantissa/test/historic/stub_pyLuceneIndexer3to4.pyfrom twisted.python.filepath import FilePath from axiom.test.historic.stubloader import saveStub from xmantissa.fulltext import PyLuceneIndexer def createDatabase(s): PyLuceneIndexer(store=s, installedOn=s, indexCount=23, indexDirectory=u'foo.index').installOn(s) if __name__ == '__main__': saveStub(createDatabase, 9044) PK9Fq3xmantissa/test/historic/stub_pyLuceneIndexer4to5.py# -*- test-case-name: xmantissa.test.historic.test_pyLuceneIndexer4to5 -*- # Copyright 2005 Divmod, Inc. See LICENSE file for details from axiom.test.historic.stubloader import saveStub from xmantissa.test.historic.stub_pyLuceneIndexer3to4 import createDatabase as _createDatabase def createDatabase(*a, **kw): return _createDatabase(*a, **kw) if __name__ == '__main__': saveStub(createDatabase, 10568) PK9FE,xmantissa/test/historic/stub_realname1to2.py """ Generate a stub for the tests for the deletion of the RealName item. """ from axiom.test.historic.stubloader import saveStub from xmantissa.people import Person, RealName def createDatabase(store): """ Make a L{Person} with a corresponding L{RealName} which will be deleted. """ person = Person(store=store) name = RealName(store=store, person=person) if __name__ == '__main__': saveStub(createDatabase, 13508) PK9Fuצ1xmantissa/test/historic/stub_remoteIndexer1to2.py from axiom.test.historic.stubloader import saveStub from axiom.item import Item from axiom.attributes import integer from axiom.batch import processor from xmantissa.fulltext import HypeIndexer, XapianIndexer, PyLuceneIndexer class StubItem(Item): """ Place-holder. Stands in as an indexable thing, but no instances of this will ever actually be created. """ __module__ = 'xmantissa.test.historic.stub_remoteIndexer1to2' attribute = integer() StubSource = processor(StubItem) def createDatabase(s): """ Create a batch processor for L{StubItem} instances and add it as a message source to an instance of each of the kinds of indexers we support. """ source = StubSource(store=s) for cls in [HypeIndexer, XapianIndexer, PyLuceneIndexer]: indexer = cls(store=s) source.addReliableListener(indexer) if __name__ == '__main__': saveStub(createDatabase, 7053) PK9F=-zz1xmantissa/test/historic/stub_remoteIndexer2to3.py# -*- test-case-name: xmantissa.test.historic.test_remoteIndexer2to3 -*- from axiom.test.historic.stubloader import saveStub from xmantissa.fulltext import HypeIndexer, XapianIndexer, PyLuceneIndexer from xmantissa.test.test_fulltext import FakeMessageSource def createDatabase(s): """ Create a several indexers in the given store and hook them each up to a dummy message source. """ source = FakeMessageSource(store=s) for cls in [HypeIndexer, XapianIndexer, PyLuceneIndexer]: indexer = cls(store=s) indexer.addSource(source) if __name__ == '__main__': saveStub(createDatabase, 8029) PK9F 660xmantissa/test/historic/stub_searchresult1to2.py# -*- test-case-name: xmantissa.test.historic.test_searchresult1to2 -*- from axiom.test.historic.stubloader import saveStub from xmantissa.search import SearchResult def createDatabase(s): SearchResult(store=s, indexedItem=s, identifier=0) if __name__ == '__main__': saveStub(createDatabase, 7976) PK9F.,xmantissa/test/historic/stub_settings1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.settings import Settings def createDatabase(s): Settings(store=s, installedOn=s) if __name__ == '__main__': saveStub(createDatabase, 8528) PK9F88.xmantissa/test/historic/stub_statBucket1to2.py from axiom.test.historic.stubloader import saveStub from xmantissa.stats import StatBucket def createDatabase(s): StatBucket(store=s, type=u"_axiom_query:select 1") StatBucket(store=s, type=u"axiom_commits") if __name__ == '__main__': saveStub(createDatabase, 7071) PK9F:cc*xmantissa/test/historic/stub_ticket1to2.py# -*- test-case-name: xmantissa.test.historic.test_ticket1to2 -*- """ Stub for Ticket upgrade. Creates a Ticket using the normal signup machinery. """ from axiom.test.historic.stubloader import saveStub from xmantissa.signup import SignupConfiguration, FreeTicketSignup, Multifactor from xmantissa.provisioning import BenefactorFactory from xmantissa.webadmin import AdministrativeBenefactor def createDatabase(s): mff = BenefactorFactory("", "", AdministrativeBenefactor) sc = SignupConfiguration(store=s) sc.installOn(s) s.parent = s signup = sc.createSignup(u'bob', FreeTicketSignup, {'prefixURL': u'/signup'}, {mff: {}}, None, u'Sign Up') t = signup.booth.createTicket(signup, u'bob@example.com', signup.benefactor) if __name__ == '__main__': saveStub(createDatabase, 10876) PK9F)&''2xmantissa/test/historic/stub_userInfoSignup1to2.pyfrom axiom.test.historic.stubloader import saveStub from xmantissa.signup import UserInfoSignup, Multifactor from xmantissa.webadmin import AdministrativeBenefactor def createDatabase(s): ab = AdministrativeBenefactor(store=s) mf = Multifactor(store=s) mf.add(ab) UserInfoSignup(store=s, prefixURL=u'/a/b', booth=s, benefactor=mf, emailTemplate=u'TEMPLATE!', prompt=u'OK?') if __name__ == '__main__': saveStub(createDatabase, 10664) PK9F,]T,xmantissa/test/historic/stub_userinfo1to2.py# -*- test-case-name: xmantissa.test.historic.test_userinfo1to2 -*- """ Create the stub database for the test for the UserInfo schema version 1 to 2 upgrader. """ from axiom.test.historic.stubloader import saveStub from xmantissa.signup import UserInfo FIRST = u'Alice' LAST = u'Smith' def createDatabase(store): """ Create a version 1 L{UserInfo} item in the given store. """ UserInfo(store=store, firstName=FIRST, lastName=LAST) if __name__ == '__main__': saveStub(createDatabase, 13447) PK9F uP P +xmantissa/test/historic/stub_website3to4.py# -*- test-case-name: xmantissa.test.historic.test_website3to4 -*- from axiom.userbase import LoginSystem from axiom.test.historic.stubloader import saveStub from axiom.plugins.mantissacmd import Mantissa from xmantissa.website import WebSite cert = ( '-----BEGIN CERTIFICATE-----\n' 'MIICmjCCAgMCBACFAjkwDQYJKoZIhvcNAQEEBQAwgZMxCzAJBgNVBAYTAlVTMRMw\n' 'EQYDVQQDEwpkaXZtb2QuY29tMREwDwYDVQQHEwhOZXcgWW9yazETMBEGA1UEChMK\n' 'RGl2bW9kIExMQzERMA8GA1UECBMITmV3IFlvcmsxITAfBgkqhkiG9w0BCQEWEnN1\n' 'cHBvcnRAZGl2bW9kLm9yZzERMA8GA1UECxMIU2VjdXJpdHkwHhcNMDgwMjIwMjEy\n' 'NDExWhcNMDkwMjE5MjEyNDExWjCBkzELMAkGA1UEBhMCVVMxEzARBgNVBAMTCmRp\n' 'dm1vZC5jb20xETAPBgNVBAcTCE5ldyBZb3JrMRMwEQYDVQQKEwpEaXZtb2QgTExD\n' 'MREwDwYDVQQIEwhOZXcgWW9yazEhMB8GCSqGSIb3DQEJARYSc3VwcG9ydEBkaXZt\n' 'b2Qub3JnMREwDwYDVQQLEwhTZWN1cml0eTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw\n' 'gYkCgYEA3ucaT8gUB7BEp2dfRulWBRT6tTDELA7sJzyk+12E1vxQppJDzwG8VgSj\n' 'sOl8Jw0qnUb/Qoe96UlA8hDYBbmwz0CCvVRSj+1GYj6Ka8NeheME2RU3/benLbyL\n' 'S7HUQ93Mqs3VWrlv2lMbgp29njwJqvqMRt8JGB1ql8xUDSLw4kcCAwEAATANBgkq\n' 'hkiG9w0BAQQFAAOBgQAXxMBJu+VkazQSuOnIn5Ewug2tHmf0sxT7FkcB2nEviQ7U\n' 'e2bb95IL9XqkO0yKEbJ5K8T8SyXW9VNUATce4JO6NNikyVCZzV1dG2+ATDBaaVHK\n' 'S2Beh1p6boFvv0+k2qZ/9JmJYVx4l1xPavc70x95rR2E0kuwhyw4miHpSMqfpA==\n' '-----END CERTIFICATE-----\n' '-----BEGIN RSA PRIVATE KEY-----\n' 'MIICXQIBAAKBgQDe5xpPyBQHsESnZ19G6VYFFPq1MMQsDuwnPKT7XYTW/FCmkkPP\n' 'AbxWBKOw6XwnDSqdRv9Ch73pSUDyENgFubDPQIK9VFKP7UZiPoprw16F4wTZFTf9\n' 't6ctvItLsdRD3cyqzdVauW/aUxuCnb2ePAmq+oxG3wkYHWqXzFQNIvDiRwIDAQAB\n' 'AoGAG/YHgeyKPrCo3AsGk6GfjcGk9WeppBE3JHDiDToc+M7r2wlMAkKoem3Yjs+r\n' 'KEbpipMmYBUhCIuM3xCn2IgDmq/9rC+mDmEu7mEvL0Rnl5Ns6m/uw61kYKDAghYg\n' 'K7lD3jlAT/a9I8wB2UO9F6p8166YERU736Qa4GUle4l8irECQQD0V6ZbbW1o5j5s\n' 'IUzhVvBr/flWabpMJ9Vw3eLy695iFjgx+5W0nD+JK1ny8MiwCRsjoRTXldHhdaod\n' '8VbPz/QJAkEA6YmX6XksIb8JUYFtPk0WodQmz51qzo0jol3COL/rXuPVkTcesyTM\n' '61S7WSv0G6pMqE9xw0llMBON7Pr24N/XzwJBALW+eFvrEgWDtQyi3FeEXkJFX+/5\n' 'pnu86VMRiByeewREeLoc4ya7TbsOxtIgbXYa39fpmeIda0ajSc0J1UOv71kCQQCO\n' 'q20vx8PrNc7WiTAY4HVUFcxEB5Ipb1X2qjqt+qkrBhsBpN/PZ0r89X2iw1RU1lwQ\n' 'csA4Io17qmaJAORziqxHAkAb2zin9SzS58+X55pGVp8PwhGLmm9cGH/DtWVSIAl2\n' 'q3pqCmcxnimc+IYJJlY6dkk7jtnIVTWz3B9XUOtKGEYF\n' '-----END RSA PRIVATE KEY-----\n') def createDatabase(store): """ Initialize the given Store for use as a Mantissa webserver. """ Mantissa().installSite(store, u'') ws = store.findUnique(WebSite) ws.portNumber = 8088 ws.securePortNumber = 6443 ws.certificateFile = 'path/to/cert.pem' certPath = store.dbdir.child('path').child('to').child('cert.pem') certPath.parent().makedirs() fObj = certPath.open('w') fObj.write(cert) fObj.close() ws.httpLog = 'path/to/httpd.log' ws.hitCount = 123 loginSystem = store.findUnique(LoginSystem) account = loginSystem.addAccount(u'testuser', u'localhost', None) subStore = account.avatars.open() WebSite(store=subStore, hitCount=321).installOn(subStore) if __name__ == '__main__': saveStub(createDatabase, 7617) PK9FNٸ* * +xmantissa/test/historic/stub_website4to5.py# -*- test-case-name: xmantissa.test.historic.test_website4to5 -*- from axiom.test.historic.stubloader import saveStub from axiom.plugins.mantissacmd import Mantissa from axiom.userbase import LoginSystem from axiom.dependency import installOn from xmantissa.website import WebSite cert = ( '-----BEGIN CERTIFICATE-----\n' 'MIICmjCCAgMCBACFAjkwDQYJKoZIhvcNAQEEBQAwgZMxCzAJBgNVBAYTAlVTMRMw\n' 'EQYDVQQDEwpkaXZtb2QuY29tMREwDwYDVQQHEwhOZXcgWW9yazETMBEGA1UEChMK\n' 'RGl2bW9kIExMQzERMA8GA1UECBMITmV3IFlvcmsxITAfBgkqhkiG9w0BCQEWEnN1\n' 'cHBvcnRAZGl2bW9kLm9yZzERMA8GA1UECxMIU2VjdXJpdHkwHhcNMDgwMjIwMjEy\n' 'NDExWhcNMDkwMjE5MjEyNDExWjCBkzELMAkGA1UEBhMCVVMxEzARBgNVBAMTCmRp\n' 'dm1vZC5jb20xETAPBgNVBAcTCE5ldyBZb3JrMRMwEQYDVQQKEwpEaXZtb2QgTExD\n' 'MREwDwYDVQQIEwhOZXcgWW9yazEhMB8GCSqGSIb3DQEJARYSc3VwcG9ydEBkaXZt\n' 'b2Qub3JnMREwDwYDVQQLEwhTZWN1cml0eTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw\n' 'gYkCgYEA3ucaT8gUB7BEp2dfRulWBRT6tTDELA7sJzyk+12E1vxQppJDzwG8VgSj\n' 'sOl8Jw0qnUb/Qoe96UlA8hDYBbmwz0CCvVRSj+1GYj6Ka8NeheME2RU3/benLbyL\n' 'S7HUQ93Mqs3VWrlv2lMbgp29njwJqvqMRt8JGB1ql8xUDSLw4kcCAwEAATANBgkq\n' 'hkiG9w0BAQQFAAOBgQAXxMBJu+VkazQSuOnIn5Ewug2tHmf0sxT7FkcB2nEviQ7U\n' 'e2bb95IL9XqkO0yKEbJ5K8T8SyXW9VNUATce4JO6NNikyVCZzV1dG2+ATDBaaVHK\n' 'S2Beh1p6boFvv0+k2qZ/9JmJYVx4l1xPavc70x95rR2E0kuwhyw4miHpSMqfpA==\n' '-----END CERTIFICATE-----\n' '-----BEGIN RSA PRIVATE KEY-----\n' 'MIICXQIBAAKBgQDe5xpPyBQHsESnZ19G6VYFFPq1MMQsDuwnPKT7XYTW/FCmkkPP\n' 'AbxWBKOw6XwnDSqdRv9Ch73pSUDyENgFubDPQIK9VFKP7UZiPoprw16F4wTZFTf9\n' 't6ctvItLsdRD3cyqzdVauW/aUxuCnb2ePAmq+oxG3wkYHWqXzFQNIvDiRwIDAQAB\n' 'AoGAG/YHgeyKPrCo3AsGk6GfjcGk9WeppBE3JHDiDToc+M7r2wlMAkKoem3Yjs+r\n' 'KEbpipMmYBUhCIuM3xCn2IgDmq/9rC+mDmEu7mEvL0Rnl5Ns6m/uw61kYKDAghYg\n' 'K7lD3jlAT/a9I8wB2UO9F6p8166YERU736Qa4GUle4l8irECQQD0V6ZbbW1o5j5s\n' 'IUzhVvBr/flWabpMJ9Vw3eLy695iFjgx+5W0nD+JK1ny8MiwCRsjoRTXldHhdaod\n' '8VbPz/QJAkEA6YmX6XksIb8JUYFtPk0WodQmz51qzo0jol3COL/rXuPVkTcesyTM\n' '61S7WSv0G6pMqE9xw0llMBON7Pr24N/XzwJBALW+eFvrEgWDtQyi3FeEXkJFX+/5\n' 'pnu86VMRiByeewREeLoc4ya7TbsOxtIgbXYa39fpmeIda0ajSc0J1UOv71kCQQCO\n' 'q20vx8PrNc7WiTAY4HVUFcxEB5Ipb1X2qjqt+qkrBhsBpN/PZ0r89X2iw1RU1lwQ\n' 'csA4Io17qmaJAORziqxHAkAb2zin9SzS58+X55pGVp8PwhGLmm9cGH/DtWVSIAl2\n' 'q3pqCmcxnimc+IYJJlY6dkk7jtnIVTWz3B9XUOtKGEYF\n' '-----END RSA PRIVATE KEY-----\n') def createDatabase(store): """ Initialize the given Store for use as a Mantissa webserver. """ Mantissa().installSite(store, u'') site = store.findUnique(WebSite) site.portNumber = 8088 site.securePortNumber = 6443 site.certificateFile = 'server.pem' store.dbdir.child('server.pem').setContent(cert) site.httpLog = 'path/to/httpd.log' site.hitCount = 123 site.hostname = u'example.net' loginSystem = store.findUnique(LoginSystem) account = loginSystem.addAccount(u'testuser', u'localhost', None) subStore = account.avatars.open() installOn(WebSite(store=subStore, hitCount=321), subStore) if __name__ == '__main__': saveStub(createDatabase, 11023) PK9F. +xmantissa/test/historic/stub_website5to6.py# -*- test-case-name: xmantissa.test.historic.test_website5to6 -*- from axiom.test.historic.stubloader import saveStub from axiom.plugins.mantissacmd import Mantissa from axiom.userbase import LoginSystem from axiom.dependency import installOn from xmantissa.port import TCPPort, SSLPort from xmantissa.website import WebSite cert = ( '-----BEGIN CERTIFICATE-----\n' 'MIICmjCCAgMCBACFAjkwDQYJKoZIhvcNAQEEBQAwgZMxCzAJBgNVBAYTAlVTMRMw\n' 'EQYDVQQDEwpkaXZtb2QuY29tMREwDwYDVQQHEwhOZXcgWW9yazETMBEGA1UEChMK\n' 'RGl2bW9kIExMQzERMA8GA1UECBMITmV3IFlvcmsxITAfBgkqhkiG9w0BCQEWEnN1\n' 'cHBvcnRAZGl2bW9kLm9yZzERMA8GA1UECxMIU2VjdXJpdHkwHhcNMDgwMjIwMjEy\n' 'NDExWhcNMDkwMjE5MjEyNDExWjCBkzELMAkGA1UEBhMCVVMxEzARBgNVBAMTCmRp\n' 'dm1vZC5jb20xETAPBgNVBAcTCE5ldyBZb3JrMRMwEQYDVQQKEwpEaXZtb2QgTExD\n' 'MREwDwYDVQQIEwhOZXcgWW9yazEhMB8GCSqGSIb3DQEJARYSc3VwcG9ydEBkaXZt\n' 'b2Qub3JnMREwDwYDVQQLEwhTZWN1cml0eTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw\n' 'gYkCgYEA3ucaT8gUB7BEp2dfRulWBRT6tTDELA7sJzyk+12E1vxQppJDzwG8VgSj\n' 'sOl8Jw0qnUb/Qoe96UlA8hDYBbmwz0CCvVRSj+1GYj6Ka8NeheME2RU3/benLbyL\n' 'S7HUQ93Mqs3VWrlv2lMbgp29njwJqvqMRt8JGB1ql8xUDSLw4kcCAwEAATANBgkq\n' 'hkiG9w0BAQQFAAOBgQAXxMBJu+VkazQSuOnIn5Ewug2tHmf0sxT7FkcB2nEviQ7U\n' 'e2bb95IL9XqkO0yKEbJ5K8T8SyXW9VNUATce4JO6NNikyVCZzV1dG2+ATDBaaVHK\n' 'S2Beh1p6boFvv0+k2qZ/9JmJYVx4l1xPavc70x95rR2E0kuwhyw4miHpSMqfpA==\n' '-----END CERTIFICATE-----\n' '-----BEGIN RSA PRIVATE KEY-----\n' 'MIICXQIBAAKBgQDe5xpPyBQHsESnZ19G6VYFFPq1MMQsDuwnPKT7XYTW/FCmkkPP\n' 'AbxWBKOw6XwnDSqdRv9Ch73pSUDyENgFubDPQIK9VFKP7UZiPoprw16F4wTZFTf9\n' 't6ctvItLsdRD3cyqzdVauW/aUxuCnb2ePAmq+oxG3wkYHWqXzFQNIvDiRwIDAQAB\n' 'AoGAG/YHgeyKPrCo3AsGk6GfjcGk9WeppBE3JHDiDToc+M7r2wlMAkKoem3Yjs+r\n' 'KEbpipMmYBUhCIuM3xCn2IgDmq/9rC+mDmEu7mEvL0Rnl5Ns6m/uw61kYKDAghYg\n' 'K7lD3jlAT/a9I8wB2UO9F6p8166YERU736Qa4GUle4l8irECQQD0V6ZbbW1o5j5s\n' 'IUzhVvBr/flWabpMJ9Vw3eLy695iFjgx+5W0nD+JK1ny8MiwCRsjoRTXldHhdaod\n' '8VbPz/QJAkEA6YmX6XksIb8JUYFtPk0WodQmz51qzo0jol3COL/rXuPVkTcesyTM\n' '61S7WSv0G6pMqE9xw0llMBON7Pr24N/XzwJBALW+eFvrEgWDtQyi3FeEXkJFX+/5\n' 'pnu86VMRiByeewREeLoc4ya7TbsOxtIgbXYa39fpmeIda0ajSc0J1UOv71kCQQCO\n' 'q20vx8PrNc7WiTAY4HVUFcxEB5Ipb1X2qjqt+qkrBhsBpN/PZ0r89X2iw1RU1lwQ\n' 'csA4Io17qmaJAORziqxHAkAb2zin9SzS58+X55pGVp8PwhGLmm9cGH/DtWVSIAl2\n' 'q3pqCmcxnimc+IYJJlY6dkk7jtnIVTWz3B9XUOtKGEYF\n' '-----END RSA PRIVATE KEY-----\n') def createDatabase(store): """ Initialize the given Store for use as a Mantissa webserver. """ Mantissa().installSite(store, u'example.net') site = store.findUnique(WebSite) site.httpLog = store.filesdir.child('httpd.log').path site.hitCount = 123 site.hostname = u'example.net' tcp = store.findUnique(TCPPort, TCPPort.factory == site) tcp.portNumber = 8088 ssl = store.findUnique(SSLPort, SSLPort.factory == site) ssl.portNumber = 6443 ssl.certificatePath.setContent(cert) loginSystem = store.findUnique(LoginSystem) account = loginSystem.addAccount(u'testuser', u'localhost', None) subStore = account.avatars.open() installOn(WebSite(store=subStore, hitCount=321), subStore) if __name__ == '__main__': saveStub(createDatabase, 14982) PK9FAA-xmantissa/test/historic/test_addPerson1to2.pyfrom axiom.test.historic import stubloader from xmantissa.people import AddPerson, Organizer from xmantissa.webapp import PrivateApplication class AddPersonTest(stubloader.StubbedTest): def testUpgrade(self): ap = self.store.findUnique(AddPerson) self.failUnless(isinstance(ap.organizer, Organizer)) PK9Fي.9xmantissa/test/historic/test_adminstatsapplication1to2.pyfrom axiom.test.historic import stubloader from xmantissa.webadmin import AdminStatsApplication from xmantissa.webapp import PrivateApplication class ASATestCase(stubloader.StubbedTest): def testUpgrade(self): """ Ensure upgraded fields refer to correct items. """ self.assertEqual(self.store.findUnique(AdminStatsApplication).privateApplication, self.store.findUnique(PrivateApplication)) PK9F#Rt,4xmantissa/test/historic/test_ampConfiguration1to2.py# Copyright 2008 Divmod, Inc. See LICENSE file for details """ Tests for AMPConfiguration's 1 to 2 upgrader. """ from axiom.test.historic import stubloader from axiom.userbase import LoginSystem from xmantissa.ampserver import AMPConfiguration from xmantissa.ixmantissa import IOneTimePadGenerator class AMPConfigurationUpgradeTests(stubloader.StubbedTest): """ Tests for AMPConfiguration's 1 to 2 upgrader. """ def test_attributeCopied(self): """ L{AMPConfiguration.loginSystem}'s value should have been preserved. """ self.assertIdentical( self.store.findUnique(AMPConfiguration).loginSystem, self.store.findUnique(LoginSystem)) def test_poweredUp(self): """ L{AMPConfiguration} should be powered-up as L{IOneTimePadGenerator}. """ self.assertIdentical( IOneTimePadGenerator(self.store), self.store.findUnique(AMPConfiguration)) PK9Fׂ6,xmantissa/test/historic/test_anonsite1to2.py from axiom.userbase import LoginSystem from axiom.test.historic.stubloader import StubbedTest from nevow.inevow import IResource from xmantissa.ixmantissa import IMantissaSite, IWebViewer from xmantissa.publicweb import AnonymousSite class AnonymousSiteUpgradeTests(StubbedTest): """ Tests to verify that the L{AnonymousSite} was properly upgraded. """ def test_attribute(self): """ Make sure that the one attribute defined by L{AnonymousSite} is properly set. """ ls = self.store.findUnique(LoginSystem) site = self.store.findUnique(AnonymousSite) self.assertIdentical(site.loginSystem, ls) def test_powerups(self): """ L{AnonymousSite} should be installed as a powerup for L{IWebViewer} and L{IMantissaSite}. """ self.assertEqual( set(list(self.store.interfacesFor( self.store.findUnique(AnonymousSite)))), set([IResource, IMantissaSite, IWebViewer])) PK9FN77?xmantissa/test/historic/test_defaultPreferenceCollection1to2.py from axiom.test.historic import stubloader from xmantissa.prefs import DefaultPreferenceCollection class DefaultPreferenceCollectionTestCase(stubloader.StubbedTest): def testUpgrade(self): pc = self.store.findUnique(DefaultPreferenceCollection) self.assertEqual(pc.timezone, 'US/Eastern') PK9F8xmantissa/test/historic/test_developerapplication1to2.pyfrom axiom.test.historic import stubloader from xmantissa.webadmin import DeveloperApplication from xmantissa.webapp import PrivateApplication class DATestCase(stubloader.StubbedTest): def testUpgrade(self): """ Ensure upgraded fields refer to correct items. """ self.assertEqual(self.store.findUnique(DeveloperApplication).privateApplication, self.store.findUnique(PrivateApplication)) PK9F6/uu0xmantissa/test/historic/test_emailAddress1to2.pyfrom axiom.test.historic import stubloader from xmantissa.people import EmailAddress, Person class EmailAddressTestCase(stubloader.StubbedTest): def testUpgrade(self): ea = self.store.findUnique(EmailAddress) person = self.store.findUnique(Person) self.assertIdentical(ea.person, person) self.assertEquals(ea.address, 'bob@divmod.com') PK9FXxw`4xmantissa/test/historic/test_freeTicketSignup3to4.pyfrom axiom.test.historic import stubloader from xmantissa.signup import FreeTicketSignup class FreeTicketSignupTestCase(stubloader.StubbedTest): def testUpgrade(self): fts = self.store.findUnique(FreeTicketSignup) ae = self.assertEqual ae(fts.prefixURL, '/a/b') ae(fts.booth, self.store) ae(fts.emailTemplate, 'TEMPLATE!') ae(fts.prompt, 'Sign Up') PK9F߮`4xmantissa/test/historic/test_freeTicketSignup4to5.pyfrom axiom.test.historic import stubloader from xmantissa.signup import PasswordReset class FreeTicketSignupTestCase(stubloader.StubbedTest): def testUpgrade(self): self.assertEqual(self.store.count(PasswordReset), 0) PK9Fel54xmantissa/test/historic/test_freeTicketSignup5to6.pyfrom axiom.test.historic import stubloader from xmantissa.signup import FreeTicketSignup class FreeTicketSignupTestCase(stubloader.StubbedTest): def testUpgrade(self): fts = self.store.findUnique(FreeTicketSignup) self.assertEqual(len(fts.product.types), 6) PK9Fo#l0NN-xmantissa/test/historic/test_frontpage1to2.pyfrom axiom.test.historic import stubloader from xmantissa.publicweb import FrontPage class FrontPageTest(stubloader.StubbedTest): """ Upgrader test for L{xmantissa.publicweb.FrontPage}. """ def testUpgrade(self): """ All the attributes of L{xmantissa.publicweb.FrontPage} are present after upgrading. """ fp = self.store.findUnique(FrontPage) self.assertEqual(fp.publicViews, 17) self.assertEqual(fp.privateViews, 42) self.assertEqual(fp.prefixURL, u'') self.assertEqual(fp.defaultApplication, None) PK9Fc0xmantissa/test/historic/test_messagequeue1to2.py """ Tests for the upgrade of L{MessageQueue} from version 1 to 2, in which its C{scheduler} attribute was removed. """ from axiom.test.historic.stubloader import StubbedTest from xmantissa.ixmantissa import IMessageRouter from xmantissa.interstore import MessageQueue from xmantissa.test.historic.stub_messagequeue1to2 import MESSAGE_COUNT class MessageQueueUpgradeTests(StubbedTest): def test_attributes(self): """ The value of the C{messageCounter} attribute is preserved by the upgrade. """ self.assertEquals( self.store.findUnique(MessageQueue).messageCounter, MESSAGE_COUNT) def test_powerup(self): """ The L{MessageQueue} is still a L{IMessageRouter} powerup on its store after the upgrade. """ self.assertEquals( [self.store.findUnique(MessageQueue)], list(self.store.powerupsFor(IMessageRouter))) PK9F:ii+xmantissa/test/historic/test_mugshot1to2.pyfrom twisted.trial.unittest import SkipTest from axiom.test.historic import stubloader from xmantissa.people import Mugshot, Person class MugshotTestCase(stubloader.StubbedTest): def testUpgrade(self): try: from PIL import Image except ImportError: raise SkipTest('PIL is not available') m = self.store.findUnique(Mugshot) p = self.store.findUnique(Person) self.assertIdentical(m.person, p) self.assertEqual(p.name, 'Bob') img = Image.open(m.smallerBody.open()) self.assertEqual(img.size, (m.smallerSize, m.smallerSize)) PK9FK+xmantissa/test/historic/test_mugshot2to3.py""" Tests for L{Mugshot}'s version 2 to version 3 upgrader. """ from twisted.trial.unittest import SkipTest from axiom.test.historic.stubloader import StubbedTest from xmantissa.people import Mugshot, Person from xmantissa.test.historic.stub_mugshot2to3 import ( MUGSHOT_TYPE, MUGSHOT_BODY_PATH_SEGMENTS) class MugshotUpgraderTestCase(StubbedTest): """ Tests for L{Mugshot}'s version 2 to version 3 upgrader. """ def setUp(self): """ Skip the tests if PIL is unavailable. """ try: import PIL except ImportError: raise SkipTest('PIL is not available') return StubbedTest.setUp(self) def test_attributesCopied(self): """ The C{person}, C{smallerBody} and C{type} attributes of L{Mugshot} should have been copied over from the previous version. """ from PIL import Image mugshot = self.store.findUnique(Mugshot) self.assertIdentical(mugshot.person, self.store.findUnique(Person)) self.assertEqual(mugshot.type, MUGSHOT_TYPE) self.assertEqual( mugshot.body, self.store.newFilePath(*MUGSHOT_BODY_PATH_SEGMENTS)) # mugshot.body should be untouched, it should have the same dimensions # as test/resources/square.png (240x240) self.assertEqual(Image.open(mugshot.body.open()).size, (240, 240)) def test_smallerBodyAttribute(self): """ L{Mugshot.smallerBody} should point to an image with the same dimensions as the current value of L{Mugshot.smallerSize}. """ from PIL import Image mugshot = self.store.findUnique(Mugshot) self.assertEqual( Image.open(mugshot.smallerBody.open()).size, (mugshot.smallerSize, mugshot.smallerSize)) PK9F0v!-xmantissa/test/historic/test_organizer2to3.pyfrom axiom.test.historic import stubloader from xmantissa.people import Organizer, Person, RealName from xmantissa.webapp import PrivateApplication class OrganizerTest(stubloader.StubbedTest): """ Test L{Organizer}'s 2->3 upgrader. """ def test_storeOwnerPerson(self): """ Test that L{Organizer.storeOwnerPerson} is set to a L{Person} with no L{RealName}, since there isn't enough information in the store to construct one. """ o = self.store.findUnique(Organizer) self.assertIdentical(o.storeOwnerPerson, self.store.findUnique(Person)) self.assertEqual(self.store.count(RealName), 0) def test_webTranslator(self): """ Test that L{Organizer._webTranslator} is preserved across versions. """ o = self.store.findUnique(Organizer) self.assertIdentical(o._webTranslator, self.store.findUnique(PrivateApplication)) PK9F0]1xmantissa/test/historic/test_passwordReset1to2.pyfrom axiom.test.historic import stubloader from xmantissa.signup import PasswordReset class PasswordResetTestCase(stubloader.StubbedTest): def testUpgrade(self): self.assertEqual(self.store.count(PasswordReset), 0) PK9F=BB*xmantissa/test/historic/test_people1to2.pyfrom axiom.test.historic import stubloader from xmantissa.people import Organizer from xmantissa.webapp import PrivateApplication class OrganizerTest(stubloader.StubbedTest): def testUpgrade(self): o = self.store.findUnique(Organizer) self.failUnless(isinstance(o._webTranslator, PrivateApplication)) PK9Fdd*xmantissa/test/historic/test_person1to2.py """ Tests for the upgrade from schema version 1 to 2 of the Person item which adds the I{vip} attribute. """ from xmantissa.test.historic.stub_person1to2 import NAME, CREATED from axiom.test.historic.stubloader import StubbedTest from xmantissa.people import Organizer, Person class PersonUpgradeTests(StubbedTest): def test_attributes(self): """ Existing attributes from the L{Person} should still be present and the new C{vip} attribute should be C{False}. """ organizer = self.store.findUnique(Organizer) person = self.store.findUnique( Person, Person.storeID != organizer.storeOwnerPerson.storeID) self.assertIdentical(person.organizer, organizer) self.assertEqual(person.name, NAME) self.assertEqual(person.created, CREATED) self.assertEqual(person.vip, False) PK9FS\**xmantissa/test/historic/test_person2to3.py """ Tests for the upgrade from schema version 2 to 3 of the Person item which changed the I{name} attribute from case-sensitive to case-insensitive. """ from xmantissa.test.historic.stub_person2to3 import NAME, CREATED from axiom.test.historic.stubloader import StubbedTest from xmantissa.people import Organizer, Person class PersonUpgradeTests(StubbedTest): def test_attributes(self): """ Existing attributes from the L{Person} should still be present and have the same value as it did prior to the upgrade. """ organizer = self.store.findUnique(Organizer) person = self.store.findUnique( Person, Person.storeID != organizer.storeOwnerPerson.storeID) self.assertIdentical(person.organizer, organizer) self.assertEqual(person.name, NAME) self.assertEqual(person.created, CREATED) self.assertEqual(person.vip, False) PK9F 2 schema upgrade. """ port = self.store.findUnique(TCPPort) self.assertEqual(port.portNumber, TCP_PORT) self.assertTrue(isinstance(port.factory, SiteConfiguration)) self.assertEqual(port.interface, u'') def test_SSLPort(self): """ Test the SSLPort 1->2 schema upgrade. """ port = self.store.findUnique(SSLPort) self.assertEqual(port.portNumber, SSL_PORT) self.assertEqual(port.certificatePath, self.store.newFilePath('certificate')) self.assertTrue(isinstance(port.factory, SiteConfiguration)) self.assertEqual(port.interface, u'') PK9F=z6xmantissa/test/historic/test_privateApplication2to3.py from axiom.test.historic import stubloader from xmantissa.ixmantissa import IWebViewer from xmantissa.publicweb import CustomizedPublicPage from xmantissa.webapp import PrivateApplication from xmantissa.website import WebSite from xmantissa.webgestalt import AuthenticationApplication from xmantissa.prefs import PreferenceAggregator, DefaultPreferenceCollection from xmantissa.search import SearchAggregator class PATestCase(stubloader.StubbedTest): def testUpgrade(self): """ Ensure upgraded fields refer to correct items. """ pa = self.store.findUnique(PrivateApplication) self.assertEqual(pa.customizedPublicPage, pa.store.findUnique(CustomizedPublicPage)) self.assertEqual(pa.authenticationApplication, pa.store.findUnique(AuthenticationApplication)) self.assertEqual(pa.preferenceAggregator, pa.store.findUnique(PreferenceAggregator)) self.assertEqual(pa.defaultPreferenceCollection, pa.store.findUnique(DefaultPreferenceCollection)) self.assertEqual(pa.searchAggregator, pa.store.findUnique(SearchAggregator)) self.assertEqual(pa.website, pa.store.findUnique(WebSite)) def test_privateKeyDoesntChange(self): """ Verify that upgrading the PrivateApplication does not change its privateKey attribute. """ pa = self.store.findUnique(PrivateApplication) oldStore = self.openLegacyStore() oldPA = oldStore.getItemByID(pa.storeID, autoUpgrade=False) self.assertEqual(oldPA.privateKey, pa.privateKey) def test_webViewer(self): """ At version 5, L{PrivateApplication} should be an L{IWebViewer} powerup on its store. """ application = self.store.findUnique(PrivateApplication) interfaces = list(application.store.interfacesFor(application)) self.assertIn(IWebViewer, interfaces) PK9FaM M 6xmantissa/test/historic/test_privateApplication3to4.py """ Tests for the upgrade of L{PrivateApplication} schema from 3 to 4. """ from axiom.userbase import LoginSystem from axiom.test.historic.stubloader import StubbedTest from xmantissa.ixmantissa import ITemplateNameResolver, IWebViewer from xmantissa.website import WebSite from xmantissa.webapp import PrivateApplication from xmantissa.publicweb import CustomizedPublicPage from xmantissa.webgestalt import AuthenticationApplication from xmantissa.prefs import PreferenceAggregator, DefaultPreferenceCollection from xmantissa.search import SearchAggregator from xmantissa.test.historic.stub_privateApplication3to4 import ( USERNAME, DOMAIN, PREFERRED_THEME, PRIVATE_KEY) class PrivateApplicationUpgradeTests(StubbedTest): """ Tests for L{xmantissa.webapp.privateApplication3to4}. """ def setUp(self): d = StubbedTest.setUp(self) def siteStoreUpgraded(ignored): loginSystem = self.store.findUnique(LoginSystem) account = loginSystem.accountByAddress(USERNAME, DOMAIN) self.subStore = account.avatars.open() return self.subStore.whenFullyUpgraded() d.addCallback(siteStoreUpgraded) return d def test_powerup(self): """ At version 4, L{PrivateApplication} should be an L{ITemplateNameResolver} powerup on its store. """ application = self.subStore.findUnique(PrivateApplication) powerups = list(self.subStore.powerupsFor(ITemplateNameResolver)) self.assertIn(application, powerups) def test_webViewer(self): """ At version 5, L{PrivateApplication} should be an L{IWebViewer} powerup on its store. """ application = self.subStore.findUnique(PrivateApplication) interfaces = list(self.subStore.interfacesFor(application)) self.assertIn(IWebViewer, interfaces) def test_attributes(self): """ All of the attributes of L{PrivateApplication} should have the same values on the upgraded item as they did before the upgrade. """ application = self.subStore.findUnique(PrivateApplication) self.assertEqual(application.preferredTheme, PREFERRED_THEME) self.assertEqual(application.privateKey, PRIVATE_KEY) website = self.subStore.findUnique(WebSite) self.assertIdentical(application.website, website) customizedPublicPage = self.subStore.findUnique(CustomizedPublicPage) self.assertIdentical( application.customizedPublicPage, customizedPublicPage) authenticationApplication = self.subStore.findUnique( AuthenticationApplication) self.assertIdentical( application.authenticationApplication, authenticationApplication) preferenceAggregator = self.subStore.findUnique(PreferenceAggregator) self.assertIdentical( application.preferenceAggregator, preferenceAggregator) defaultPreferenceCollection = self.subStore.findUnique( DefaultPreferenceCollection) self.assertIdentical( application.defaultPreferenceCollection, defaultPreferenceCollection) searchAggregator = self.subStore.findUnique(SearchAggregator) self.assertIdentical(application.searchAggregator, searchAggregator) self.assertIdentical(application.privateIndexPage, None) PK9FnS6xmantissa/test/historic/test_privateApplication4to5.py """ Tests for the upgrade of L{PrivateApplication} schema from 4 to 5, which was dropping the 'hitCount' variable and adding the L{IWebViewer} powerup. """ from xmantissa.test.historic.test_privateApplication3to4 import ( PrivateApplicationUpgradeTests) class PrivateApplicationUpgradeTests5(PrivateApplicationUpgradeTests): """ Tests for L{xmantissa.webapp.privateApplication4to5}. """ PK9FCn3xmantissa/test/historic/test_pyLuceneIndexer3to4.py from axiom.test.historic import stubloader from xmantissa.fulltext import PyLuceneIndexer class PyLuceneIndexerTestCase(stubloader.StubbedTest): def testUpgrade(self): index = self.store.findUnique(PyLuceneIndexer) self.assertEqual(index.indexDirectory, 'foo.index') # we called reset(), and there are no indexed items self.assertEqual(index.indexCount, 0) self.assertEqual(index.installedOn, self.store) PK9Fyt"??3xmantissa/test/historic/test_pyLuceneIndexer4to5.py# Copyright 2005 Divmod, Inc. See LICENSE file for details from axiom.test.historic import stubloader from xmantissa.ixmantissa import IFulltextIndexer from xmantissa.fulltext import PyLuceneIndexer class PyLuceneIndexerTestCase(stubloader.StubbedTest): def testUpgrade(self): """ The PyLuceneIndexer should be findable by its interface now. It also should have been reset since it was most likely slightly corrupt, with respect to deleted documents. """ index = IFulltextIndexer(self.store) self.failUnless(isinstance(index, PyLuceneIndexer)) self.assertEqual(index.indexDirectory, 'foo.index') # we called reset(), and there are no indexed items self.assertEqual(index.indexCount, 0) self.assertEqual(index.installedOn, self.store) PK9Fo[ԥ,xmantissa/test/historic/test_realname1to2.py """ Tests for the upgrade from schema version 1 to 2 of RealName, which deletes the item. """ from axiom.test.historic.stubloader import StubbedTest from xmantissa.people import Person, RealName class RealNameUpgradeTests(StubbedTest): def test_deleted(self): """ The L{RealName} should no longer exist in the database. """ self.assertEqual(self.store.query(RealName).count(), 0) PK9F21xmantissa/test/historic/test_remoteIndexer1to2.py from axiom.test.historic.stubloader import StubbedTest from axiom.batch import processor from xmantissa.fulltext import HypeIndexer, XapianIndexer, PyLuceneIndexer from xmantissa.test.historic.stub_remoteIndexer1to2 import StubSource class RemoteIndexerTestCase(StubbedTest): """ Test that each kind of remote indexer correctly becomes associated with an item source when being upgraded to version two. """ def testUpgradeHype(self): indexer = self.store.findUnique(HypeIndexer) self.assertEquals( [self.store.findUnique(StubSource)], list(indexer.getSources())) def testUpgradeXapian(self): indexer = self.store.findUnique(XapianIndexer) self.assertEquals( [self.store.findUnique(StubSource)], list(indexer.getSources())) def testUpgradePyLucene(self): indexer = self.store.findUnique(PyLuceneIndexer) self.assertEquals( [self.store.findUnique(StubSource)], list(indexer.getSources())) PK9F1xmantissa/test/historic/test_remoteIndexer2to3.py from axiom.test.historic.stubloader import StubbedTest from axiom.batch import processor from axiom.iaxiom import REMOTE from xmantissa.fulltext import HypeIndexer, XapianIndexer, PyLuceneIndexer from xmantissa.test.test_fulltext import FakeMessageSource class RemoteIndexerTestCase(StubbedTest): """ Test that the upgrade from 2 to 3 does not drop any information and that it resets the indexer so that the changes to the indexing behavior gets applied to all old messages. """ def setUp(self): # Grab the FakeMessageSource and hold a reference to it so its # in-memory attributes stick around long enough for us to make some # assertions about them. result = StubbedTest.setUp(self) self.messageSource = self.store.findUnique(FakeMessageSource) return result def _test(self, indexerClass): indexer = self.store.findUnique(indexerClass) self.assertEquals( [self.messageSource], list(indexer.getSources())) # Make sure it got reset self.assertIn((indexer, REMOTE), self.messageSource.added) self.assertIn(indexer, self.messageSource.removed) def testUpgradeHype(self): return self._test(HypeIndexer) def testUpgradeXapian(self): return self._test(XapianIndexer) def testUpgradePyLucene(self): return self._test(PyLuceneIndexer) PK9FYI0xmantissa/test/historic/test_searchresult1to2.py from axiom.test.historic.stubloader import StubbedTest from xmantissa.search import SearchResult class SearchResultUpgradeTestCase(StubbedTest): def test_removal(self): """ SearchResults are no longer necessary, so the upgrade to version two should delete them completely. """ self.assertEquals( list(self.store.query(SearchResult)), []) PK9Fb,xmantissa/test/historic/test_settings1to2.pyfrom axiom.test.historic import stubloader from xmantissa.settings import Settings class SettingsTestCase(stubloader.StubbedTest): def testUpgrade(self): self.assertEquals(self.store.count(Settings), 0) PK9FmI.xmantissa/test/historic/test_statBucket1to2.py from axiom.test.historic import stubloader from xmantissa.stats import StatBucket class FreeTicketSignupTestCase(stubloader.StubbedTest): def testUpgrade(self): for bucket in self.store.query(StatBucket): self.assertEqual(bucket.type, "axiom_commits") PK9FzOĺOO*xmantissa/test/historic/test_ticket1to2.pyfrom axiom.test.historic import stubloader from xmantissa.signup import Ticket from xmantissa.product import Product from xmantissa.webadmin import AdministrativeBenefactor class TicketTestCase(stubloader.StubbedTest): def testTicket1to2(self): """ Make sure Ticket upgrades OK and has a Product corresponding to the old AdministrativeBenefactor. """ t = self.store.findUnique(Ticket) self.failUnless(isinstance(t.product, Product)) self.assertEqual(t.product.types, AdministrativeBenefactor.powerupNames) PK9F&)2xmantissa/test/historic/test_userInfoSignup1to2.pyfrom axiom.test.historic import stubloader from xmantissa.signup import UserInfoSignup class FreeTicketSignupTestCase(stubloader.StubbedTest): def testUpgrade(self): fts = self.store.findUnique(UserInfoSignup) self.assertEqual(len(fts.product.types), 6) PK9FFww,xmantissa/test/historic/test_userinfo1to2.py """ Test the upgrade of L{UserInfo} from schema version 1 to 2, which collapsed the I{firstName} attribute and I{lastName} attribute into a single I{realName} attribute. """ from axiom.test.historic.stubloader import StubbedTest from xmantissa.signup import UserInfo from xmantissa.test.historic.stub_userinfo1to2 import FIRST, LAST class UserInfoTests(StubbedTest): def test_realName(self): """ L{UserInfo.realName} on the upgraded item should be set to the old values for the I{firstName} and I{lastName} attribute, separated by a space. """ infoItem = self.store.findUnique(UserInfo) self.assertEqual(infoItem.realName, FIRST + u" " + LAST) PK9FA33+xmantissa/test/historic/test_website3to4.py """ Test for upgrading a WebSite by giving it a hostname attribute. """ from xmantissa.test.historic.test_website4to5 import WebSiteUpgradeTests # Subclass it to make a TestCase with a __module__ which won't confuse trial # and to make stub discovery work correctly. These two things are implicitly # discovered from the class definition are unfortunate. -exarkun class WebSiteUpgradeTests(WebSiteUpgradeTests): # This website was so old, it had no hostname information. So a default # will be filled in. -exarkun expectedHostname = u"localhost" PK9F;gB+xmantissa/test/historic/test_website4to5.py """ Test for upgrading a WebSite to move its TCP and SSL information onto separate objects. """ from twisted.application.service import IService from twisted.cred.portal import IRealm from nevow.inevow import IResource from axiom.test.historic.stubloader import StubbedTest from axiom.dependency import installedOn from axiom.userbase import LoginSystem from xmantissa.port import TCPPort, SSLPort from xmantissa.web import SiteConfiguration from xmantissa.website import WebSite from xmantissa.publicweb import AnonymousSite from xmantissa.ixmantissa import IMantissaSite, IWebViewer from xmantissa.test.historic.stub_website4to5 import cert class WebSiteUpgradeTests(StubbedTest): expectedHostname = u"example.net" def test_preservedAttributes(self): """ Test that some data from the simple parts of the schema is preserved. """ site = self.store.findUnique(SiteConfiguration) self.assertEqual(site.httpLog, self.store.filesdir.child('httpd.log')) self.assertEqual(site.hostname, self.expectedHostname) def test_portNumber(self): """ Test that the WebSite's portNumber attribute is transformed into a TCPPort instance. """ site = self.store.findUnique(SiteConfiguration) ports = list(self.store.query(TCPPort, TCPPort.factory == site)) self.assertEqual(len(ports), 1) self.assertEqual(ports[0].portNumber, 8088) self.assertEqual(installedOn(ports[0]), self.store) self.assertEqual(list(self.store.interfacesFor(ports[0])), [IService]) def test_securePortNumber(self): """ Test that the WebSite's securePortNumber attribute is transformed into an SSLPort instance. """ site = self.store.findUnique(SiteConfiguration) ports = list(self.store.query(SSLPort, SSLPort.factory == site)) self.assertEqual(len(ports), 1) self.assertEqual(ports[0].portNumber, 6443) certPath = self.store.newFilePath('server.pem') self.assertEqual(ports[0].certificatePath, certPath) self.assertEqual(certPath.getContent(), cert) self.assertEqual(installedOn(ports[0]), self.store) self.assertEqual(list(self.store.interfacesFor(ports[0])), [IService]) def test_deleted(self): """ The L{WebSite} should no longer exist in the site store. """ self.assertEqual(list(self.store.query(WebSite)), []) def test_anonymousSite(self): """ An L{AnonymousSite} is created and installed on the site store. """ resource = self.store.findUnique(AnonymousSite) self.assertEqual(list(self.store.interfacesFor(resource)), [IResource, IMantissaSite, IWebViewer]) self.assertIdentical(installedOn(resource), self.store) self.assertIdentical(resource.loginSystem, IRealm(self.store)) def test_singleLoginSystem(self): """ The upgrade should not create extra L{LoginSystem} items. """ self.assertEqual(self.store.query(LoginSystem).count(), 1) def test_userStore(self): """ Test that WebSites in user stores upgrade without errors. """ ls = self.store.findUnique(LoginSystem) substore = ls.accountByAddress(u'testuser', u'localhost').avatars.open() d = substore.whenFullyUpgraded() def fullyUpgraded(ignored): web = substore.findUnique(WebSite) self.assertEqual(web.hitCount, 321) return d.addCallback(fullyUpgraded) def tearDown(self): d = StubbedTest.tearDown(self) def flushit(ign): from epsilon.cooperator import SchedulerStopped self.flushLoggedErrors(SchedulerStopped) return ign return d.addCallback(flushit) PK9F+}+xmantissa/test/historic/test_website5to6.py """ Tests for upgrading a WebSite to move the L{IProtocolFactoryFactory} parts onto a separate item. """ from xmantissa.test.historic.test_website4to5 import WebSiteUpgradeTests # Subclass it to make a TestCase with a __module__ which won't confuse trial # and to make stub discovery work correctly. These two things are implicitly # discovered from the class definition are unfortunate. -exarkun class WebSiteUpgradeTests6(WebSiteUpgradeTests): pass PK9FPezz0xmantissa/test/historic/addPerson1to2.axiom.tbz2BZh91AY&SYޭˆj0-jP4dHSFF4 =AOMd4`4 `ic44 &hѠC44 &hѠAD&e=SShhh4H4byK,S0[!S7tPaM(+82eeȺӎ=>J!K *pB:`UyKhSo\oA)vX%ꈦf3 2uDwjNoWB#ck # ]K̪dWjgGH>9+d.c?S_^'u7؄W"ԏ|CX@I,QeP prESO\!#@], +(l?hLbMl|)}3j1JPnU rHKQ܍\|3h2,ڛFٝz]ƽJp3ݩ; ~!ob%1F/x~aHSF}k)e$!SZUxOpx{ӏeчCp:u'mi8Pb h&arߩ IK56`,FWM&ZM;(Im(P¯i",3P402C"(;9f$~bB"D|`ތ'EbX~,"8`"C[CZvgG:^<q$?gA`RNmoi6tEl>x sI.ߩ2ߦdL h z]$܍|FNx D/x7”*RDЕA0%H h U ȊЄhS43B AYN%L H|2V]<)41'|0Z DZ4*j_WDPW[,/#+t0 [lЌa+ +'A; mQL: Z@cө `̆&k $nptV əYͅ".rhC֜D}DSK#3Z&CZ p5z|Xv WH%JDED(Rqm1N 340I$.p H>PK9FhԳ 7xmantissa/test/historic/ampConfiguration1to2.axiom.tbz2BZh91AY&SYW8 ˄F _P#DXH_+ahSzCOIPM4'F h@44444UOЦbjm2ChhѠ@i iɣ&##`4h2i@ѣbh@h4 4 4фC4dѓ0CF4 CF4ɠhш14di4M AM#AҚhɑ!0H 范DI! yC#bh=@zAih MוJ]B1ћ`{^Vq䌴hPJ#P 20,yh tZ8#BtYV=l\{oh=%-:ky7ävОrxO#Z}x3OEy}BS~(_ehX1yL1Q|.*xZP/JV6At oBլ\!kCL]Ce\#eHF(+Ro5`AAmM}C>4 6^MKblp>2cQ&h>h~&x-NwC֌[!%@hZ$H6z>S(E&>0 ^'6A0 3of$ZW3 kwL@pd"!hlҤ^HXmhEbGIX"o&XXg]HΉQjC۱66݇ ??,0F|~`4,f< 0R$4 i@5y^"mВ, bsCleȂ6Rh3rhM%HNcfC?ϖ,N{M6ۍTBH$s_KTziOFRʛ"1Ya:λYia z2(Ev0YS3Ze Qg!,*])7$cQkY1E)zz7e)=Gy罿L u6 溾}?'q)C1q#ȓ5 p³LADqN$3);e:5V+ 8ӺЄJs! Q'yJQh56+I ,>6h&AMOK#'!ZnOAj>G<;-kq5PbR78zvUGa& pj -R՜AE V$)7%dweAl-Caw]]s}Y=ß^IL} }{CԘo\N-րx NR D$j)XB#q|)y}hlcʱ9\hA61ԩD:$DJ^m.ƷͧvX$U֯Ui9 }Jg m麘2qʍ?$x=Cmm(m-t/> 2b #2f2OFJnՀt166Mn[ž%P"D#[Kj0X^yHB/3$vmO-a"+4ZB(aCHRe,R` [V8-i>7 +s@T-X# !\AH801,f᫾~azaRɒVjA BIDR.XsCKl &uQ~vT0X\JĴ%8I(nPP0p%6[<'F(_%*Y%b DQ5wjԍ9Ī#CcmrkǯcNs_< VۃCL` c]Ff aG"VfRjN8h4)0>{ 0>11=l3?c4)F ¾ef +)E5֑i6:(H mۊvbږt]={*pML.6Ƙ4ۍ_u %-@rC0wL [X]8aKySEsSKeCZXK e6HlLP,Xf($ +p*(I jyB.^.Iؖts74bb#( ZT[ԩ4HGB\Md;{m}Fs=K#-^Y_ Q'Dlf2%hGiD$j4`}@a\z,vDH#jsH]TԶ- 8:g]F53Aul<@oMZ$_f? CLH`&H`dÆZJfXjADRh퀊b2P"Q$  H 2 *sPBI LbQR`ăZKWƑt݉ , Jƕ #]1ۍUTpYAɘ͢e Kރ|Bi*u'~B' -d<"wמ(K9.RMXLlY m gq"=!^3tDVt-EPiF*H#X#=2et!y&n9UϩhЀbhT1bmD7-3u;dLEV.rU^%MDĵPlamflRx`8?GY`Cp@ fQ*@_ޅ5s7LG}xpzɎ9>7Rmkeހz! 0PA (bi 14/N_-)„hPK9F2ԯBxmantissa/test/historic/defaultPreferenceCollection1to2.axiom.tbz2BZh91AY&SYJj0D* P~р ҟ@M4ѡ @CjaDdi 2 @dƆ @0CF@ @d"5j5'5=MP 2cP=CѣSbo Ju}Đ< (yvDP)HiN 2 NMZBF8p]IzgKt @-eB(8X[SNnI#2.Y{ ֡ eJm  mxƳrh k& 3Y4@h<9a/}F9 J?fISw[IT!~[i ı UEĈ9<y'ċtbRLs3{l_b6 0k}tؐ-BY"J\AFy*\-82`%TaDA& T@VƞP۵'{ݍdrYT٠Iy/2A~FNDe8T÷l @\4^h.h\㈪c0x 9HM;-BdľR&)\[*@ / MZ矟‘X5\QȗT]d\}  QpH/YCZhF1̉ru*P &DU-t6u݂f~( D`~aM}5͓ȜI9I!h1L$q!Sa*= O[O< B͏Ȏu3ɰo&"mc[  F!EyHDoQ."Ki% }=$lCq2\C'h"nnpkcȎ~F RX`V]B@<׼PK9F.;xmantissa/test/historic/developerapplication1to2.axiom.tbz2BZh91AY&SYM*4JBLJ0 @h BLL < 4icSOP 2hhڌ$iPIif@' pd4ѓ DO)dڃ@4UkD7~.G1u` ٲXQkD M{&:s9 AEQaMCׂKv'0^{֭\t=&Ҳffa9^ErM6;ֈF)ckzzKt;x:Z_F"p7A4;Cq}hdXflr#>yv>! xɓ&KC F!`"v* FFܣvH$GBt!\״i B8D$-=!`ʍr-CVџI2<Np 6\jD±5*r)C%$jigM33ZȥKbI'JA-q~5&C.qcTAgcPr"?%EpBaIgG޵o+m<Oi+Ft*d2y^ȑNnZ#0X[)۳d.H+ES oh$U$ u ;aEBQ/E)Oِ%mA9d1ոd TAZYAaQp3&f$#EWuv44L4Y ;^(7;V7yEJDPQu1 d]BA6PK9F K3xmantissa/test/historic/emailAddress1to2.axiom.tbz2BZh91AY&SY=v˹0P)P~hQБ56)yOS#iSz"z=@44h 6I0ښ4ѣF2h LHA4&FCTSaz2iM 4d FLJbR8!Of䐚 N;L{FXCg$kɗ.\yre޼(S{b1!=$c|7Z1:\HjnМpR}iN!"*NU䕉Y,X Nt0X걼ܧ"Fe=2I޸,٧~P !x*(`f=]5שgD-?]i8$DvJXH8e4hKi0NQs'X(݁t0H4lj2CuQ C$ahe0>_]6h- OF(7׍e&(B$ٗ||D&c  '/_n- [ Ko&@&*NJRj?zjR/ T,# R+ &g,<o!BE`UxHK{ އsq"1!A#'c7s4+tv9@~Ih0ؼSLy hPwP'+f&抨,H:_+D ! c [w8q jDOTFu$'qlu EB 4=l'QFZ%h "eNeU3L7W.{E)hVX u)ЍkU"b3߷yϊ.kvw94}4 ;w8۫/o9fU H"CЭԴΙLU.̛чV׽26ܰ@0{}2TA"X@NH[)ǩH.7XKF3Ԥ.ě¡wK@ :xĴϱMWeFvdm&2ɦ JYf]CA $D̶}JTA H+ctirW̜ Sغ7 wkULRYۚ;>6qu= =h.#-b@ĊZJ>tJP񢀹)]/%+uxWmi0%bT[ew}J K,ó^.DPR82Y2;ךm|ǁ2Qfr1hP@OGLi];Z!Jp׊ ۤ}2`Vh azqX C}(x b6E׹NzB)NF"! |t9H_Ӹ1;Jڤ31D; @L+0bE ٓT<%)Җs?͑z *@&1 :`3Eߵ)'I$b !meE B&]@.HhSK)Rhu)Azdg:݈jSųE WJ\v$R;vUz_m-]rn]te)MNr"O!ʩU!>HR wЬCnT'2fV% $@ofJVvoVXWQ &ftdWr"Efqg"HK oʒP̑v&&4".Qe;'09eoH.$ݤJ kaK!MisfɫZ"m "kHW^s/ @pnO8AX,ꧪ&^6.[țm *mrLY !wAIvA"(HNڀPK9FB7xmantissa/test/historic/freeTicketSignup5to6.axiom.tbz2BZh91AY&SY5=4#Ev(H$&Ly ,qˉ@h%k3K즌(˛T'[eZY46 𤁻c;`ՇK,I|!HJSKQC*Az蛮ADܢL5( ˓Tdm( BĴ|j|]G?5'`Z9v;):5_=y60rЍ 3;.R41:4X^W&K+dv@x-z+6(\(98qGK+/!džD0B#)SX+9)<ĠhiJJS0R- kᐐz%a+QuS\ω-ף+Ucpڪo0hŏ"U$Ă6~Gɯ/"["tE$DNp)|HC3{j=8}|yԁjcHJ`l-^T|DJFHv_RoJq;(LW4HH6FI2VJ x7'dí^Nc0c,L^ŻyؔaϷ6Lݹ BJ,X Br?M?][!i8Fkwx7(Jhf!c-o`b"3V2@nS\3 Aٜj1EXE(]B捴كS.rg\ EDBqE4PTHaI £$ZH_D)kR\P0fU*?4iLK[ 3R̐n-BceEBSJJH@ Rҗd[9%ZCHP Te%C4 ʮ]kJY3D T ;<{tP Ӏq 5%r bE6v^oG+1[ <(7sVUΞT) R2ү.|t2 9B|_hBh +k&klʒ\Xhi&N % bZ[㶋|{ҴL,I7ʯFEWb$QmNO4 $@4+"jH.\5I#i^uM@e*$l_F^D8 miȱ7X-Lj fA(ިN+!觞f_6$\؛C6P+ D"fG2IJbI b@g.p j)zPK9FK00xmantissa/test/historic/frontpage1to2.axiom.tbz2BZh91AY&SY`bJ0$ @2YP.̀H4M=Ld4h4=@h   2' &0& LL#h 4ѦL!048hdh@44$hhFTF4х=M=APhڃ=F=C&jM.Cˠ6Hdz:edk[] E10eY0nvbCq:*Fp3aO0~ۮ6k'QV6ƾ1gdyūf߈M"I DDD~:2"GpH`w~xHJ>ML !J N.ǘnG숥$α$*vߡq̇'+7ⅳ{Bj!RIBC LC a R7QstD!M!}CR/~HB)Chk7$<T7GXʎf` ):K/4 _1PhE,UYh"5=(b:9B*@Gd90P*S35DD.}XI8`kC9~5zg9id須I_C HzKGZ;=|(;0.ZE̘c%Ord19Ab4crI\*Y3-#X(o!˗XDq2l^0+,Gt,9T5!xD$1:BeI ɣ(u L^i:# =cp0y@#;W̸'uM%}xiL)x;f7 ň{Fsۈz6d"t-rl_EuQ@ 5bH!+ו]0fQ6ddiFP(B>d^g&pR gl}a󻃵@ %.p S2PK9F113xmantissa/test/historic/messagequeue1to2.axiom.tbz2BZh91AY&SYP4hťϴbeA^hdCBm!44zM=GA~=#'$iT=&Ѡ  &`LMb0L4рd&F0M!z 4Sa2F@h&`LMb0L4рd&F0M$ &LSLGF4I#@40 B4O]Dv_Ũ!:2TA<wu5oq|EAoc PNO_#P{K$I B%Y]E5Wc@ =n!n܎tz$r!}#SBHeiy09<_Z̑R܆^׬w֯ΖƑc~Qj_ 53"ұ-9^R\̎Etm,Զ 1Ҩ2.pՇSEB%C꩏ř )LՎj*W?_b7FU!^(,$vz~Wd8aEmG#KZz#j5#=ѥ= YkxypXd2D#nIMbb{;-NQ][ @]l-O0)<:3ۗ׏yn:Ll mi=BFe"BT+dPcR N"

L2,J(Fv<͕DD3^ki9xx+y`LT@dۢNP r 6I#2_Ho N!rT/)28 ($QJtmgkŒtf,Ƙ4 %jD)flҬEH-4z1vezĝ %p`B!^_0} pMBhVHRdtOr#u~s`,jd%ąBX1 ȹ]o6y8_B EFXy X,2$6pJ2q5laTX_ 4mH$fͭYLVTJS/饷la8YI#v0ߕ iH4c놀8NDRI)4 9DaUe*u]qՉE %Bc˓` ~kZE~nX K@%8MD![=څmit@Fg%Bb`оH,fX\V0 D>nxDq$Uޑ4XvZB냃XF=ڻ=4Z Fi%.Ӄ{"tYPqj:psi1Byp]E$sУO60 E@3D6DmI pFe2@ CcmX !!mMl$w$S OpPK9FhO ߡߡ.xmantissa/test/historic/mugshot1to2.axiom.tbz2BZh91AY&SY9E (PTB( P D@$PJ H$(HP  (2L0& LI`&RIM4dmOjziF2ie7щ zjh1=2)O`#d7!3MQ6=`4ٵN:"ܧG.I|I("C*D+(T,RĬwG(RD\RDE"HT%EHv'krtB%zPAoMJ:*ꪹJUWEH-Bh3*`ݫy$E'$vb0#({cJ!Gwiҥ(׵CtmQjȦΥDһc, |׫cNC1,n=ohQF\ܮ;t cZe%WԔEYQ&^'i#\:==3k-q\dqZDCPStktPNޫDs34j村Pvݚo=Sv0 'uu g 3ed-6vbԤR!gшQ-n +`Sg$ @sUC)}-K']gYoyLڦvw%S Mgق{_du^6%\V#x cYMէiTX_ھgrg;׶n*H8%Mrgu|Fį7|s5VGg [^M6U6mIJF;3ƶ;02†RBП3nӨE/OJ-='Lw#q(]rVk˒yTU*xV+{:hj>urӥG Z|ne#QuzQCu\9R}-gV![YfHFmfIqhbSfe bRUI`[5Yjgfi-(EJ6I4S SWS=/Š+-bsޡ1 c Bګ0pɬ rep޶]]6J@1ڐ7nk+ *I-ʪL+F$Ϛd2*p> &jB EطQ^Y!A^gjW7`{ş*KF|^b\6ͼNGBDuUu^՗Eo.2p!#H>. zvin'I4ʺZ/JԲr $R<.F!bֳ⒮Nsv5#@/l?M?N7à:Rnaťk ű松ˎ&Q6`@?K5ag]HInQVSUn锌7*7ƹj0H['H$7l1+*z2+h٩kիl%"46Ρ%o 8V!WߜEPw*:tLɹlD3j 9#1&:v3iۮ=2bȩjz1Cؚ+)ZH3+x3!\߆UEE8Nih 9.dNī*J2ik(8̙Tu]҆ej:";z*aZu|uMl`YgUUM0*\y`ԫ/ 2Ũ b,+zT8* ?I%,Jp9Igm}MCp%]g҄ Mr_9,Fd%eT)0~VvXDn(}L]GqдKOIZӭ{4()O&)yoILVe1._qr ( V`^v9oq4GpeeGNV6HƵ7y9]WFx^IkV`S7f!mS+kӶQ5VĝGcX-vUzP4QzX-ImQAt9FGXDAf7T䫪<2+j)Ҙ@ɒ\[<27K"x1h{ "P2oo%]kAS/[PXo^FyBFgT=^TZǯ3iҀtץX82D|8u%G`=N$L :І8J@6j^#sbkvز1(;h쁾"J r&&*܅ @4-*OC'f"O n&+jR,Jz!.ϲ(󖓥kb0C>9ΪȰ- XVoPaф\QKݶap]8YԵp~qFun^ى\EP-f4LFĥ`ѥ^gX$i,pD~dSu\VyL\]xtM_`U3s5XR4iTVDc.1;9C"*PO389Rrק ;ħ*Fօ)*jLK`(.# e&mtP5ch'u|S]I&]_[{GA~+VՊ[|OiOmISMU:xy=VgPg1ttQbTyIQd%@S9ERVSѷ[\A]EqJ\XF-]G1#nQe7Wk{kYpisSU@sd}SZe#qUyZ՝@MS\QXw!jUgODh9=Cy]Nv(vܖY:tDQje9o]P1 bJ eu`cH w)NyttUke^;-,E9G-tPb++!tI+V ֟39k$:J92JvPiY6VՅYY$U'!ϛ# M xN6Jſ*j=㬼2vG&q5[L]f[晷uPh5y_]GYgNAKJ\nѴLR慤f%XG\6Hdu:M]PT }ro~]1뻌>D0W^Ѷ hqǮƮ|{ ꅃ/*' s*)I[]w\D5dCGlBڗ]aX؍J˸.yް+/kT ٪\5=3(-R." :ϓ춧~?iԤl:RV4- !*2 =}mއn[1^[gkm#S[5rP}RAVhWQvZI-mfUoWz9baYǑ kE_ZQHhRGVSTN7L 6GGAMTe Tf0`U]vSEuZv߽ʿ*v,{P/_ƭ6ev-oFݷDiGIWWQlDXVwiw_Q2!AeC޵uj\uZt=pŊ]!%|Wy~WnWG1|qY\'\OO2ȋ#3JV2jPʪM'MKn9c٬,2.C*i " bB)s\BT(n?S ,i[K1~, #;Ot:~ؠϒ! ƶo[&<|,-j[®1 \) ;x= ZPE"Ku-Tԑh55OYy٥X^v![cQ SRHPٟQUt\6rսY{ѤYf[%GZxfil]ljw]lTyP9|g LSDL)#8|-~?cFa~W @, 2 x[?RzbL͒TY[gNJ[VUP)EZ}ѵ;4ʱܸ/ʼrʸ i3fܱ*0ic"bN +:ҒO7[?HPԹI;J1Ot9q`ƱJ 2Bٽ K3O>ȋ@#hˀ+C`)ҏ52`߰j3ʦV+J! j.$l 4(Ҡ4:?)^ b1o!s9F.i6?O^1KdlPf=RDQPV*`in5ag\tE]QՖnCT5qUqa6odIQS=P֕]ճojaDYٕ%ToWׁQ}p^7Rewބ1aAH2꣖ և Ȓf1h% kKMcmz۠Bӿlԭց0:=": 9)J~e"]Y![VmYOJEbD]ggfم}Bd9\e0pMP1l&cP4-]cTDQs}sqוm{mREL-uCc\5yIPrdEQjT|EYr;OqiWU=tJ7ht=hT2FRedd=:jםP^6)E}:J\%qdLqiqOZJVfKV C5w6cZ%lgMaF2rfXvONcRUEem5nߗ)^P!.B @=*L?ekQX]ZZz]wyEd4Ae q6QB$Mq. r6.LZXáo/ ^;jz@8b 6o><{:KGw T[F^vq^j$hX%AhF~_8װZƫ.c߱Ȋʢ>0jc nz=LZ:*8n*hb%+'o& mŒ:>ڽꑭn P޵+auwZQA(NپZ$|Y_u݇AYBWeHD]0]U \C}m"qF:qU)T})Q^gI~ץUSF nU٤s JОȍ+?͂h̍.=UsJ=f+]޳Ih?a\54OZ&teRA kյq^ƑND9G5pUNtiQ]ơSyQuhl$t&FhR'bNߟ+ [:IK<߲lеl-O\W͝]eWG-[}s=SM\Uuh5zMx0oc<=ZԩJM\ݴKC<+Ks5l[ ۺo-64KBOkޚ!-*ꢌ+ѣ+k:!!S`jٕ]U߅57~X6-ZMVX1o/,DK ,-SwFS xqlׅcۄA%-KmuM1eĥNTabxI5XEW8&dHe7j\ YiUb~PVn[W-yT.ڌnJt5-(,hѴԚ}Y0OM^vIk%Oƃ tءM> !삒h/ Kr2,: Nθ;q] F}N; 7>%eWuC~ͬ"/"2\"H)JdGᜈӲ۵#!-Z;ܨ ঩JRRqO RYYU*Clѵ]ZE ESv(}TCCzF_Uy$eA7bɄJeQd^ŸSU$U֫⁡Ipaf*#밲%\ˢ. lR>N.̺AZ^s:Fбk֧wa%k.Z6UvVyRӫnԻ1 VEDwt7mhO%1^ZU=E7~bM+YW6KL™{XI`ÈKm ]VfM&g$yYX&ymUױNY5PvS!]DE~u2WZ+SErfz%B3Ϲ:$4r櫫J z?IK:7IIC6$N'kk%KfVEKJ'|<笵=e.,(Ʒ-$!L.͛V(ˢJ(1ۊ- ޺;:-d8b̴bĹ7k.Sh#À,jjX)Cվ ; *#?b*><Ըϵέxb1Dc"(Ŀ|Na\Gl52\Ue]FZgEץ1r1\E{R% 3|RZkOq_g9kwt{7}`fWɰk#~: 0 Z°"H-{=o*^Vv2*CIzrH,2ÞWNCz3H6|f\0s2Ey~YsE[rfVUOwOZ!wWlaseizy`kQ&͐wQIJckٖ+LUDHU[:X]!}w qqYi\&{^yV2=H{"bePGU wgTU5F#4_V=12ܧ^C0٣c,L*N$;ҫ ď,{b ̪TXlS!Chc٧>@%;T *i%z(NB4*CթX2B\8Jv4E>6wWl<":sU\1T7eyuPTyG9l}]^$CHY6-ZzɄv_U1qߤQ p6Q3[%bP4v]mciS^1wĩ [Sp%g1SeY^WLж}RŰmӆJS-SjM^wqxSYD| uP %k7oШ"745\hJ֕ j"ü;4n#nʨ'I*4"TVTPE`U}Afpғ<+`>+?:95$"[6jtξʧx^ۿ)f!iN%&ܙM0.{Ѐ{QmK(ɴK#$c2i*d˸J,OڮTТڞI^`0h!3.1+L,/Cr)2z4.[خ=N!桡Kب3ҷ1I0'm;ϳ#0;Z5H?3\ʻ#3ZF&nL%[60l Bɢѿ/8Zݧnnޮ*23PȊj'wWzMV;J֍FTvIS&YYLCU(Nl3r& V.Ѯiz B|"ʺhGuhWE]eeU7qpZljD^7+`G oowhR~Ӥ 2Lb+ {0*dֹʶ*pficaqt}R]o~exy(yvy}PwINE'X_aE^ڇVt4y{R(jԤES5u j#3z)r*+-/Ҵ(zV2$8{ E" &iQT9M6*|U7gVWG(jp_T`'}}G!^yKkַCt"+ zɱT/z>L/1 b-l= 'o~u|E9jGݛrRd}mU6eFTnvqE4)Y_LM[[نɩE0cL0 ##4{2*0Ѹ2rQv'Ra\ǭWFTĕKGYYsamQmyi-WTI&z֑FkZdoUs,'iT潍bVIEQeO_V=ذ9Ϡ ȴ)9L̻SĩtM]ZдeOOtnBde\Uhe)L]&Nnl$I9Pw%18)^&<)#.kR6)"o"8,K ?ouYG%d-{pPWV[R&bPQ^ 'eQfCB5OyZ4,\xuR7Dכ'?(r&L"h&G%Qwl]A[ݷa8XuSn)jE yLԡyQMp|R%xP5B9\ҷ aжɍEնrSMx^ aW@d$k^yq5mh[aWtP6}n%ѭVihRewR_.*ٻ/4k0,`EYigamT1t(2Ρ2T,-;:Hd62Hd <: S&):hbIڥaNؕ%]E0@ӵyՉ>T4/ܹ֍J⻹,P䩡91734.oVUQdՐle'n+LkVϳo(!2 *MR7"+3"²4.$H# k/+O3|+mjJΒ( F+{4r ? ʿ8F8m8rF,̳t7 ꘭[;k^5z;lZEWQAnn%lOձL'U9%DVWimgbDp^~ʸ,zXi߉xgcmuYf ~=թc8I۔YeS-gMID9*FPǭdRVmqF1y-`Y%MDQ99SX* J^9J>ISaC$5cTY;Y%UM9AWfE{Y$uQ]WE{wbէ-k׷A/T]{SDJݔ 2PoVI`SbQVPdyykDnFLhՑz}x%mHhULS%1` jI{uEt[SdtA[4DRY$^sԇ v}gOgQ;r ީ鲠"Ljl7 ,:-k",-2NkP0 {l#^[ݳ &s>)î:H{$6~n("Xgũ2P4A`|]EUzٳL\4^vABytjҼ₍2(P\[IsZ\9RL_%WXT-Tf䑘vWɛkۤJV=T#ꫢNS71)À&λ䫌T9O2+lB(hJ.M;HR*.>2/I7)N\tɌyNͅnJΑv{81bDukUݡdgBX]pW P\"kRGZD\o'5[DZI% 6xݦ HdAbVG Ldoyu>`e5lPOl*-J*)' BOmKmi7JwyM_E=ihgR3z 2 Xѧ 6&+չ SN~fH#؃(.S?'²42ݡC*˔ [® .++Ϋ%8$i̴1b)4/ΚɊҪz>r4k24;,(K0Ꚛò)ü:;r'/44zr0Z&,Z@5j*PB,tIC'JaRa6M!Rqz,Rv(oޗYHe%WdTIyua.L Y]'*Xv6aNsՙ ]Z䪫 {ҙ- H,-j| f1fo$uj]?lеٺ(x5-p!s8N:ΡLZȡ*-kR^#/z(*7J8R~㢯"!i+ɵ/AW=jeYM$PƅzGGRf%ډtX7݉af7;J!C3/ "+$߲=øί +Kh"6ɴHh!̒3ҏ8 (+d,! ZT?.HC&3O) clٽ^8M 9l;,pq"_Mt+3R:'|9ΪR~mIEAnd=(YQF}Y3fu/pUbu5r]CUMhy#mp˒z~kN!Jj(Z&jlK:"ڲi 5 &*K Ì>MeEUuuw\fId!k=F%xVY8e]Xt>OK-qSmqWiRy:i3,$[b]sxT `tlQ[f~z^Wg[qXFfU!~ArwVueqZUX]¬/ZаH2,Hҷ jF>*.'͒2s`*LllJ3,9*jR1OKRj Z7 ˼.2`r2Î+%kW,K򱴍2զ[NѼqYusgeSpl]*kĥ)aEub&aLٮ/)0F:âj-ʇּwVU4HuGޔ'qP1+|U`@g<[T֩]N4 G)q}if\UaHrAYФnCӇb]Wgxe&hx]v?G+.JaPN腍OVԡ'IW4!kvk۴AzضMtՍNR%pDzdi]GZvA AR$`cZyMم{W`W]FduDE^jZTy+JvaTSTRӛп z,&9٫ՍU$tݰo,O-P; leAWC8KR#ƝzYD|V[R'=j)Fեlvk-m5QS61j[-R"l.2$r:6S * -HVj"12ɮN':9{+*m[)Sͯ0ӮJh2;*,L"9Hj(Ѧ JLر?LJ(`̵/JJ(֘!h?l21,Ȃ7)K༴l=R5c\ݳ򮪸̢d< .zP6)|{,ܑ3Š!Kƶ5:Bywф{7H28)ٶB@}tƹ + 29"߰#jf̣,[M3F/ڀVuY O~4ɣWNj_^DXENԽGҵilt\U%pmV^wtXܶlAc]'5m]m`t5tK٬P1f)@9{?HNr(9NP+BN'nkƐB.;\9 / SD#Ojt)JJĿi6Z NBor¹΂¨MJ.C l*^ Ūb¡0|Il:ŪgD')(·*S,ޣ<*.%΃ջ 'MZk *ԸHryZWEeֵiKl_mg_1lIӆ b^Xw ~YrצEzteVJ^7`EoveYhRyMbeӧB~ѨBVw=(bZ)vx[aaZS t]Ƀoe1_&mV&UKфZvQTWvAfQhXt!uwgLUV h)VG~k}-t[Ko@-tVѶ8Diet~*O*꽭Ҷ*[:%Jp|=BC;zM(Kjw)vwJI%+r%HF֙pLUYrJ\#bV9k7-^Z}T]vAINTUɓbPV=cڤ-R$uLGZW5GvRuXPE^5%RѵDUeC~ئU/h^e%R@rV D(]$9Ehr_DoԏY'_~e y7QVY^Y]%x-T\F]`ֆݎkʏ #KГ3; ƭS"$ #j>k ^3+ܕ2-*.̉1ףL1B26!jV˜[{,E& $"kgb2:9)ӧ&K(w߹>V*R?Ni+D}sY%ua1TէwYTM҅-[f'Lb5Nd7C,@̳^肈˳4"jݢiz:9k0UxE3\DOT$bfO ]і/+"nʼ<-*7ʠ#*Jz> h"+kz6< ;忩 t4k%APǍBhj6nc:o=j˄ȶISfR`u&2[Bs.3O=Z*m7˚ڕ:Q]s<0N64)[=,LޫTyv+ۗDU?5dL& >WwHT+OqCʹPP`\VݜB=JɫUqwFrh&bG%UsrSqjFuD TzHQ,IPZ"'J%&}V)P5Q=k>v7~z.x4+^.c(RJ$ \ѡ,j",#s ü:쑤B|Kgw [7 DYV3h}hP5s4m֡3| HWVStuP7I5s DpyR~^E&GuTŇJf)Yĕq79v\adGLٔUKX2zS&%eAV45KVeawery"bRA]վKV<&N|(Md\e=Uf)cU5xwaIRU_WG5 q}xѹ/hU/Gq⊢2oC3O( b0Ϥ: 20#ی ";2I , R*8 Бr̪(+$P˷K3KҖ/xBl>JIDۦAw% <@ nt5o=+$Kf̸Z9UѯAWѮl+3ɚ7 R^߮2j)/b| U)DU^cyqEXa;gVpYJ3EW-QUq]f sQcϊ #RVln:0m;/Ҷ!4( z2* FCf4s5.GlXgB柍fr[+ASA⬵GZ+˝NNl5̪н洎!hzj-?+L2{ 3!3P?kM&,l6˻L5*~˴训H4mRlԢ 2J>$:64KDĦ.$/ aTM1zMiY Jޡi~0ٵL=mjpl*L&L ҵ2ޱZ>/Jk86S آJ.˨/KF6-K,ȹK( B+;&-ZѮj2Mp0oj) :؂+>ZeTm|yho% ArmrQf e{XWL'9l\ME[6 @(Mb$*8N"^-)ҢoN2 JD.IC:*)K[f#\6Op$a+m^ VȺ ')C׵Ȣ+*~5L=. . ҋ;)kZ:%IC^" ? 0HL{ƭj(X; rHӔi5(38+扫RlKk$h漏?N`Y(z ה^R@8٦}|RYvT@} zJγಭ!LS ,I.ɵ)#[oz|"&J5C:/BčI>ǾrkZ,JjRG^tꞢtwlѪx(6^%$EcPպ,!}ĭp$~]mDOѠj>Ď:^/K0ی2; pYŵf_t]MDYq-Pf AݰhN7"J5[YE\7]/tӥMctea.!L,+;AηX,A@f wg 9[/jc!DxڧIFEƉ[X nنIUUrMZwuG/B9 ;,ܢK0 V ک.!"ժ,L&$pU9v&h1cvk=Acaq fՇQ2KwvSu rǵF6IGPd]GB_ 蒳*9+B0~W$JxQEAyYp_e U媨F,M@*YrmRkgd+zxN.*חaQ'}~ɴHB,)[2D߹ &+@>Mx0ZϫKBl6n9iB$3螧<9<&j- :zԦ;9nːMJڶ1EQ%+rjF-߲-0/QZ55σr!pdC:5:1Q(~'@`P}KQך &."a%D޻i}zw+iBϲضιfe9sV}zI3)⚛2h3/:"Tʽ. 9\$_s5%Eyߥ]~ZMS!H}T5U@#oĕ*d+>*zbs~Dn cb5a 2-Ծn=VNa} )Qn7u kԖA:xY$ ŝ{k)U_x7fVTq]&EQiƼ Sl߸*/O!N2jn7k;Ӭo4s 3ݤ B921f*>7)3>K4 T$$,2JB'JB,ۺʫ2H ҧ2΃T˫6moBF0bFzk:ZWQuS¹u~g!Snkӝ"XY]kEԻUKK& 0:/"":DѢ S"* v㡋ZJĤv/ޅ4JZ׿ ©Rr;h䌱͛>2x JD/[ &{Tֲj{8%(@8TЦ+*αtTֆP1iR1 K8H'DVof\WNpYCjXW XFZHڵ4݄y]d}dSIVZ4Of]y;B-̲t5#h϶rژ.JJ(:'J#;`(B,@`H'HayKu!MrYXԕ@եn%9ReyRJsB" hh{ΞH*~Եm#IT8 ;&, 9+ .˓;ͳ/.f++-ȳMZ$Lzkn+r(6c ngWfYpaqrY M'N@\[6\Eg|PG2eT c7i]`QUpe`Uݧ}Tt 5pV]6L%.MVqqmQwpEL$1 :$o㊨!꺙/J"!ڌ./l3΢Ĭ{87Ik. H&#Ǵ!M2ξ+lkx,,H"**;Pϼ3ľ2=iR0 ;8:r๼r& Ѧ* jɂ+kbﺯ3! /ܠʓ̡.˞kbڕr͠$Z!+:ܹ OKL[JŻy}YfMĥrW{ɧGatj%}pm5:yZWA Z6K%{~ Z&aXva~ߥpҥe]NJFbm¬?-*.%;&iֶ`E_Rĩ{buP)yVYqſK_t8xQ~@TU Q^7SǍNHVEF@WOSlFw%=l YiSW^*bUCPSCa}8zaTee6MQ%[W0]V'WU)ONԝ#rYPwYRwQ |Y^@&H'-@GE]d5uC$,Hz3dM`oD A ! v)"Ì"z4KB'S4J eF tF5.R&0>@#1X3ND5ĉlgAU$m R׷ylyIgWEe~Fҷ`RD)r^ѕJ^ԆdBVG\UǡXTt$QmAG2oG^vlUKUS[?kuqqv]fQG5(%3˺ :<ҫ b䮻ε1)* #H?HeRgejY$UU9|TD)nqyXi${W%X6|Q(xF]IvUu9GQv)TwtZF9h`噈nV VP Rtع R&)Kū. d!RR4iKr3)rTuVMWї͒H9FM|i)3jۆeY&XE4uߥM1~D-QG^f-nՔ2dAFO}hWQJaWn%mxܖnmcTF%~R}x7Qp@; Ĭ.2/{~:킀.<8*- 3B=,Ҡ;(C^2Z62(mL |_Wny~g:Y‚5Nv"󀶾K^J!S*5 ЋS6 k⁤c.'n%le5iVEǫr<1c| J)颪oo*^>O"~K+\ƶs$ǫd4 $*7 Z #- O$lN=I6钤6IC,8ꀭ+mJ)d¯J-C6c%b4:Lȧ2 2̰81 ˖1Lޫ/TVAuDg8J󺭴M#.34aK9QSf)JܗPayfЇkb^fYZV @mTHzۗ^XgFgWdKiW!?ZRGVוo~=eV_Dq|[D)HoW7iĩS%yJ_UvDGJWBXC~ץӄ*">ϬHcͰZ +R"1j[(4λV,۰5"y۵)nyytQUYP]סLTX_fkGabIYEpFBuqg^O[US}s\_ ;;jc8Kݰ+D?2M|(l|ڷI #߸[*­Z;8ƭH(kb.(" 7̛λ v 2*xıϺ(BD;Z6O*X+6* zvdiX6Dٖv䝀xMt]_ݤxjva_U[Ɖ"}_6dڇ)G|Qx L HuBӗ-YפC9pTֵ(lW!xZCkU1H$ѰL%)lԥ}|MYVq&lWiFD}Q#pM[[T<&)5ml# PHN/ H2!ˊ'KCᶉ"& 3^% ĺ;&%H2SP¢C^?i虪JZ6үB;L;.7hZ6/k~#ҵ 8l7ʻ %C\&Oxʚ:0-+ܨ Ѧ/ K0緪\׬kcN/,ʚ.M 4cǡ K+<0N <, #Ɨ&Z\-K2:ϰ5 X#mC+)+|K2V˨Ib.+l% hj$*%)$) "2t v&ڝk~${.iҶ*p/ D<4[7 ˸FVcf /+Rޣ+@ɢ;r$ܾ/:0+\J#Kj: ̝8- L .#P6 އor& vīHS)/:0 #ʥz' Dߪr+Is.=#|7iJ൭^Ͷ Ү;ư ㆇrH-0B)RbԳm8m;-Jz"컆*Ǝ.("),kj3k[HR6˱<'~i*8{Hk(  )'+@]dDY|fQaV4yTVMz#~ n~?ijt޷ sk{ui̛9mఏ[?jt'(r-'M,[bf0{MJ2J/*X)[z\>Kd鲭r[r-I$J+Ҁ?kj/:s/VþZ'Ol2JD&c"Mr@*B܈QMt{mp[NG?Y]-xRU{iMW)/C,`PX}R55 gޥtD6uMPALfIDսrn]VYUzmV ke+ruƁY-oUN6ɠm_uoSHIGmlf|oAfe_fmbU0fW koFwބI||5hug_OmG}M`PIDTzu~a$izżTX7anEdYu+]7Tؗ1yfu5q\nG!dIZAPA }wQ`_aq:MmiTd`GkW橀x%6FZ$sVe_DrO##j󮨲bJO⳺[ зM|.ڸʪ0< **9sD9+c ؎#N(0>LCp(HD R*'nSV$M˧- *xB4+s#*-&kܮY_t=Da[hUw]E-B ^) k!]iw\]Ayڂ۲ j8ڹIKj,:Β=JJү9L b:.%hs` nc' t=p^Q feJMEmhPǙCBS3`sʨ.&Ծ=.^ѹIHιh$ܐjxϩ(0HZܪ8Ϊr4b7B +B,(øzȚ7ٲ;l)+L롌4vZԳ'sȮ *$N$p>>Z.R 3xmS$ R>L'j+7c^c'+F.I$.Ĥ7sdW'w]%HYNTioYFxQ a$o\مW܄yoN'anx݆MXކ%{V7~𿽮ƈ3m-Kr$8JZĬBNo@(B&O:٪1Ib({R/(쥯;o#8Lĭ3;"r1 K2i3:j 3|6jn :C˪L4s mQfQ0ԭksͤ#.X')S[Uqjw1>K[ OGmH2XEuHfmUgV.Xͤj5}@euM%]%JdhS^4m N\z\-#`Piwj_TCF iWjT]WtEJT|qimX%|q >:7M3VRl׾b!3nƦ-|vMFeKޗ5a!PRfbf[zǥsZRxQG4KRQo ^\6U(m]Ny!j`v%G[@Pq(b$mnx;UBvMۖR^r}qJZTNP=v]yJԅ{c sp?hS< $*234s69Mb m;,'K <쭮K 9*H DZ+!**h#I{2j$(DeOe!M_!LT3BKp6,ViB 칮 8) ˡZCL LHH{K r 2(9C慰NCdϚ$ΫΓn`0 $H,1cƖ-+(2d%򆨫>7REmDfW7^d_enJյAfgU[l9X7ytP-_ Kav6qGѮbu^qIh\ͼx5I`5v+ZuD\\weQAR55kUr^CUe֡{VqSS\IIQU%u!*VV')jfK\eMJXCuM1ϻd%J`6,;=bjͨrMCx?h{,ΦK:'ɳ:0Zh8Z#br$-K 0OF'ad9yǭ zV5CФ nZE{D uB*tVg't4_ۖi]|5amjmֆ/c4Y6s]DwMFU'|fO6PI2<;2΃t*#/(*Z"C4/Z(>J:ڱ `!b2 ;n4ݷҪH(2?2|&+"8* BV4n#Ҷm3,(HH6lX7~H[ؖŴeYW]QFAIpP 򼮮CBPJx!"?r*&B;j8lR#s@*l 42() Kag!XskR Cs]ou\st]G&e[]pS\Ea%|T1WUyz6YlEBQMuжe&ySawZ5DVuTt)@jYivudQԡiAЦBe]XfSRi^sYE^@FY>so=rt6YY_)1S7fX%qQ1{LGArn_w4ZdZ61|NG[{HTx_`slWM~vBU ow3zѤF?x pf=]AJF_E_%}MSPܷXZ7Wv]4cRN!Curpe- Q^\ѥ]ZwuZ[pǹFdU^GI Sfl6 Whg2~)f}mU&y%՜TK]4U|]QUzGZGɘxHDEvDDXQ}PZ} HWIWDPZU4gD[5\}}1kXAB$nWNJ'TۢȂ(-86ijj4ļ8+&Λ,J,#d:*4h> )qypܦxW ޲J\#ڼڨ$n*fiC".))6;5,e5=KT%TxQVV[ujd)`SQrVvvlG]sgL&9+m-5i1sLRH[ե{tU#֎$rB6 #F%l!rJ*T# k؆[0 r(+4t1ֺ"IÎŻk2T3M;&( h*8߲j(r`2Kr"B/hJcB̟9(V=mҒLzJ, ê rO{nBڦS&h)b& |) :1ϵάBn&+"k" J,ĮlC8hb׽--SWwٖw^ ^kRej$GC-[uX-saH_Jb5tWDtPE1yP9EUVuTه%aVjwcg)Fd]C5OhyqU@oFVSl]dQDA_DqRTֽDXu`VXmwьSلE?dtcQib1DGpPTP] t'-'}"UETfJS֡\҆ dFyu}&[UST5KAGH%XK^wF*MN&CS& ~rXj SƸ. 2 &hZ3M{.OTYDqIPUdfP[Wu_ISmoR QI4"|$Vd-z_j Q'w:t^9]4ihE9mX䭅fUl^kYkEK5HFisD!qm}~ AY|ŅM_5@L3<У/%n:k֨=N»n̺҃ C-#|!,S8.Br鵪ڲOJ89 Έ#͂J%% z ϣhRhJJ1+;{vϱKsJ\:m3L9:;.sLC*Zh32C;֢MۺKR:`j89j+*3+z֤ .k.::b#36̗-)L &DSr[WqӶuPPiLI{wԗP]mfWvqR|gad_aw&stQ+]gcf{Weч\4YU5InMVNUeh_U(Fsv%|]:NӗYkRit4h7(e5D)z"*̃<%#T-o 1̵Zl>jǦ3(K|;i#P4,s! 8ġ˚ «:JϪ+ɋ2jΊJ8B긭J:N#HRXB:躨 L& ޭ:OsR->ݾ*۰ *.KD3n#>c23ϭn(x'~>/Mz,.:Ӻ8*B/`)׽/*;$¿ҳ ٺ 46\޿+/j:4;*jN+,!Zdݦ+L:&4Jz^%MȰ0I8*~*OR^S,!j:?K"΄JF)+l+˸"(i ,0 jI9 ʄ2 J2(?#">*:?*r*ıh֠Cĸm3HB鳍;jF!m.J權xM2⳹ʌi[N0B% 0+~n"61ɋ*O0N;^7j^*nݧj+2>;F% 󱈣sJ6n|) C(ҽhS=K^#J.ɛ4;j[2*ȿ#M+4[(# :켯dA%PޗWa\ rPw&Pߕg'YKOQ%IO^rDM6[Q*qp}GkUAdkSti[bM{uUYK6\KufYٗ\eY8CٕNg9S_7vQœ k[4.jX8 jbJV7mS,zj>Olk+T= `(s>=N&B4.iŽmR,ҢJ3r #8ڌͻڞ!2v1ئ퓌%C4MNʾB-Ү%Zʨ9C@NҢ6۪ʣ"[);1/댒`& ƚ#/4ci+ĆL8?Ⱦ ZT.l&&Ȣo3 "#.ˀ0)3ܳ(8kȈ7#J{8?3./ z+;&l "ӽN$2KBBӫ<5*:ү @޲T(Ϋ/4)Rz==n+S1 (0l{ͷc#ȺZ諮b <~+ƸR> Nl+:$kf,B~JZھCM hiXчkT2~^d9 Pu=ZS~A}-tMPZmA;p)V{&[veԵaTUPTsץ1oI}\&RO`qxe' h\uW4ur[OsWTgvJIotAYmML-p6_ESp7{eUQXiI@>HTȪҺl6r'm쑿-hX]vR7y9{жɶTZQ\g]-c][nEuPSwBqQQt9JXIR9EdH}W ad@hT 0x'k4ʛh1I- *̥銠쓣(6ҨL:d'򾉪Z%j8쬧ǢH0.$[Va{^&-e_-]RfU%z=|U4K]]e#YTalXGQ6{z GUT`^|YMgDVU1{߅}Ju$ [[ZrIƢKXI?cL3=K:J)Kb E@IedY@VV7wYq?kPU^Y6.rdT!Gii=RLf5MS6?yvFYiWqneVWUoNFōJ ,4Lr&hk ͯR-c8Ӯ;m9͠Bܪ,r-s#5qHd5r`%V1iѤL5zVANuߣ ˧-2L#V8 ;.L k8{ #rڤ-ZL*831o#8,䱌C"Jz:/jkסB$7$#꒶ m#Ҹٽɲ,/fZ+J4)X-%tĵswaEѹk% }塻XWUMED2D$1qN݇B7$CzM2䲶&"1Ҫ-L8R1!nێɳJb""r¾m.Z#ŷ2$xkʒ R6 K$%'XucVGJ$a1qS6t_%@ Bg[t駎R8I۴77+K$%z$=JΫȚfףxo3v?kzޑok2 +-bա(r"Orh:M$,2#* ?p4^5J2R!;2I;¸;>hR-Ƀҷ7H#4kӹzm+Х'-k9cH/v%c+Cz*X2*/L髄֦h̹kj9c' ;Ѝn9B#?0TWu!v=mM\gJ[\P7MRUagoE"NwTDɷZՔLDQ2oZFj4engݍ*(9*SF*1b- Ӕ[X$(s45J볪T./2 'k' MJסkn-*;٨ *C- b kr(˃5ت0S͢>kȊ˃ȪڧŠ={ԓ!o[/*<(nH9DuE&%lvpi\['QS]]rQ6nfŬU&-Bs[U&JDmmU k^Gy*I5yYDeYR<`gJEMJY]qb7Mf3JݜS%tvm*kmKZV&͢|G1v^F[5*l.͚z2,+5XŝgbS!G{y awM1D#]G-t$3E^fe6u7LUm8Nu:AdS]wE@]7Vi]҄\W;qY55OnU ]ֵQgM=Q SӶaR)dPL]lԉj6qaDՠaUv7oy*Y]w]%iG!4ICm%|q%cwTHZUe'SFY_E8]FRuGyZ'iv1@:kRBJ9b>V߳ Z" 4MZXm+i:6 T:b>mc8+8R5d5:0 :ĸc숯躐2ӈ C^'N ڵH%:Jk<)n1 R= 8m^GXPY{he 3rW4ums^ (&1r'/J Ǫ8Ȋ뿪8n),2"/*n?M%kJpDI&(R=/k2[>L(%RU4TwSXuE^[5sWmWU1~hRgEOF-pMw-sV^{HEEQg\wN^mMpB [2Rڂ(rޯ־5+rץ̛>8 .^ࡌ&l:{2˔.R-NOjFj"l<*xD.)*noJ^ B ꪨ뤩2RghgT\F?SVMU^/L'j%k(iJ; ;[&ʳ3NRR"﨏Kp"2..)Sݣjs69S>/3ʲ*jʽÊ#KrL+ڠ,i*,6.c=**>))JlKj +*:!|iugeVwA\݀F)h[-g%agNFmfBҽkJ-kB׼BȐ >. $Jj-NkꊤKJv*bx ;,*jbkSlKMܪm Ĩ+ zPN>J)r*1&m5[6{͢sJ&0k:KRQI'KtT{/A- aG6sX1g[ɱtߵa[SPZ]'u%m5{t {[Gf&~u.qVu8ʼn%MIeN~9euѳoV|ڦ*#i( 겒I:2Oc"ᬭ :kȤnJ*^;2,O<& *oӸ< C⿏Ԫ5٧ S;[̲槉j-oK")S泿-bzJj 2)*/*bK"#N5j%i"v13zmvJ5]QtԧE] Uܴ]0vy ArEPUssQtw5Yk6YX9iل_򬸿 X,64n`4 9 30 D,>3ht+J:5.rߴ(صN+& , Ģ6#isܲ { c3ƥ-zO@%"W& iYA@וd_UBL HVi_b\)?ץu`ѵ:tݓ\XV1YF:g&=TD^՗}y)!OS7E|ؗjpP5%Itg G֡rTpfQ]mq xX[WYQrPIXwat^I2舿呪j{XQBmQIy^W9AVy[&yJe|qY'b7Ys\&l%y ʒ"+"`>ҽ0?w$S MPK9FWi.xmantissa/test/historic/mugshot2to3.axiom.tbz2BZh91AY&SYpTw{n|p{ݵv{u׮Ǭk{ήs|yݻjim[K׸uowサy뭟zϽky>w}zv<_r]{vgsﱬ>sꨕwzۻʽ{/{]Gp{os^^vӮue˷YM=7.79ۻ[GQzewx==vη^2y}۾ӻd*~L a6`iOa2f``&M&L53# 4iS&bSL LFL0O@4` z h   4a0L`P y=)&L2a0M2b2d` LɊxCLMFL1S،L iIL M2t00hdѐL i4SOBL0zLтbbd4Pdɱ114e<j~ h4a0L2h4a10M4iLL* $Be= ?2F4ѣAiM0Sтhi= M40LS2&2&F2&xИ OTz `؞e@SoIp  qM8c&#&Lb"!16̧*}L LEW,nicL`6N' |1dp<ňJ>ǭaFF8 PIAf@˿@7, f:Oo2(ͺ8-y!9&;'xzp+$*U}W|`r7cK^Y~zR}ScE0%x5y֚͜eC@};USmqN0u1茙([y vxZt̨ i߯jCk,=z鄂t Ӈ-!|[8".p+}fmnH>=(w^1B᠖^(d43w|}4p77i/1ݶON 0a ӆFvIIJeOSun3WQ/ϵce9ݙi DůQ`KrH`Y^eݳzA?_8,dƈ|ߎ5~vZ'WmnMcR.f7|;)a@XϸE{oзF*i7:% I!khxx1Y=}j!ia$1[N*Mb?hNuq%~`9N3F7]NM2zࡗhL/.vL VݑRmf?\cFIRXR9W]1T'A˼?nu33ty X bDb&[dOᢛwI&Sګl<\AWnKVytkDp{,Hu! ] {.D>30+& W-j6CQ{0*98εyPccJ#wrfɵ4[ JiutЀMva˜d#CW٫a1EV/Ŭs,0x_).bF^EkAph+&(Ggɾ >K-‹3E2PJ wwDRJ7Y^|PjqI.#!t,]/|2=~ $!لvAe 4H[/p y3+e菘Kru+9HZjL!- lE< 7dk⡹O[g'a#KYPU=:V@12JQKʝ6gj FM˱w*"YS-5 6Jf, ق߽W#5tJ?w ڱ 7SRQ~m+Lo<rSz\JciQ$,C h+#sFs9@&%uȷP%I_j3nZ/|`uDbY"%tfíܨ₵RLyAZEp;U$L3K> ^Nzu$gu*Wx7y9?w1KخآȚ2aۀ:.*iշOqĒe }ٜ#*"1NM[UVArb{~;2>>«, 2rlyIJ=cL2|rIK eh_imMs>|Wgf6\H5Jc9U*XBZǹE&aDWE#vKyh~n>6`X))ηͭ.eKVg-WETl25taᖵ]'ݔ.>UD{큭 IZᘶ|g{=j\-=k8*tnUB/#;zZTοS.[x)H ϙ^Yo2;QQ4c#1hoȋLM9j-yiHjyq7UyL18|#fb@c)Ao*Aǽrۙ^Xјʅ:|eN_SHYHl_,7ń ̀s=Z‚"-GT4TTz<%LTZ>w ZFVq\8ZU0"P3B:wyU<]&J.0/ȻzoʺiЅ[ 6 5x:vS ꧫ Sqi+*,֦:uL6:UTD5NUpwl%~ 6Huo0F?^ `*mAwX)}^ 4JUDYjc Osm*~NAd) SF_045k`v.ƪ{/ b,> 3JcvO{(3aVJJbE:\7+zиl1g[5@B!TXO)=j%ٙ!wD!LIv)zta{-v4Y1 g(Y;+мWbŚYo* :=tU1Y)'=VAa}}cɞ.D*GP:` {fEEAe[9Jxn 7օd@e*'e6i^C;rb 5o(! 0j\J{ %13D&f=MۊpW }$ik4#EU/Ɲh_CbJ+n KWR~c;`wLM ǰ;T[qw>s#QKfqj _{N8S9WWRɏyXƉ18;!G]zъczdgY!ꦭX:#\fAf2㕕FkW]u.%M g//8gC1DG7R~C'Gy'UU8t OӲ017B\Y.j}A%ՇVm3m7=;9|3#y0yӼ"Ap$%*R1=1覑e>r\% T2_S$E'+NV L܈.=#^[vv;vmޕ ?(H<~^Gx ?8S{)+JN3 LYS2UL`ImA/|ڛ Q{QEwhytQP'| Nb&ϠC^e3}z E0"vJU b,D[Eލ /՗گ* Ri>HXmр-DB4gObb5W7{3<*ikNH#9;{,%;P DžCD*/'8HR$bz\(BueXp:l*ֹԸRuޘیT:T]E7uӬm_ɧ,NH`L9[Ҡk%wWi5Tz1v"EriC>b 2B_G0z(균y/CP4Yt2^Dc,m]M} /|F3O~|FqʢUre8vw E/EȊgЮy籺/xK=ǁn}>@XidҚE55Aod'oڏ2R C32rUk#[p¤ZY6`@Z͙Ҹo8veHI3?bw|}a(.A3P4s]A'[jA-7d6-9[Z fiGϬc@cڜ5hR#.2./aJWJWSvrW!Kg-0$[o`*2 Ɇu]`ȼˤqCگH0~ǀ{M[Wb0ND_lWdzE0[{Ƀ8fn'=d~¼xec-\{z~#S_n7 U!fK ? :ebF#t\سd/F)dzHI}$b)GӤ&6]`g/qѲVo'- J^v/V׊oMO(4u߅Z7e~5yƄSqoN G'ƈr؎P㦍*K%McҜB t?4k K4ոb ͩ-?A miN !?c9@mVSurV/EǓ?ʨWŜdT6@I4ot1^aLVw%;1Rf&Ҝ]Y-?oY-桄vIt"~HStC;ѯaD|ceSZ50ESY3QS .*Pq3}26 sbVpp[; TC>,% ̴%@!m炯w(: Ttb> S;\\e/ =e:/]զA]/1~L;i VU+ic4}hͨ~^/ʍ7 #g\W !m&0|n˨#ʤP,;K)[?:XU 2`}눖 @*#*=*{R |X]}>{6j\◝%7pbNEѿU Ydۧ£k;c M#حiۜdUl>-dA? 6P |VnZg(8)??'+7TQA ;M49I0%^rD ;wIOդ|r:&@4qzHyhn#ЦcK:AFkʘ+Wuq6A(3UFY)QNb ω 梲ELyyQhzҠo.(utu8𯇶iYH?Sn^2XMG*<ՂӼ9]'k v7U@MfS2Ns^] "SNc ~!jţUiyR7]we!,Len-6sor+ HjUN`8ϋ Pƻ9Š,#g&D"@*BW6p8B%[cZ!JhŎf{o.ޡNiڥUjZ0lBN4 ɻY!SʜUsSGKh}CtSH;`Էu$J7z 8!9sQ❈PBA@!,=v˜%F''UDDY찆}+5IAÓavT ,{ {7t1< Ic6,xlnx 0q%XlN%kSBӶ&lkk':4u4 _N^!@ Ber- =azDE砈P%潘@w67vdnBN݀loA4d'󹸢%#(NR9-aNMEvK߀FlP"l6L[|[m߆bA3T^y ?&n ƥӈʡ-&i9tb6'p ~LTJg(چOMqm:>qv:gH^ʬ8V\ah>o LN4i?Q6{o?l ;Z"8Ba1~@.@v)S6H7yԛ4_z͊+W1?Z>ֿd3ڥ@t {H&q`&i??T{"!ulUGPaek$mrjI_by}, \VrNx َ+gxx'o.y8jzs%sI8=Qlc@ 74Z౑`NDUFITev2Ps"2Ay/K:=N$#MiZ]SsT@Ma~ [׍>*3پt5:>?(.jor5C]čV"caEkƣ 5ÞtBbxubu@ jɳv!޾{koK#8R}5w{z!i;Jh$%;uCX~XuAUcCMX-lYJ&IZ23 da+@R9>1}Mgۨb,|3K!/sS=)15 WqG|3kQ g+N1CJCה٬˾b.BYi4*k8&Bky1=*tSБ$G`G`}B^vaZӞێ=0yVs!m7^MR <IHV"tqG^Ү#>l)HI>Ekaa\1e5U!IzKY0H_7U}P-kU:f!ď=" [7BAژL_vq85+e m9.=J `OE~DEPZ|[JZ> \?6s[YX.̖-XK2=9PCh^^٠َ"wB( ;Хsydºg((G0]coW>ަc),r,gvؖмa(ddbYz~,r0͒EE[;:9,/osFxNc|[P.bˤYeت57Qc;OȔi6!}%jO­?a^E)*{?k#yr 9djxہrF Br [Ivu}%dB68?9XE&n9A͊$n3|6GrU O䭘jQGUT_%/3ݯ|5jg *1g;A9lV(H\ o= +yU/Zz%+^ +Q?.rٚwNgfȌפִX5K;t;hٌyV=X>3h2O %8A_ƟHUÆiNR7A&*:qVY?:A>T)T#q#E-ޒw7ތ_JE, *kVb~9뙛'u]& DwneiqfՅZi{dJ` @mϨ9.}EXDֶ´GėrضIXG>YvG%rog|=J隘MH6@,{pbȻ߿4 }ղ:j{w٠@^!!-mt2XǩrR1UkNGBpdFRV2- `4e U( լ=a;,uGV͒N\>\弆#H%+4yrŰzٓpT$B2/&!uތ/F@cjSx$oߥ+G&ۀo~9qd>f_ckS3H_H_(4&OQPB߳Vc21α]ln/1] -V"#:)-Ħl]u&F𓀇M%,fk%LiB[EN!>N4Vng/>@lj2>sk8h>(ůyE(DDr9ܿ"l}3 h' 0q/`IJh"w_/cWMUcU޶maY#+e✑/6`l00N(Uq>iP oLkH_b=Gf21i0DZGb'Pgh13pWlWؖwNwY_ͬiCV_ ?=XuTx˟|X])MK/:?Ty/%ɫId}_ǝB8EDT}"fo(Yy[vp:S0 I / :PLOkĮ<'7K_;M7> =};e]qyA2 T!kWƟ>t;WT -]X51^h7bL;^d>dBo9,YTˁsmdIBJҋ5a]Yi,N˛ efx>I':,ۏFYvx 2?GPFW-V*xY:98[ة zIάC J Q.#|Ygo|{a wQ-yG3 ,<U6@D77m|3gd@x#&AK,צ0AF<<{8_'a(|jGYV9E}Ǘ4}*⌍j4QkW)gyRI¸`)/KgޫB׵Fc=2k[O^:jikȳ*;+7zUu'xavgZiHL(n>):h*_n شAdR6kT޳>f(^>#}AN9[Id> yT,ҩ$%ܠXHtmw5aKo/r;"ۘ=3ФֿC|Peͥ$qzOId,WA*tl[ȩ$"]7-c~A,o{ۙB*RP0q#iei5cB_ddߡC )5/7q|f)8w-ST3cha ^t1[f=Ӧ>䌙 9P ,dfBgOpa9O}Hd@Hě"Da*l ^o=!)$ -JFe y@8k4dbwu5s+f˘|z!=hEY|3&̖rd.klW*ݿ4ϡv}|u )Ӓ=vzM0U.,fyI &z`9*#|2њ87sj2IDbJXwk؃~w_)g-Fꍎu>s @Ofj,}ώ Y;mg}Ѩsf,5YfmܥRcGROx$;"\2e jO{HCD! O!!\}ݼ!]լKesuBw)y A/Äv'I[<&r"wgυzPlO >nE )a[znǁ8k6|遜_г'M>蜙ϖ r_qeUrӆȷ}NeAjdgL_1³W E¹ E%3wxz~_gw7 VGԞ 8rω/6r o[)N~93wC#mw֐))]m yDP+[8GV8S9 ^GVī⟫VTj8O".kAW(ޠYœ55DJˍgSKT1okQv>4i)GL(\`@s@imh"Nk_!.; TVkypGc@ Q"w;אL1Opsv%F+tcGwoA?IX=lBf5G͕>'-EB112ҏ?-̵rP*n()^|zU$jKTѥK,JjӤ6Q_Vm(ܶMDR NgmRra.w@#Ũ7[e->s_?te`\N[N:j}A$c b*jxq! *{Y}t9C0ωۨ|~ OGAOK6i2^ xK3u8L/ϯsmwv/;dyjp:y#8v?@]/4Lw[>7 wM#Oܢ"c-W#X/|sA8Jۯ*Qk-og7‰`afr}.;f6B8ԧY(އϵF RcʮEEAgS}Q/=Bu;Na+'P4RL\9Igwt]3HO(¨Phfq nlJ%Z{[k~u9؛HX;U? 0I'T6}SCӷPvvV[ yl㜆ժ>`GJiV$х)/<:F=?%Eiڃ5.fKh=)\N.g$Yi],b,o'fae7>xp;*G&sMܞF7'HĆb1נNOaſ3|J/`4yi y-:uT;vs]zr/=Tͱ~'e/W o}D)(S`])G6A( 6i+/)'j"9qE22(>`C:̀\.d2t&!8@+ȔdW鲉'Sv1€rgyuԗp zgKI^ח˸ _C6{Wʼ+,[]rk1zkS~.&|L^[(uk/$0땛s:>^ mPas6LKڶA#d 6F׼Ӆ=-4rb: +x ^| ; P@N!.!{p5]x 49Ie4~1y55Bw%\]9kAz}"VM\ھL"Ii#qQm[{C^ɫ tU=~a 4#s'/o(؟JDo喷dJ6yz6|Eo.U^o$+ s5a~;\8Nʚ53gVr|cͦ0Dґ9:[~CTcgեORNy30%bd` :)j_X]b"~|7چXk XI^Zy@M, tw=AY3C>5('*12|v GH9;R4A.ZVS^. w<'*2Vr zeaTf#ٗ;%[I"tnʍJEWVߌEeƷ S쯞2!""k}Zsd(wv撍G=}KPqF=u.@uro+iZTbxz%3儚13O.er}E|%w$bsnpzn[mugn~wv5q &X_·r[1]mIիsu_dodܝ) kmc{!_Jy`_^Rv0 =¦9SOR.3iYׄi( R$zp |Wm(#yWvAH[ܨi=s  u?R!n e6Y}-xy^͵, )iU !''1B4_֐VKoY{Go=  1cvXGɾ':D[֠=920avf1KZ kQ@'deAA#p{zĽb& W/W> kʷ0!#pm[ \롆B7 W~N"Hu\~P;zj?}c%cT\uzD͔+C컬~w׺l.BZa)$z".Dˤ2BY#'Hё  qmc َVϩqNfEZH>ozBGA>~*Hd:i<[$h5#C~WMwfOGtǍL"醷7 &DNm#5zhQd~` ICv >Abav*顱 {|X_Yno*Pd":09!Zʨϯ,UV[P_z^=zcpL&d'RKnT.lH<'/Źx+h;!XFMw\czf6ĠXԺ6*ư?Կfγ{\mU>}wrGI"ʯ?zI-]f?S-کbɜ\_TmIo`Kx-WuL2ǪQ` J!?ɫW>TГy r=EI\ꌬlaPukA:Q̢;PL"pаHY; \M_] Ј [>ymwux] ܗg_,:I2W md\qzI WE{2G-߭FI?\|BhDC`;d;9m o8ر܊Ғk% xi0DrQ7t䳞0w -9V폋?$RŬ"6&q~VRv{Ի"ϕg~o~=/4ϟQ fLCU){F">{0>E3Z{hL7'/ ORȏĵ#0ufg/VD ,hQhJC{x=pj,Yăk}(Kd>٩zӿ/@OoPHR'atb+zӮc 25=00,^ߪ2-Ik/)`V~BWV ~U5''&_oTK~S u#E^4?O&ԚKFժju٤)EJ[ nv vedDH.PKֿ9I3l'6]K _ݔ~r nNkr xCSƉg}vgwq~YoS)Z/d9FLu{1eu %>j<]mOGtj&ͼDreW:\4Jh暤~Ogɨ\n?a\i9&Ĵz{ vgۓ^ =]CɇB`"_TM<7sߚ7H;Oׄ Jd*̰YO<^g]I`oAߖ煳A-2< c#)s$ό N<%hFlۖW/mxv, qH`6<ҤvJ0bvֶf?(`STCs.)+5GF ?_O|v gYO/xs. \ C5nØwfZPv(,D^,[y}ŭ(:l3Q^*J3\i9ւf}[ŝ#l5TUb]AЅ 8 ?m*,HU烕X9cr&@Kc.wc=4h˵Dן pP={\1.9֋UIEtLcTeK,awؾ? =JQr<Xgtyfyں 1n:MQ7f7..j٨B/opҩ$܊~A :_W&jQm_{rUЊ|Uߵm FF+V.ws6VvfZ T0rkF4XrF9:V' ;^3j} |i)sf&ho>(^p4y,UYiE*mq'~jSK>P~;vl8t7p'AGNq-5xM[򟚄xlC]{?D.lb&] 'nd.w{ Qq4YkKr4>6RȽYY1Qab6_(12g boh |O-X_}@j%w tN`g_ik_kiuy$u4;t<{^DqO+ɼnf? @Nm"\iuBKb3! hTш䥩LSOIڅ7 {pKuWeyrT9Li?㞎A!xIs?e&$C8ָ=x88>;'.C:|Vϋs";ͫ #_ಕc4Z=ku</qC5\{IrUIߍ]j[ YHľ*U}utM3&)=zȞP/m3n{KR*']ac-H*c` r~t;gap"ĂC}x_R?4p`)|`Vݮ[;f@J֞挕@Pi myKD[A?, ^Q&JW{vc(G34MZӜf>bt}@Gb=8%L~1Fgp )ED%/ѳ 9Kxi?Nҩ0L[I[/`'cbպ %c?w߯uq67JD+yNgy]U$r9„KlV3 җ'GWJ",{:(ciSOF Z2lm¹/z; \Ө&O*y*j"[I%#ۛ߬fMp8s60 {xyt!mwGs:G]+$htA1.\BPbA/qa˃xʌW=-En~Y8=IK(9_qP*/ihߞ%i<=V.W3JNe͚;Djz)1}U\"yu1ve0>"! Ǫv N,9RX {LA|3Yc?DW,qjj U|<ZB .D܊e; %0iI=Ւ<qLd р71x|qmpM .xvq F3|nK ^ݫ¤fR;kuwzcn(_zYV\'ʿu3Nߪʜj ױnwu.?*{Oj0Mm͇˭YW; PH묠0vw&wuSL7l `AvMM) EIw`ܙEKYz1geSgL&[Z&4yJC }}5h=vkS9>.->i%fAe'蠻iKR6)&|;!c Kkc^uaEJt€hsI$FkST-=5X(E^>JYV>gxCw)%}]F=4mXr`TLw3Mqf"Ԑ-OPW.C#L/J9Eyuz9X<2]ڠ#%{y V<:SЪaYo3 7ϵTJFq8`{$VX9uׅD3DJ_ ذ%;otA/Ǥ_@#nHB3Jespw_cj , PS0;<4;Oce-W9tGVI1ٌ.o`bh4#a^sm .T#pRvZ=Y[ hâ-65!v)OTv䷷3Rߩ {i v veϸQ9|dGh:VIGggV>7`{˞>JcQ rhRg߷~^^ؘmw ghNc!յ~{t >+7B9n疣1JlХ J6nkqIjx|̊gqVE{ ae@qҠ8GfF%뛪%dY 4\Yƚ EtW}@]K\&xxB`9M-l'"q,q+ '?4#;T/;;8!#,"-|K\0jF)˴*P`ӎD:mƶW6*x T5oyGp[GQc{Fv^j+x uCD=xgS2M.xϬj-@M/X83BR_Jk~qI9dc}t={KIh5-xw׺ao5n L{~cZd$Oo[Iw<ad|=ݫw{pDMM̠\\.<Ux'Ff'Sgij7o잇Mjާ':N‰.J9(x\I}ĺxt[uѪr5wURh&@ x|\2ɍyTb}<^4?n4c/~2mg-͛9K( wsU) 2Lj~Vٹ:w){Z ?At#<}*3$G3%IfDE͏e]K['a\ ]ΙΔ gļ)`uPg2c_VּWO T$U9Gs׈}!pfLIr.p%6y6`s!ӧ~[o M IgyR@#]@D X(!ۣb[7lJ<3 T8^Vwz~jc^4̓~=A C KN9iN|ʣv_[B権1w\! [s;aX'1F5f îaTB&* ]}G2Z#ҍo3rAʍ)I'lCCoj5!Jb'K,k䤋/z]&x?E<&Ɂ#; S䯢c9Vm.f=zLӚ:UN-gת8,Q} SQ7*TB3n =PqD|_urPx]Lkr, *`_[;W!g7X`$S L^c>HChMPZ]XTGWScTȵ >) |8B{v|3ogs7Uߺ~^tT%](]RS>2TrMҔ]q :OP3FfF`+-IyC  ՕX9IdbmS"!ZRC7ß'邇\ʀ/b)/thQ?XÛEuE+&䲢;#$Xߍϋ@S$Q睞.4]%З޸m^GbV 5g?@ˀ_%T8J.6]2dK_y]UmPTׯQ*0@ּqqVn% '-|SsC]o~uFp2M ,@!M<=A~ &CkXl},ZCs, 5C" d+pz j=b# ACU+f<|bm (6B_$nn-ttرxQ\e^+P"V64[YGTw; e95v>-}h 141;P2OD$L3 /ɘ:`Ŷ^=*<ڒmB?DŗRh!"@aoJJyTp @S l_1auaxV$jAqFE)C^p(/Ls`AFY^ legL32NơjP ϢC͑,@jzڲY73(  rf5ej5L'b2k4;F/p40e3+9? ܌<Ɗ )m94銌(:[ݫ h352 (7QɈ<;vo+J}BV_2‰VJ< iqU@u* M85w&/P,x^仹)E Ù, 򹖙UߙxyzNB"ʷDjpfBEi4g;L@@C4Ͼ*JBK}/ ANM.T[/l ]CLzװMBeKoleOwez=@IXrrND')Ѱ1905Ssjz}1|e#'W~Wk-*.ɱ48%xP1kԊ0sP(ʠG ,9 V^8ӑۧ|~;$ZU>|IyKr.A"18͞,,L!jdh妟ܣ/]t7Hٔm wVhm]CYc=3ɝ\M@`zRi,bخ%RsԌHbͦ9"Y1#Qcgg:'9]CVfJe.a X";͙^(nMDzWa4ٝ32.Öl +P ɳ?5}UV!='haV-W7,>1)Ax{~0$ÌCA)]>z:3M%J҃}%xT&j#1 0tztьy#?QZp0qŽFV*FD6CU,vBi|BތO*$KTz?-YqTrWzM:_ڙfu@PʠObiʴ3?='򊒆lgj Qk؅=HPi (O3d5ʛ|]}?iHI Mnj9Қ!"=LՕD-:ʩaN:V_?ad5@",%,qjpH. m|0D󶜂85E|8$\F@)RS@YMw﾿jK(L2 6Rӎno#8.Fȁ;MAN,`Pvz0|۰u,X3h==7"plnHQPO|t?؆Z(QRYꭩsB0Vk}ĶL#dzƭ *pv^,Rq>5jP!]kUR\\-恵\QCQ=,]O\95Æͅ]pUğHMߎC^lʬ";-P^z/971f.](MeO+yQc: `ya[J*ބ e2}R<\JbVV廏Z71*U:p"bn~-iKG>|T.-*i 6A0[Mf E)sfZLf‚ӕ 2鸂{趢{.cyShyZ|Q-:ʰ.A,S?.pY1`&%1gWQqPPrW9f,P+ͼ _8,00/M8Cl=G6~ aJ[rΧLR落@DPPJ5}AI 2*jhc_8R#l,^]M ~])Q5jpqK70tĒ<=RvZeTRklpӑוXXxź.6AJ:"2#Y!'=q~W1hOtGI\Fyg m}T~N^?):B׎-N{?蒔?:}?g4g1@JY)Sew۲4m&Pl3]k&pػISg 9\{F@Afpv_܆m1?Kj3I''a@#v0֤X8I ԟ)>u<ܳl.|QC#ҠՇ3m9`.Ek7Rv3ɭoX">(ў=E[i|u#@xow׌ō:$p\ u;Lxnbec$9ua)M%-Zãt{OUc \to exVh8čjR%btq.*; 2*md!w4OS3V_eZ|iiNY49eL3'f%jvHm] >@PuT՗.ϖ0ͧc5wV88~x$z3fA4vpLg4T)b# o]YdLg=Z:L#uA\KFng}&<֦|ϔWƎ, FE[϶Ksy'tr$\0FL zp -w$2^%P'VFfD0b-'sث"j/ؙi~=h/RLpkÿ?nro\_6 <}>`2wǁ{(EHND@tSRh[P]o/'UkW=K MKLZ)ܙY,8E#fGড়=-wg`/˗ u(,?a&8I8?ro0r^zׯ2<ԛG>D3hWRw(WWTNOp{h- $f۳Ԅ/'hٶ`9;P'P2׃~dxU 6'm]3 "SF]tBDVfO{QOr6޵'0CBLu|lXYY}OnȢ} D3ݱ/BBi!-B2/p\,⭊1%пT U7="Vea,M m1=6&w1dx|览|q$lYJ78Et& 5|> T=F]ٿo9;wd6Sav-,q/7*M~P8u[|VG>cj=@l&qiMUUҔmb1xNC0ǂ>c8:I$(eCaMB,a@>d.bM A5'(.?aL XVw?Ni'i1BL}=d&׋I]k{E߉6|%?γ$Q|A+/HHyöSux`I2r h͖v0R\ i>| Z}lJZam rx]Q>ZJQE66/ Z ۬'1 aAӔzX 9o)4EL&.O#։Xh-)G8E]v<$!PUmWD$%@?lp)$2 ~F"`,[m?c|T2WtfΏҳ _B&ƹ}3ROHqOG-@7#W>2< F٬:b0y B;N޾%MZz2ҁl3b;(TԷ(A!fYz,Fz簾@wo%1eu}S9֒.3's'D59q"~m '['X`O~۟Kͤ[;ne>\n2H 0ȗEB3oyy -uoVB[~=׀'} DxEdL曓P~يt)@иJoKYt|&yuGtzxB//LZA]WgEH !H]Vŧ¤L}Vfۀ@[ID%e_> xf?|SF)jޯzNս]2x;rA: &rr8_WKLy]!@h=P9qX .P^*5N7r/$5z^fպ3PX:O7УJY?ڕ!9@߅Isp5؄6G`G!^O6rޛ 0h9FfPfdZk㱚[_ճey >z0;(rj80h'n(60^[R3Pߝ;yyCL qw kR qD"z1wUst' wUOt*i|j=qʱ<0}HN'b}П{HMUHt!zi!J5 }u@pS*EsU-O[.ܟm9}WrnBͲyI-cə+] O;l dbJ)vHdua骂+\ '+?>|x'NT-e;`ڨ\3Fe>iH$Cwn`%xn{Է-lXLfWcDZ0l7 ǴeqI5hO ;Њs"نຆ2?#N'W'^+o=+mTxmmP{~W#זk(N>$2Od<4ϐ4 8l=#^,#\=eTW_ aF8%'&KL>!AOixﮎαw]OhH7ΖUET }irSܪ ad6/NT]1HqZv˚ dsr=H6JE`]*ii#M火>M2?N{ 3&tb3U:;; mD"5kuTVW8Jڊ NWELm-?j@>e°U2sɄΊ*{f4Y)Nҗ휴Nձ;CΚ/H!ȗ[)*AvBcOvA]uEYj\B 8E6ӿxkcŃC_Dm sWI$GOz,65O}δq* cz K^ -c6Fc_iÃۇ5{pw};](y-O=#.~˶ _|0hfEe*3{{}Z|۰WSx2 j S\MhyF)8+&YlV17 7z"gŬN#M&=xY2 v{Һ5 2 orOL,Q^U:x "+Ž'l^GoYfS8'5ʂ** 0&gp[`S313"IۅM9Y:uܰ~V<&QScwӣ¼hc|HqdL<1{?7}B Оپ䴔 {*<ٝU{*b3y*>9UuYSH?(ʱX7%$@2Ce{;ed5BI*nׯPKsW*q5X2-Upy6֣93cc.uʌ23%%LOl MW[}oFkeV#:v}+sa:8M$<Θ e} g@PQiBӉS?<<|)}kKp` +Ǟ녮D``I]nIư+.i8L`"#W2<ũwޣr:3RIo[;7-Ff|7d@[#ߏƟĴ)tA|˶%1EZ*v{<uKvO䬾^`ԜmNjټDhSWϫIh)Gf#bcoK˙^W{8djR3*bҖ4>(}C.a&mC. Jkg`8NAfv.ypCcA?!̻#n$ZhK 潫_07knԍv\.C:aVmwHJ)7~91s{ Kt6~o+qN$c,]?Pq eE3<\lvAjC)iJdIQnrJ~r_h [рܒQjk($"SNLrNIԫ$V⾴s4; d\h4hrdp.=v'~@wFw?*z)„nPK9F ͪ0xmantissa/test/historic/organizer2to3.axiom.tbz2BZh91AY&SYa4{cZ5Yv;0T ס  2i1OȢo)=4E=OzTO)OSj3Ԇ=FzSi4GFF0#M14iC hjm5OMO'ޤƍMѦMCA2 =FP@ё4L4S$ѠM4b2b 2FhFL14hM2d 44iCP2zMOF22=Sѡ4I0i110&#MF4f=L@ 4  @4hh #&LMɣR mO)5=4ih4 F4  44֊ AQ %-4@ȁBnMk?ǧ{8$m!RIP|B}r#UIt$Dh"gH!5eZ΢ =;95\q+?D 2 DX Ճ*$x1c5j="#,SRBnLcͬ僽ڪ {^?I^.n8LB `}y}7 faEUTUdл+ wijfKed̹.s6gnYmd wK"Xv+RN-ak J$Ҵ-Nv=Ԏ_řoYF%Ur!2qu f $$U-ӅB@uQPQK7?|'~*+÷Ⱥ̘tN4$ `$ HLzIխ8hڮ&[ktĽrҜm ^ α1u+$!ǝ0RU@~,%A[^&|PVhBbKDԬO{ m[ vdRÜK"xI7u`B^KiFd!X_Onݾk"e[eBl Kڮ[$8 5ɠz/h$Pp#~ 7NLnmBqC!QpLdLFL>g;;Λ+sld<<` hgJu|! `0V ,!NWShm *5<66 + ;C^ѱP#-o/Ozgl9M&~mj+9QAX6ŭm8V[BU+‹j2l ]c 2tV*hlb 8%怼'df;hi` #BŠR`YEaY LŊeea&bb1 DJQ)IW:7nD 4i%^N^晼$!JB'-5Glb-!)7g ̾ Be/:HIL3MZYZ]7*b˛UUUUUUUk^Wm^[Zs 1 c(@1#F k01 VyP,~(2dugq$+A0*sxBl ͂|'K woN.E֡PAA(DT6,%c(STLB`rv^6=#Pwe-Rk`N4`mb Cj5s k[z0,ۀ%B,B1 v+4f Jb҇O C11E"d %S6E% ٓ% J!JoR; HIb"R,9Ne=o4P*{_ m)%Ô QīAFɯ'P0* +Is D :)Jb_5U:d!/)i'`:,) rHJ 3JJ>2hYZL)=Eqj@SVZĐіWm7Wp{:KFRBE<̅ͯUeqȺ% V\ :]*MDhwR8D6Lt/56%iHX XU":Ê B@B@bLxa5ƇJBƆHSq-C4 %3Ѕtj]f*rbu쓭.U iRALJF1̶F})BsHmzxT,0t̛ wv3Gr] ݾ bһ&>N%.})f*'R\XWZt)f@luP`)fIa,aV 2}f$bZ|ԭ:WѪLM6pkbH &`PK9FMJ4xmantissa/test/historic/passwordReset1to2.axiom.tbz2BZh91AY&SYH@j1*8h-dMSL= `h&4hd ihhѠѠhdh OG䚌##FF H  4qFF2 IDdڍ4 4=CC _5D9`0&K]l|r.-\Ep ;UTTGș/zǁ Cމ|)`fBAp]i5\(PkY4x|pb'EG#3p֤ׄAv僌IJc H|bDKy'{k<ƸAP"VCJqggrv$!fjETTQ+!.,wml"Z>5KYJYʖVc`ν!0nc@V0n;p;bm': ;C$&.p PK9F#II-xmantissa/test/historic/people1to2.axiom.tbz2BZh91AY&SY lBJ8 @QUS2i=A4M42h"4S4MC@b=HCOS@1 @4  4@4Ch$Q f'VoREUC~!K ޵VCHu. /KN yTZ$8Qϣ!#K/ګ-9¸+jER0B#i; [z(0139/]]L+PWȋe+keL wx%-`+CM;mf``$1ܑN$=PK9FǨdd-xmantissa/test/historic/person1to2.axiom.tbz2BZh91AY&SY_=2^CpH QP@ *f)#A@@ `& L&0h` `& "d SFOMi6Q2i Q!L C5A(i@@tC|~~-B43fI$d q:3w]c0ن6U1}dd<ȇ֊4 [ɉ[Ix#g[jUakQsz䃲2B*! LDl&4U~d+m7*Wp n6&B ꋆ)D2& pct-ظn\d?I:VǮ u(x($(ffIͅEt,J.ƈ_4c{P2XvJhSI{RFBSHӎ Q\s-fn~?&0cgaya)0JҎ|"I(+q?ΊI&k xDJC̆F njvCNmEDw>wFU)u2+;trdF|#5o` \R-рRJd}FZ6P,i@BBݪsb[k< e"PF 135S$H3if Gu)n)$)w˝a+zv<8n#jڂ m fg>]c3v?edbvM yH HBw$JI:lJ^nIM(7N2RĶee0L]A%2H*M#4XȓRTI]҃% 5.s -C@r'q:;>g^AFDe& { .>;3?ԫ3˶[{wЩ*2wk `GJk8 K*&)V4`6֏e6^UDDqUq30N}0?! hk,rJNAּDxD5u" U_W.i6KC^ /[7ve Ivq7p|Ma@ jYID㔌e(;G^HBDKʰrwY^&EWoTǣ ϒhSJ(!.r[aA( p,,tH5SVˍ/]D&3pԏ& r-|ov ޯ@P] PIB*?RX `H `PK9F$f2xmantissa/test/historic/phoneNumber1to2.axiom.tbz2BZh91AY&SYl0  ( R~s99F1b S$SMhi&$1hF&ң􇩣Ab4"HM4#hiz `` &`&LF `I!51Sڌ&itLԙ+N 4,i#niifǪld.Mx xDCGvB |BJBQQO!Cf%20ns Y!b %۷#QX-n5_D(/!0BLX>~ڔ"VЕT @vQ鮥Bx Fx wn`VYuB.&&6m 4)H2I%ZR^DLELId_1v@ 1WRd}ӏ,w.+Ul"f"0|OGiQLK 0tV}5ט+]|ڽ{~"l梬7"`G"#|,T0! !XDȂ!(v(%":1v"LzBPgcY_RArn4hĸf#ڌye#r}AY> 뭹8)QH~7E2v6(22)>R oX PM9 \陚"P$ΗdS{" "̘ՊI`1t\jN#mL&ᡏz1 ]€m4#S "@5BH=BG\=3tvxJ, Ht)LFJG# M}[ς)1?ؑ,q%G!O]`q")Vλ;M6K)!$./ZBNa[E! I,ȫ[,=Sy5oVfaNG zEBW)a@/<.C dm#JOeY q$2v,9`L5M Nh)H.NtX[GاKW.Sh[ZH+̓b5L|S(KNۣ`rl"~66xwoNM}%JL Q 'm Ҧ0K(Ta̹v=#tW0G:KA/'Ӝoj^n>.tIm@сǾuɹlu`:A̯z*;Fy?oLU ߶VF/X@`}<,D 2C޵ Db{4V^XػEguF DS\N}p r* 5EpG˼{K3W%EIk5#larm ݍOk&/TT^ڦ 2=ٚ7O5g_a!pGsA>XucZN,#<pVX-Z_Dv5RkDR@L\.%5:yL;vVRPV{p֭޽g3VҮ6tݓ̶=z[훦PĂ J3tNh ѧ# 6 @sBr2yuW[{jt$wf<>&I\mu*WyUHtdD*9U2|yXٛ#oRf R5 =s$ |Ĺ䀶@8z^O?yCt+~<1ZLowƦ NW_|aҭe*%K CKD;&UelуqL5Thnp- 7 QD6(ў" #k8ZlGHGr\\Ɛ - HIwYV J@Y4yӆدNuI|^a aکs0޽R Kf t͆JP"}&C O Qo$zV,R>D=|k\4Rf^36qj@*\Na2}4ax[NT%?l9)i&Qw҅# M )93m:V-^aF-- 0RRwS$W7FoցE nX}бVZm 6kPKKmI|،r̚->L\@kRؑ>ŦIei>r!C as--H߭kOJ9,`Xb[;&)S!N$V,gҐ"E4 )lc9͝$r#XW= ;LhƿmiwʀpIHغbYÆf,Vʟb7#@a֪$qV~U;lҊȀ<%ܛn'0کg"H0(O}-޾zg:.R fFX.!˯cYVI4}<>OmfmW[۵{>DqZ`EEJaLE] f*96Pux=xiJ ,xhߗCUqYR1mJܧXy<`G&&}ޔ&Z2cF^dzLA긿=f'HvvDI2$02-!,E`!j"T}Mxx&)PrԽ-̧w"*2hbrQ&MyvlaQ(8S!'Hta,jێv஖NoE5/0L)g TWnJ\|Hy\垃.tP/-Q;vv,S @u ,OXB$g'3@*1lq1FQhDv9pEHŷ]P4]Cu5Hhnt6G4\;aW!iY{wsRq `@rȤAV9u\/ ꋡ>bTGhH36FE SO8'[#fYl3qt7 vp x@idX O_ H"</D‹PA4! —pѝ$] _jՀ߳RqI+i0ax BC (K_}F<ύsk ۨ`Wz,G` J8 5Qґ31ScE]:rF7& 阆^"kMH ҽ, Q*=fg_:6d@*${ pB!@.Xn5pU*q̈aH @@8)„M-XPK9FcA&?9xmantissa/test/historic/privateApplication3to4.axiom.tbz2BZh91AY&SY03"4O++6邪>&(P$(4 P`8hi44i&ڍ2 lCzSz_;+.YVNP&5*aa !x-U'I$- Cu;Hޗ|_*㌜B&?""AE&,@Q"IIQ* O,yz)N"`wu8a2LI 4$[Ϛuzߑo Y( SHz(MFe*w.p8NL"& D,u6u(3  F,P!wֶp G9\gI!5/N}TtRٺ7b/{T[&˳W=Yf_Z rNm`'4µTUXb~|$< wP$Bl4(ؖDYUƜ&mJQEQ"ȌW^3Fu%JAHE*g@RCZ,0ЩSA*B}ߋ HgV~q9[PF:!N'ϱsZ7b.]E+y^pLEmjDV5p[DAЈCo.@8'Gn :ތ fJ1`.u p9Fjˌ\zS8 VmT(^2k@e$*5^Shmz*=ڤmT0 4@Vb` }N.8  ,8LbxD$j``"9j).?ܟq| G{CcO\⌊H0@0H(U᠄>,0.a|a`i#&B!N!niVqRykAfQXF"Q|C$NaEl>8D# t1ImK.BǤ;is߉_z?OgzomU7 pDc)b@rh_d (`0M2~ka fL0=pXDVرr !QoHT*c,2ctDD$nyOARQE /1g% B8McTlEZ#;#JU3j$Ѵ /Gǝ<%'P#En4-y Gz8$r*ePis9} <{H/)IvpXzID+Y넶ӈ6owM e/M^F0 &!FH5. Mt- ^yFZss*ۛFObxTUwj4 _V]ȶ[=d61W_EU_l&U[W<;&mRe6uXɪ -{BX@cHu1CV. xB J#NI#JM>R3QeIJsfSrKXEqY@7Ӈ>QO С4"1ҭ@3h-B\BfZb- >t} LX&)ߜ&S}L8a.'Bo^KQ8`,k^RI3=a܅ 8Йj8Rx:Yͧti7QSafۜykHt56c9e!$+euEۑ0P ϫ<5t 09T!jի~n{}9sˢhiU&!ᆖp._e&r@Ba@AP{QVtHu\Y@ h%F҄dfFfk+ԠgYe.Q2h9%!$Q" kjL S'~'VT|޻?nR2)2$fCR΍O̓kc੣ڧ6&ujt&${B"0XBUw[32~Łg_YԳϋL9 53LaSXkADUQU&\.ѱ.ťڙ> ieڝ eqV֦JbG[%9[&环q~`\PjlKA;,WCZu a{]m UWJ3x n/z(vRSb]eYe1RIʩf_KeڶTZ.TDToo|f P#HlK"ld ֦nu`m~-/ ͝IEx,qU߄lbuUB\w!]-.Fa.M$l4dH\< D@d`#J[|K8q5:ДxJ8ryu<4gi.k-#T4̽ծ&{ŵm_o-Fh; z6!IqسW[8r?_Ϟ|'%'.#˜I9smhN8o&z +?s0jsx~<ڶ?ksui#|Qx.*H>UUReSyG ӊGE$.$}?`lvgp^G;\>;i喇Q$>¬a6SGYOcԪ>>MwIzDzF)(=hIܦG$v[8}.L8cތ}I$`~E|=?frŻ 'ђ<,-mn3c5ub LB,~uwI r.HO'YVO=ƢGS꺺\$.IJRG,$`뽋E67I2e$ivRX9^mNEp؎-ML܎p)f\JTET" PX1p{Cq,k}54q "7 pZU<> R@9`߫* E%*)QJ:$+ K+bJX.6/5Gct QIz^΢RDPU1dBL "N*X0_B] ӟsT3 5#c/eZIeiRj%VD(JG[iO$ g J&T7'}ݺm2yWCbfE]r̟FXM#jc"Nd>Oi%ͷZ,{ Tj  # 5΋L/v*E1LW' z>'xLIWqOȗJQ9;Or2JM5*$P{׆+3c)&) ;nLHR%;-i9"o{J7Uz&%IBRI)!P*p,|"ő֘LZE8(1J0`Q %.,DtD>6;R m ы;~y\$͂QO",贄KR]R>䋯KTh\L?Yy=%NgVzM~vVe12&+;lG+0َoBJn5+y""dLr$.=m 0귋зKwySsk+⒮Ylc13&1az)NVwgIP' .:yE(f",$ S53O3=%)[<.:C0%3YQ5%yuT(T$UJDȜa G 6 )QJ(Qi,YSsTPۋء~HodvڜE/wӏ2dznHNG(YVu0)='Y'5zd$}}HN/!mqi`1ȏ*_$F9c[j"8y:Dr<,K`KK"pL3x$ԷRo{6j8mctZ$k';XV,T0P#Lw-4TM\Q9ZZeZRJ,4dj, R *Lfcs˗-`kP@mkY AKQ!B2Ad *@BA Y[_/c|5rfbfDyt0]B@Ϗ|PK9Fy  9xmantissa/test/historic/privateApplication4to5.axiom.tbz2BZh91AY&SYe^'J>ߴowWn֘i ;ﶁhhMRѠDih4"i6J%=ʞ'=Q{T3OT= 򞡣@Jz#S  |*z)#~KRkr|_ȼs|bieo,k jp)Nzv+2c.|Qdpn S~V괈p"jϫXHsڎQ TufnlW#d*,hډ2oFE@^Ť'B9a2"#0F" $ [zZ0 ,e EoM~\ )t̶C4El N~H~xzR}%oHt }~64bH㘼ZJY, opst3uK48Ofw:5zFOIg}[¡~٩>ha E|PGK!BK̷= 2OgjȘx|>c"U7.:;՛}lMfWum$IfYb,7 FQ`?Qhf8nfsp?g?xP.ϓ:}z o6-AlxH^^h4).G BE֗ݨﺫjx͢MFWm BI:]͒84B4֪\ _6,|!J~]DK o7"Yvә [(8>'Y@ ?'S9ډ?Drd^lQAbVC_9;oNW (DJ7=˔LEd7-‰m hkk_?n Ѽ涊ێrpѿX3䑷9Fjb 9f[a!ЧdL\/rdɓ'wџ R&˝sg}[=!m rBaJֵkZֵQa?C 4:0pCEru$ȏ]@p\OD B Ƭ9#R'&dAuaFEiKJ0tpDo@:WX5?[TDZuTAZ 3mjK!m;i_[6$T8EpبB)b mbhn;@R7zLn"@:3d᫹*qm6Oyq6csG.gZsGeL.lܸ-Zvn ,]7Wt}VV_G]U}Vf<5E[F{a.Nڔ*۳NIcuf_L2'Sr4,\b Rm;v1u O21'_qiQlWvJ8qC8aub~\M.:s!83 L#4`0;}R؛= S7}gbܓx$ד3bR} 6 <3X1ڿt,b> Uiq n#u5jԡ'l}Ɨ[߆+0V$g%ZY<)lթoD5DIMڎg:5d+~6@Flu} !6h"8 D`( 8&,m!xئkr2h)ZɌNl։kxy=j/ 4:"R837&<&9$M\ !r `w0 c6A8y%^2t`?<819MѼz&O)zTM?Ez|e0e5,͊ ~ 56uMnsZ" Ii'()JJ"ؖSfşZU*? гˋcSG9kf40DAH1UњiY[#r̘ɂlE]v efSsftłyx+GE <Sfy}7bfɌC$&XU뷩8kw=wCU={:JRz/5oMVѪiUT*Fʙ /c륥|M0 WoRe#I)K'%A9IR$&O{gIbR qXJ/RU}LI+ <ԃM uu |DVw-R-I1^oj&5ձNT89ɭ}2pG_UfSq̗G^o'!8I2OC{a5gԹ88KnՃYǷg\Mh 9ڈ,86PdL9‘6%6O:6D\?Ay@MpꢣEfQHLF1.CڲV8۬y[jxLƥv{ěOYI=8SKBa /,oBa.҈i"P(_}dx(=Dxi OA?񾬃?w8Md 1)%D^"`MA{mQqRÑ;!wm| GepbkYw[@ @b#YVcCՉd)ΤrT{kD֦c"f dr)IiF:#:qN8cc^G5Yȥ;+Z Eʄ D D@D] C4%f8bLٮf̚ڒ V:Ö.)hw3jbIMkl:ZdɁ)4›R&7 8Ӥdc+bmp1b'IS]iĬY^M00ag q131C xdu~)V]f$ڄ[У͚MQ0O9ʴmmYp"v}$)JPNCj)NHZxvI1l`%%p$Y䵺kkխӭ$` ďio< <ݚ`L*vwp%)MQ0'דq'H_2u)*v׽c^YdOm/=4gmdQQzuSlH_2?+(멢` 6 ZiSz|oNAuT,98݃ħ*xu5>NQRRؐMxC~]Dwp6MT2(g>1} } gDRvHV5BD"')[1҄Blۻ" Dnm 敁g]L$ @-HbC7@ti+Oʧeo@l`IysmQ[Kl' R[:~۪(@Mᘩ^3j1,6k(V NC52`-NeLjW԰%Ja'H$ EZ-t^~Lm5;B'wuH$ gО$M!?jSJ&>>=R !񤴥/J(АRX&IH*H@~JH"~Z{_~"{(P\ԚxzOWo_x<*L&$id$R2y{)8x*BMހTIIbEG7U ZM|J (bWJ(I@p.4kjcmcefwr(gC2A:!& lG=mA $#f+pu]մ]9T酠WJ88<]؉Q-& h lJ`NzmùcM7ŤngwUڝxE.TU&P+5JgH܉TY]]" W  Am&ذ)-AD+C 7ݭ bi"(HNcPK9F4 4xmantissa/test/historic/remoteIndexer1to2.axiom.tbz2BZh91AY&SYH |֞8 |#taA(HP4iL1L2h i0&MhѣCM F&FFLT<ښdh4 @ hdCM414L! 2 d2`L#M00 5D&S6Ih'4h4M414CA ѦC  24B53BSh4d Zpp''e $؊+B@>xi ]!-KRgRQ[4=d,`@džYWk\d6ڱ QE زAi}\4%ʆѻԺA>([5}r}ejV x>wAI?R^ob8CiJkm_[̞h9JT-FYj1h`.MG;9w\tln;# ¡RXBRV`Ҧhyt~r1)ͬH;At`  Xdeb: T)]\&MNeknu1V\yG_5Bs 7rrVeKҀa!D*m9h2r )BX/J0xTZYv #ٹSK9{ ku}]'ܢrQ"Xza==* Tf? KY@ԎCyif&IOll+T;z{:؍@yG#)v&\(յ26l&a1BԮ2T]~ !H֮UcTtВ1OdwPUȡlVL;0Cs0ьˏGe. Q&Kb(LY/jlWKs _72h)sk],K r5& a-m 93 -\vlX} |:`cEq+8 1`BInMz 8VcgtEXsm Ջ d2-1LJxl_vѵ-k6UUUe k^h(`J܈KbBՅլ<3fFM_1 * &VscP8벉8,IaUm"" ڂ`} sQU ^ J!+$fd {Va"1"$`%R c|RY !$!_h,7V[267nRUyx0ѭ4|-JT:vY$pنؓ$KHX$u0VWx=54ڀ&f !-I#y@]ߟ$d#^@1u'Xtr,M6V4' қZŊŴifIlʬ  %:n4(f-Imv{+u =Y!G6] Qn1u.$-H;> $qgPYLɝF h$(&'65]'%lx֑IĆVu*C2Uzi/VJ!^Ҫɾ}xN2>7_2@ !/} )j:1LaI-vRZIein. (X$*(6-KRɫQ)&c- 1cY'/GMjT^T!4asF6/j}jLs>8L}օ$,\mp*4cO _.Z21a0} U $ -0"sLW+x98-2nz)f @XX-ZϐXĉXH 2 M+@՘КK݁z) $ YW#+R+e9!mޛK), E(` 0D0ŵUǔҘs5+D[1+|YyuJr7H*J( UI*B,g2GΆ\]9[`b PMmez$̚lK\[;ýkav\g7l`d4uĀPK9F3xmantissa/test/historic/searchresult1to2.axiom.tbz2BZh91AY&SYBBp ( 8!(2BL5))=GMɞ>N0e0 2 "IK $!N %ƈׄLHצ"}Wb]2'r:_>lRwRz}P'9-8)]c=צ K[\+JĮA˾z?d)/>ĞcuE 0<<ݶۗ %9ćsM_1Ѡ2hUё4 p>FFb HcĢ! YhQY"}O% as#uWŢO_::B=2ڍ3-.:ty8fhPĬ4]{Mޠ.|\Zrc9#!-$ 0b7!Yh]m1'آ` u:1 @%^ H ^ ܰGEDA0`4A̋l͹gu_]<r,KOc= %e BPC *ЬPL%!OCF2%z$:b  "$} &//Ûi:.lFf_;4򓋺)p\3ώ 0̤Iy(k0B%T M2*J"$hDD* ªC3["86ƪ6T 2 ډʿ=JpcB%a@@E&l4++ɜ`@E].h蒳`7J %eYZ#$21gƕܬ!dw۽,DDbklT(뀷l}:C!Բ3z&K*l4HZ5,a̓ U8oz3Qby کQTDJ+Ng{H~jF 6~y2irD,2CcD9L E mQE4ˍGْEN'd; Ds>l^ ״s#~ 0fE@-C񫞡5o9b0R2k4jMvW۱!9iFk1 d'@a`sy$ p -K/;W a8ɍyܨT^!5v@D ^6 pA)@0Ge诗1_, 3Ezfߏl3׍ i;@| xADwku`.nj^ Fz@SbցW`-qy׆ Stjv*PLqsU(nTjZ,dWļ[%;xLDF&R}Z EXDRobLGyQ#XU?w8XQUb9w{Գ֗/hފˋ/BcX,{`#C̑2Q4.Ѹ"eJ T5, Fha cZ-5U%U)IRSJK&~?H`;bm朵2$>,4Lnӽ3am~CRuUUUUUWmUޫ!.I z 5s?C;.Yeg[Eba ( , GxÕi$kqpcɍ]$&,@S;[2t$6±cǔ&" ܨj--P"VfBwDF;ƑUtt"[ s7r,P3W3h/>4D0.wknA |*ZU(rYKKQL , `ImEc)„@PK9F5xmantissa/test/historic/userInfoSignup1to2.axiom.tbz2BZh91AY&SY/^_<#!Ei zF4zi#b#!j@Ahc$2LiQɀ `4 сi FF b`L&&Fbd0Q FMFh?F{Ri6dzh 24M2zMc*2&tWTT$~pnWB4mdiսp4DH=^ڟB$@Z?Xjx"2P8/݌8 t8ĀVJ +j ;TńJ@l{CJWRO %bCE)iʯ2٢FZ\YN񰮾>FAE8 %4Ld4`&AɀF 4 Ɉ L52i&PdAAL 4 Ɉ L$MMOM"dOI 膞ښ=ANCct'bOG+A^A -mɅL#; qΆAF!̊pVR&d i*<>W;0!@w|TsA)OU/ՍbbmئOs@ft4 NU֦a` BnT|K|?PSCF#C7ARBzޠ?LV ƿ@DZ +YZd/pG/ %1 7"(k1SDT$ЊY)6 TA`h$d!4 "z ٴtr< g(`z~E_f=xS 1` |G{"< Ro))xDEmR~Όe$訇F;8B:PhYщ2q7d(wk-3=u7U^k`֓Lrm">3wxlΣ6#EéԶJ3RIz"l!w/Yd02@4?w$S .pPK9F\t.xmantissa/test/historic/website3to4.axiom.tbz2BZh91AY&SY, ٮk>w{ې^{悌P M|455 O&4ښ=2 ا(z)J0ZE7HFKĸ\ᗯB!_M uI$3UWחa2Q(93Pԯd.&aYa><Gh$.K\*eg[ &.ZȨg]5YB:ВpCe}$}8dNO8I R"v*̪MB㧜AB)"IO*#:dbHХ[O޷ս1#=,R5RNˏ~kI"w~'qZaˠ{WyE6ڏsҔ)N<A#>?8`1o#c?+ۧ>ڠ=A@px<&cpķg%NqLPrAf|A3MeHa Q bUL/%'t?g6^>^i{Ҩ5NJX7)>ZI{coF'"qYl1_̼ zG-R avZ mU\!xTa(Kyz'"cR0lfif۪KK+X,|kOn8Y9m )찑SH +'5!qϬ0#7[/B0MU$Le]8by[lŲ4p'x "L*P:BDZ"MӶ$=z 56Ŕƽe#qr((s*S=s5JX4s{W%dM4 zTԯCBǥ7ċ}ܟJ$j*FؤTR5gcק^,^~+4]#{K֥E^K,ƪL)܌`ɓ>5ƞzC`,L?&ǺynUzޚ_UMJrc4ry= qH#v$2h¥ɍ,ŵٗ H,P7И,ąddᆰۚi3筮ZV9>- wĊJĪ)#~Es9PG3JqىeɫM$m$ڧ򼜘5'* ֈfߎcڿNI6+jYg`ڹSJV"?0ďGiN5ȟu7-yQ>`7 6eMfwmʧ^]_4H}f49"B)OYeeYfbklg:l JEJQJYffG\52) fXoO _-/u zreN9v͢F3ej=GT#X9jzy3_$`9k%"$ΐ$vw4ĀυaglDL6)U.QMDK̒ 756>m:6|m-9$DK84utD˒^v#5DO`p?[rP+0mri)j6fme'Vuir28IEq x*JԲM\""hT|&ݹZO6 + QyQM\}B1Y5HyW3/ǖeoKqT/-RM(U65<`8c;z|ܖMzZ9s># J#&Աj#thA.]Uf+)!a$X Y1 sWyƥ"$iU^gL,]ۀϗx;:mj4l$}/O`J$!D`֑\YFfW*ʯK:n]P1w!k^+n"v rBˈZX_br*%z&5ρRRb\V4Զly$SS@G5npZawN>L" 4Jw,W_[Uf)p(Rd:Xuoբ)&&T?kaF9[8aͧ3R`y*\ B彲[!g^R6w.ٲډr\vU{QtduʻM#V(i,a\kFlO~C|RSLLPWދ7Bp0F+j:4•6uHzHHP%)J 𳽥쏊빭(]_si+eu}2?k6yXxE4Z^b/'/1  )'Adc\'u%/|1L)Vk1)JQ()!` <ގ~u&_q.*'idx\Ma0R4|-"ehlKl,:%#8i5Ɠ.a=Vgڪ#cY)s*G^%)JR)JR= M˱M^'[~܎mT7goOfH;>~V #:Xџ֮y=Vi6ԓKib¼5%9Z푃k]UJU$O|zXRk.)e_y1n{(ʦ4Գ:tgOۥ˘77wGYfFva Yhmk5ʚI-w?]zrKk[сeN F؝T^i\͖y._eS&swt"l'Mߤ##Uы1Dxciucgm晗a|ZSKg*i";746eS}~;I"k>ē$đ:*'IjwvLoؘ4BN?p*~n=y~}=Mo^dVv<̺r7MCTd , Ӕ&Aհ (sn-H f)bY4vM3MvS{yWu8X~}f6[-Tm/|0`4Dim XꚌ=df֩'Sm6M}Zɉ}k1UqrZַJf>d2p5Wc"Zfw{w7y-`J&f_s/%ʘ+DDɳ_U'Zp !kvY{wn\0QG:M c;Bȴ"V7I;''<%1\1U$?kgrnv,GjǏ_/fJJ6,M R%7;eL;7#{\; &i,eb>[㓽˩t4JdYİ9DrSd!:D䴉4^׋͸u8_WY''zh'+5fRwISp#A(GZ:5]N}܉*M2嘜*TY̜R{LY &~n^S6c2$5/xA !89%GyOe(m7z;G]\YnoG[&ʬY~] ׺V$8G>a lJ8(]6.cw׺M{O< rm&EZNF3I1NjFʍL5k`bm5w~I{3W0Roc=|U1*Oy3np#nj򔩊a*&51Ĝ.,Lv]U "`&b4hUU4<}iVM%fET,)rePjcp=P{|xkqǎmGF o;s^zYr.p|ɲ7!1CQL 3f1ƃVLeE&j] O"_(pv+tat1d[N!4/^=DN1Hym 7T90N&% 2rvvܿ;̆@T- ˫ÏEԓv~VŲ×}o~6XsUA;ب*`}9ˆp>Ϲps{:CO2~#y޼|*OTќfw:x 6q`fƌU-$ksk -c=rb|ɐ c ɀA3@(\BFՆ<#RxlybWA .>-̾(!B|AY@ԴVXѣ% {[+lbV|[amŲ1LtdFUQAQs~DvRU)MNwkBplq[7M 3.eNU-`gOCKdMsáP @ ] J}YJ{#iS]?ˊR{pa:ܠ4bu;#'A(oU UZp91_)t agY_[qNRRz'C)eBcZ\U bB@!g5X[/i7.? %) Dj 'Zt?+vuB\/{ٳUo Dֺ۱':P"Mezgr`/8.٥ћE{F85(I)}.bV%qkZ}#N0ql}M_֍2l8J_E[$ί+= [Ar0i#o+OQdj ߿A6O'(#@"%ldS%I{-g`er҇ܡnlgIofkB袩r, Xd_jZNث+ͺYat'E޳_>8[[XQ.Hha߲n{.et㩱+,ޟwYR1Ǘ~n w+ݓ$\j˄z+1φmAA9EmmP7ŊdbIPn&vX/r뫯Q'h2 uN_ͽqz\]-$dH5\J^d;\FPӘ Crp*€hp *Smv9 %E;nC%+:U`>M\H R%sfTXGv=_*RN"hJRtgH29Qu68יvc,*"D]?-E4Pz鋋?Og8TQW7!3y)TNW3u{ZqpL"$'[$ʉӪDŽ#dڲq,"ӻlAj]C:]eRq)//?t`QDKsbn5 ܴ?mϾ[о_Sev)ǏRrW_e-bd 3v}zOmsTIJJRIJJRR;͗P+Z|M ABl]HK;E>&MnKq٤Ehc`YL;uYVzRˬiC'>I;:TK5*E-eT<;}IȓO)'"Hsta0bN+}to0X\eX-*ͺ`3E y7ۧ6K.Z1LFjNqH*J*nA"qJĂBY( ۗ WrNl7m>@3(L|v<^WT^XxZD8/doJL 8XK+_4C%k[VdͿeټW2R)%JIhogs F''!}'=<SѾR%<#mFiQzO}q7>7btiJ$*HB q,8i%9n3NO!0YG@s3@QIc$wan14VYr#tsh;|S#<@fe5\ JB\IMν,WfC2A39T88ș#N ].E !Ae$ =./pDr'y^̶Q<y6д$t$of&q/+d: HZ[`O&eE6+LK:72)<.'z}}]KV)emרΧoZJ9N3t)S),((RȦDاrYJ,q%Y^fbԴIU^̞vi"hF@&2im6aLJV2Q٭ fQ^݋Q$CG93=cA)2 GN)sr^Sxn=ArM0Cw1P /b!XzPo;i;ܱkbrLŵf[!Bo{|*8h6N?}Zt71̶6eD@NE܈^>֩'`hrv,2J 3f@L˸Ђ2™/FヵTTXϩz>BEp\Z$=i1!JI#Q%B e$ 3FMtpĈ1n LC8P!(8$8M DA66h!ɨQRR B*-2 8iqUE"(Ho]7PK9F|wN.xmantissa/test/historic/website5to6.axiom.tbz2BZh91AY&SYv'\<o׷NIgwnϾO;# 8)6҂- (3T`DS56MzmS4OF "f6ISz hh4 &L&4L d𘡊zMCF4@=GES @h@ 4 4h4BMMS5= Cje44 'a3#)4`#FɈF2bhaF!4C ja@=A @d&@M2m@2SښmQzjiOPhzirp;QvӖ0Z  C]Y 왡B:F6fĿI.QhАyam:0]C{}}@ Tqe ol(׵R8>>\z55{l7p_j?H-OuqRbv[ү%x>x].Ǎ{^dFDoddsOb]j}%ĬyؚPT~&Ǚǿye~:_m{ē՘6'V4ϺXdWe܊#4ܧ[EkĊDVRUe\J,M1m.SoW1`h RФ5Zbӹ~c q~~$iֈ鍨PzZH5D.T@Rkے"NS^_3RWxdZ_ҰQMQ_E'Yx k,ۘv[SJ{N鲭mk㌵4L?r(Uœ[銴ѩ 'xS6ǭϧfQ5IjĴg(YPG_ޚhjiiиL֔Y DhĶ&";& G]f́V(lZ$`x#TPH%G Cք3^Ms# CoOT\\H=NRRd8P/kr/hYΦp4ےjؚȾǫ ]JHXfvƩaxuV k3)!7!yBs|.u몵teR]2\B3WgoF -Ձα0 cEe:i>ddEZOc8Aٗ~04pg_\d Td #t(0M v1ꥦz*GOaDDvt3].aĚZ9 ?Ѫ ;Tlө~6|guQT.v[&o46.^H;DOqjRƣOԎϫl>f?ΉdfI4,/`* M VӑfU< ^6[pYYʞT)RJ>l^{9geʦg^[mfV+Q'E^6e(HJUfkp4X.|l/bҢ\,IJ7`^fYtbi1zZcYǍK[qMK6Xt/Y}< T }c/z'k+ɴ&c 5[#dxY)JRuJ|+z7|.y+eG+Gyg/(%4D`&aU;?s燗h*t+r۱˥>rK(R^߮6U;ZՔ?)uyowϓ;ޒxTCR%!H=FN79|/?RD?L,@ÇM)qgD ƛ@+cpz os 1F h1b=,놎b~LNDʩ2ܾD, 􆝚[,zLؕۻh  iiR75XI"Wp"T+YEj̴bAg)Ē}9h~MIeΒQ"'1,z5# T@ٛM۳z=:/.lM"LWjLNGjּ >:$Pj$?v_n[󜲎lhP~:6sUɫQz1KBXDp`)18^N_]q]غia 7ޣ yh,uEЍutL q^eM*SeضU8mD B2h |]/gm3,ȕd z5(q XƌDdAғt&%; z¢IItT!go&c`e%ON; X,@ C t>V=m=zkSsXڶ)w>gws/ӥQo?fdS*5o͸5M u`eN!Rp`.߫:w"ZhF8ˋթkt0ͬBյ[%-)5s`&;Rtkfx& @ "0"@"Qq@eNFUFFq_{48d:ӵ8Rzʽ'V~1 '{ TKHURm|/qm{OF, Ǽ,gM268 m7U̍JR<9ċC8@'4- HD}H4%)Jw;V1a $&TYn޿OFDsHpGaNE"Q10oaos.. (" =x\HD)Jij)Ц *M#ܠe $Ԃ&0KRg6[EB.aɖ AA aQC#1Xơeǣ^78Kb$o+0(z:h,l_aJ@)5HZpcTNJ'teqjُ/9l rMƲ7\*sꁈ(B R%))E)O7AڜfUyYlN]^ȣŕU3eX.Y|.ۦăX7^ΈR*MA\ݹ.IbdzgsasǭrN9ڕ*R%)Ms`Xgngm%!RN RRL ,YK.Z;5- W3)$rs7И 9'WO?L͗^f.kB̅-DMGE{m)iNiئ\)"30d\޾{WWy7S9f/ڪZm%Y;Q '5hdKe\(e(pDcIޛlL̊ʒ3*ʦ V)1w퉱7ИIʼnW k +9 ,H4n4K6&3wq>ىωɌ62 Dv4oFfe lHH1S@ (1S̻D78{,t'Ѫ=9'ARysoGKTFׅffjoxNR6f uj5Dž1< @͎kY#aX 0 6pRjݬ{=^Gf˥.MI,PZFy17v9!y'xxޒ1U$m3+[fje9LˌWK]}~Ko'~_1("RB IJ\K='Y%!Zx=|a7vgOyy82v17Sqnw,˗3\hZRS_˄v[>nŷq$̤r7,)$ rgEtn,h{_O,˭Pr&ʤ),De0r^7Cӱ>ko$uGu&wN,ӦY)JH(wX١Jp&l\s˟xK^:TNZy}Re 6!y jl$BJ8\ᬳ:lpSm`WlfW6%^ Bx;0dCz<*b"uN9\Sq96&vۤnV*s[ȋrwaljr̬tد_Gum^^?=l}t>e4n$T#իoڿN>頑޴GIfu.=W"NF \墚F X%;M,$YH2c 0`P3$%uIvcny;؋.`e`J\9O0X  ʩ`CL:VMij^D6)oʈ^ρ^жRO^[eihZ^Dk=HpbbNRbʍ:'_lsX)6=۬mr\ig[5U%}>ߋI/jTbRXI{qlF HkzH8S BL%rđRTYǂ2)=f-'Ee*jh^K#; ږ.\LL-U{"x5 <3hY 2^ɝPɨxZ(sŋgމ 6^w5y%< bD$xr9z(E,^xAs=tPýy={֎Z>4NkYӁ¹!=?G4˱ڋ&9$vQ>)^ՙѮRNljI_iN?#Є}.\ΓG޻eOO@3bzNIp6UFN͒Πt  !Fp{.VLU%o{m˛K:^wmkS1'r/öȉ:is^|3{x:wwvu٦Vuy%nkKTN34ckBG2j\.Z5?}PCX 蟘 )9>jSc0 $"eGVpi4dMLQ(chhSJHB8m" t M M&i h4hJQ 9wޓIl^ۥɄkxcg"(HVPK9FJgg&xmantissa/test/integration/__init__.py""" Mantissa integration tests. Integration tests cover the behavior of the combination of units. """ PK9FTŕZdZd&xmantissa/test/integration/test_web.py """ Integration tests having primarily to do with Mantissa's HTTP features. """ from zope.interface import Interface, Attribute, implements from twisted.python.reflect import qual from twisted.python.filepath import FilePath from twisted.python.components import registerAdapter from twisted.internet.defer import Deferred from twisted.trial.unittest import TestCase from nevow.inevow import ICurrentSegments, IRemainingSegments from nevow.context import RequestContext from nevow.url import URL from nevow.testutil import renderPage, FakeRequest from nevow.page import Element, renderer from nevow.loaders import stan from nevow.tags import directive from nevow.guard import SESSION_KEY, GuardSession from nevow import athena from axiom.store import Store from axiom.attributes import text from axiom.item import Item from axiom.dependency import installOn from axiom.userbase import LoginSystem from axiom.plugins.mantissacmd import Mantissa import xmantissa from xmantissa.ixmantissa import ( INavigableFragment, IOfferingTechnician, IPreferenceAggregator, IWebTranslator) from xmantissa.port import SSLPort from xmantissa.offering import Offering from xmantissa.product import Product from xmantissa.web import SiteConfiguration from xmantissa.webapp import PrivateApplication from xmantissa.sharing import getEveryoneRole from xmantissa.websharing import addDefaultShareID from xmantissa.signup import UserInfoSignup def getResource(site, uri, headers={}, cookies={}): """ Retrieve the resource at the given URI from C{site}. Return a L{Deferred} which is called back with the request after resource traversal and rendering has finished. @type site: L{NevowSite} @param site: The site object from which to retrieve the resource. @type uri: C{str} @param uri: The absolute path to the resource to retrieve, eg I{/private/12345}. @type headers: C{dict} @param headers: HTTP headers to include in the request. """ headers = headers.copy() cookies = cookies.copy() url = URL.fromString(uri) path = '/' + url.path args = {} for (k, v) in url.queryList(): args.setdefault(k, []).append(v) remainingSegments = tuple(url.pathList()) request = FakeRequest( isSecure=url.scheme == 'https', uri=path, args=args, headers=headers, cookies=cookies, currentSegments=()) requestContext = RequestContext(parent=site.context, tag=request) requestContext.remember((), ICurrentSegments) requestContext.remember(remainingSegments, IRemainingSegments) page = site.getPageContextForRequestContext(requestContext) page.addCallback( renderPage, topLevelContext=lambda tag: tag, reqFactory=lambda: request) page.addCallback(lambda ignored: request) return page def getWithSession(site, redirectLimit, uri, headers={}, cookies={}): """ Retrieve the resource at the given URI from C{site} while supplying a cookie identifying an existing session. @see L{getResource} """ visited = [] cookies = cookies.copy() result = Deferred() def rendered(request): if request.redirected_to is None: result.callback(request) else: visited.append(request.redirected_to) if visited.index(request.redirected_to) != len(visited) - 1: visited.append(request.redirected_to) result.errback(Exception("Redirect loop: %r" % (visited,))) elif len(visited) > redirectLimit: result.errback(Exception("Too many redirects: %r" % (visited,))) else: newHeaders = headers.copy() # Respect redirects location = URL.fromString(request.redirected_to) newHeaders['host'] = location.netloc # Respect cookies cookies.update(request.cookies) # str(URL) shouldn't really do what it does. page = getResource( site, str(location), newHeaders, cookies) page.addCallbacks(rendered, result.errback) try: page = getResource(site, uri, headers, cookies) except: result.errback() else: page.addCallbacks(rendered, result.errback) return result class IDummy(Interface): """ An interface for which dummy items can be shared. """ markup = Attribute( """ The precise result to produce when rendering this object. """) class DummyItem(Item): """ An item which can be shared in order to test web sharing interactions. """ implements(IDummy) markup = text( doc=""" Some text to emit when rendering this item. """) class DummyView(Element): """ View for any L{IDummy}. """ docFactory = stan(directive('display')) def __init__(self, dummy): Element.__init__(self) self.dummy = dummy @renderer def display(self, request, tag): return self.dummy.markup.encode('ascii') registerAdapter(DummyView, IDummy, INavigableFragment) class IntegrationTestsMixin: """ L{TestCase} mixin defining setup and teardown such that requests can be made against a site strongly resembling an actual one. @type store: L{Store} @ivar store: The site store. @type web: L{WebSite} @ivar web: The site store's web site. @type login: L{LoginSystem} @ivar login: The site store's login system. @ivar site: A protocol factory created by the site store's L{WebSite}. This is probably a L{NevowSite}, but that should be an irrelevant detail. @type domain: C{unicode} @ivar domain: The canonical name of the website and the domain part used when creating users. """ domain = u'example.com' def setUp(self): """ Create a L{Store} with a L{WebSite} in it. Get a protocol factory from the website and save it for tests to use. Patch L{twisted.web.http} and L{nevow.guard} so that they don't create garbage timed calls that have to be cleaned up. """ self.store = Store(filesdir=self.mktemp()) # See #2484 Mantissa().installSite(self.store, self.domain, u'', False) # See #2483 self.site = self.store.findUnique(SiteConfiguration) self.login = self.store.findUnique(LoginSystem) # Ports should be offering installation parameters. This assumes a # TCPPort and an SSLPort are created by Mantissa.installSite. See # #538. -exarkun self.store.query( SSLPort, SSLPort.factory == self.site).deleteFromStore() self.factory = self.site.getFactory() self.origFunctions = (GuardSession.checkExpired.im_func, athena.ReliableMessageDelivery) GuardSession.checkExpired = lambda self: None athena.ReliableMessageDelivery = lambda *a, **kw: None def tearDown(self): """ Restore the patched functions to their original state. """ GuardSession.checkExpired = self.origFunctions[0] athena.ReliableMessageDelivery = self.origFunctions[1] del self.origFunctions class AnonymousWebSiteIntegrationTests(IntegrationTestsMixin, TestCase): """ Integration (ie, not unit) tests for an anonymous user's interactions with a Mantissa L{WebSite}. """ def _verifyResource(self, uri, verifyCallback): """ Request the given URI and call the given callback with the resulting request. @type uri: C{str} @return: A L{Deferred} which will be called back with the result of C{verifyCallback} or which will errback if there is a problem requesting the resource or if the C{verifyCallback} raises an exception. """ page = getResource( self.factory, uri, {'host': self.domain.encode('ascii')}) page.addCallback(verifyCallback) return page def test_rootResource(self): """ A sessionless, unauthenticated request for C{/} is responded to with a redirect to negotiate a session. """ def rendered(request): redirectLocation = URL.fromString(request.redirected_to) key, path = redirectLocation.pathList() self.assertTrue(key.startswith(SESSION_KEY)) self.assertEqual(path, '') return self._verifyResource('/', rendered) def test_mantissaStylesheet(self): """ A sessionless, unauthenticated request for C{/Mantissa/mantissa.css} is responded to with the contents of the mantissa css file. """ def rendered(request): staticPath = FilePath(xmantissa.__file__).sibling('static') self.assertEqual( request.accumulator, staticPath.child('mantissa.css').getContent()) return self._verifyResource('/Mantissa/mantissa.css', rendered) def _fakeOfferings(self, store, offerings): """ Override the adaption hook on the given L{Store} instance so that adapting it to L{IOfferingTechnician} returns an object which reports the given offerings as installed. """ class FakeOfferingTechnician(object): def getInstalledOfferings(self): return offerings store.inMemoryPowerUp(FakeOfferingTechnician(), IOfferingTechnician) def test_offeringWithStaticContent(self): """ L{StaticContent} has a L{File} child with the name of one of the offerings passed to its initializer which has a static content path. """ # Make a fake offering to get its static content rendered. offeringName = u'name of the offering' offeringPath = FilePath(self.mktemp()) offeringPath.makedirs() childName = 'content' childContent = 'the content' offeringPath.child(childName).setContent(childContent) self._fakeOfferings(self.store, { offeringName: Offering( offeringName, None, None, None, None, None, None, offeringPath, None)}) def rendered(request): self.assertEqual(request.accumulator, childContent) return self._verifyResource('/static/%s/%s' % ( offeringName.encode('ascii'), childName), rendered) def test_loginOverHTTP(self): """ The login page is displayed over HTTP if there is no SSL server available. """ page = getWithSession( self.factory, 2, '/login', {'host': self.domain.encode('ascii')}) def rendered(request): # Sanity check. self.assertFalse( request.isSecure(), "Somehow managed to get HTTPS.") # This is a weak assertion. There should be a better way to verify # that this is the LoginPage because that's what I care about - not # what's in the template. -exarkun self.assertIn("login-form", request.accumulator) page.addCallback(rendered) return page def test_loginOverHTTPS(self): """ The login page is displayed over HTTPS even if it is initially requested over HTTP, if there is an SSL server available. """ # Create the necessary SSL server SSLPort(store=self.store, factory=self.site, portNumber=443) page = getWithSession( self.factory, 3, '/login', {'host': self.domain.encode('ascii')}) def rendered(request): self.assertTrue(request.isSecure(), "Page not served over HTTPS.") self.assertIn("login-form", request.accumulator) page.addCallback(rendered) return page def _createSignup(self, at): # Create a signup mechanism at the specified URL installOn(UserInfoSignup(store=self.store, prefixURL=at), self.store) # Ensure there will be a domain available for UserInfoSignup's view to # use. This isn't really significant to the test, it's just necessary # with the implementation which exists as I am writing this # test. -exarkun self.login.addAccount( u'alice', u'example.com', u'password', internal=True) def test_userInfoSignupOverHTTP(self): """ The L{UserInfoSignup} page is displayed over HTTP if there is no SSL server available. """ self._createSignup(u'foobar-signup') # It is not necessarily the case that we want /signup to require a # session. However, that is how it is currently implemented so I will # not change it now. -exarkun page = getWithSession( self.factory, 3, '/foobar-signup', {'host': self.domain.encode('ascii')}) def rendered(request): # Sanity check. self.assertFalse( request.isSecure(), "Somehow managed to get HTTPS.") self.assertIn("Create Account", request.accumulator) page.addCallback(rendered) return page def test_userInfoSignupOverHTTPS(self): """ The signup page is displayed over HTTPS even if it is initially requested over HTTP, if there is an SSL server available. """ # Create the necessary SSL server SSLPort(store=self.store, factory=self.site, portNumber=443) self._createSignup(u'barbaz-signup') page = getWithSession( self.factory, 3, '/barbaz-signup', {'host': self.domain.encode('ascii')}) def rendered(request): self.assertTrue(request.isSecure(), "Page not served over HTTPS.") self.assertIn("Create Account", request.accumulator) page.addCallback(rendered) return page def test_userSharedResource(self): """ An item shared by a user to everybody can be accessed by an unauthenticated user. """ # Make a user to own the shared item. username = u'alice' aliceAccount = self.login.addAccount( username, self.domain, u'password', internal=True) aliceStore = aliceAccount.avatars.open() # Make an item to share. sharedContent = u'content owned by alice and shared to everyone' shareID = getEveryoneRole(aliceStore).shareItem( DummyItem(store=aliceStore, markup=sharedContent)).shareID # Get it. page = getWithSession( self.factory, 2, '/users/%s/%s' % ( username.encode('ascii'), shareID.encode('ascii')), {'host': self.domain.encode('ascii')}) def rendered(request): self.assertIn(sharedContent.encode('ascii'), request.accumulator) page.addCallback(rendered) return page def test_defaultUserSharedResource(self): """ The resource for the default share can be accessed by an unauthenticated user. """ # Make a user to own the shared item. username = u'alice' aliceAccount = self.login.addAccount( username, self.domain, u'password', internal=True) aliceStore = aliceAccount.avatars.open() # Make an item to share. sharedContent = u'content owned by alice and shared to everyone' shareID = getEveryoneRole(aliceStore).shareItem( DummyItem(store=aliceStore, markup=sharedContent)).shareID # Make it the default share. addDefaultShareID(aliceStore, shareID, 0) # Get it. page = getWithSession( self.factory, 3, '/users/%s' % (username.encode('ascii'),), {'host': self.domain.encode('ascii')}) def rendered(request): self.assertIn(sharedContent.encode('ascii'), request.accumulator) page.addCallback(rendered) return page class AuthenticatedWebSiteIntegrationTests(IntegrationTestsMixin, TestCase): """ Integration (ie, not unit) tests for an authenticated user's interactions with a Mantissa L{WebSite}. @type username: C{unicode} @ivar username: The localpart used when creating users. @type cookies: C{dict} @ivar cookies: The cookies to use in order to use the authenticated session created in L{setUp}. """ username = u'alice' def setUp(self): """ Create an account and log in using it. """ IntegrationTestsMixin.setUp(self) # Make an account to be already logged in. self.userAccount = self.login.addAccount( self.username, self.domain, u'password', internal=True) self.userStore = self.userAccount.avatars.open() # Make a product that includes PrivateApplication. This is probably # the minimum requirement for web access. web = Product(store=self.store, types=[qual(PrivateApplication)]) # Give it to Alice. web.installProductOn(self.userStore) # Log in to the web as Alice. login = getWithSession( self.factory, 3, '/__login__?username=%s@%s&password=%s' % ( self.username.encode('ascii'), self.domain.encode('ascii'), 'password'), {'host': self.domain.encode('ascii')}) def loggedIn(request): self.cookies = request.cookies login.addCallback(loggedIn) return login def test_authenticatedResetPasswordRedirectsToSettings(self): """ When a user is already logged in, navigating to C{/resetPassword} redirects them to their own settings page. """ prefPage = IPreferenceAggregator(self.userStore) urlPath = IWebTranslator(self.userStore).linkTo(prefPage.storeID) # Get the password reset resource. page = getResource( self.factory, '/resetPassword', headers={'host': self.domain.encode('ascii')}, cookies=self.cookies) def rendered(request): # Make sure it's a redirect to the settings page. self.assertEquals( 'http://' + self.domain.encode('ascii') + urlPath, request.redirected_to) page.addCallback(rendered) return page def test_userSharedResource(self): """ An item shared by a user to everybody can be accessed by that user. """ # Make an item and share it. sharedContent = u'content owned by alice and shared to everyone' shareID = getEveryoneRole(self.userStore).shareItem( DummyItem(store=self.userStore, markup=sharedContent)).shareID page = getResource( self.factory, '/users/%s/%s' % ( self.username.encode('ascii'), shareID.encode('ascii')), {'host': self.domain.encode('ascii')}, self.cookies) def rendered(request): self.assertIn(sharedContent, request.accumulator) page.addCallback(rendered) return page def test_defaultUserSharedResource(self): """ The resource for the default share can be accessed by an authenticated user. """ # Make an item and share it. sharedContent = u'content owned by alice and shared to everyone' shareID = getEveryoneRole(self.userStore).shareItem( DummyItem(store=self.userStore, markup=sharedContent)).shareID # Make it the default. addDefaultShareID(self.userStore, shareID, 0) # Get it. page = getWithSession( self.factory, 1, '/users/%s' % (self.username.encode('ascii'),), {'host': self.domain.encode('ascii')}, self.cookies) def rendered(request): self.assertIn(sharedContent, request.accumulator) page.addCallback(rendered) return page class UserSubdomainWebSiteIntegrationTests(IntegrationTestsMixin, TestCase): """ @type share: L{Share} @ivar share: The share for the shared item. @type sharedContent: C{unicode} @ivar sharedContent: The text which will appear in the view for C{share}. @type username: C{unicode} @ivar username: The localpart of the user which shared C{share}. @type virtualHost: C{unicode} @ivar virtualHost: The full domain name of a user-specific subdomain for the user which shared C{share}. """ def setUp(self): """ Create a user account and share an item from it to everyone. """ IntegrationTestsMixin.setUp(self) # Make an account to go with this virtual host. self.username = u'alice' self.userAccount = self.login.addAccount( self.username, self.domain, u'password', internal=True) self.userStore = self.userAccount.avatars.open() self.virtualHost = u'.'.join((self.username, self.domain)) # Share something that we'll try to load. self.sharedContent = u'content owned by alice and shared to everyone' self.share = getEveryoneRole(self.userStore).shareItem( DummyItem(store=self.userStore, markup=self.sharedContent)) def test_anonymousUserVirtualHost(self): """ A request by an anonymous user for I{/shareid} on a subdomain of the website's is responded to with the page for indicated item shared by the user to whom that subdomain corresponds. """ page = getWithSession( self.factory, 2, '/' + self.share.shareID.encode('ascii'), {'host': self.virtualHost.encode('ascii')}) def rendered(request): self.assertIn( self.sharedContent.encode('ascii'), request.accumulator) page.addCallback(rendered) return page def test_authenticatedUserVirtualHost(self): """ A request by an authenticated user for I{/shareid} on a subdomain of the website's is responded to in the same way as the same request made by an anonymous user. """ # Make an account as which to authenticate. username = u'bob' bobAccount = self.login.addAccount( username, self.domain, u'password', internal=True) bobStore = bobAccount.avatars.open() # Make a product that includes PrivateApplication. This supposes that # viewing user-subdomain virtual hosting is the responsibility of # PrivateApplication. web = Product(store=self.store, types=[qual(PrivateApplication)]) # Give it to Bob. web.installProductOn(bobStore) # Log in through the web as Bob. login = getWithSession( self.factory, 3, '/__login__?username=%s@%s&password=%s' % ( username.encode('ascii'), self.domain.encode('ascii'), 'password'), {'host': self.domain.encode('ascii')}) def loggedIn(request): # Get the share page from the virtual host as the authenticated # user. return getResource( self.factory, '/' + self.share.shareID.encode('ascii'), headers={'host': self.virtualHost.encode('ascii')}, cookies=request.cookies) login.addCallback(loggedIn) def rendered(request): # Make sure we're really authenticated. self.assertIn(username.encode('ascii'), request.accumulator) # Make sure the shared thing is there. self.assertIn( self.sharedContent.encode('ascii'), request.accumulator) login.addCallback(rendered) return login def test_authenticatedUserVirtualHostDefaultShare(self): """ A request by an authenticated user for I{/} on a subdomain of of the website's is responded to in the same way as the same request made by an anonymous user. """ # Make an account as which to authenticate. username = u'bob' bobAccount = self.login.addAccount( username, self.domain, u'password', internal=True) bobStore = bobAccount.avatars.open() # Make a product that includes PrivateApplication. This supposes that # viewing user-subdomain virtual hosting is the responsibility of # PrivateApplication. web = Product(store=self.store, types=[qual(PrivateApplication)]) # Give it to Bob. web.installProductOn(bobStore) # Make something the default share. addDefaultShareID(self.userStore, self.share.shareID, 0) # Log in through the web as Bob. login = getWithSession( self.factory, 3, '/__login__?username=%s@%s&password=%s' % ( username.encode('ascii'), self.domain.encode('ascii'), 'password'), {'host': self.domain.encode('ascii')}) def loggedIn(request): # Get the share page as the authenticated user. return getResource( self.factory, '/', headers={'host': self.virtualHost.encode('ascii')}, cookies=request.cookies) login.addCallback(loggedIn) def rendered(request): # Make sure we're really authenticated. self.assertIn(username.encode('ascii'), request.accumulator) # Make sure the shared thing is there. self.assertIn( self.sharedContent.encode('ascii'), request.accumulator) login.addCallback(rendered) return login PK9FOu{{#xmantissa/test/resources/square.pngPNG  IHDR7~ pHYs  tIME "&tEXtCommentCreated with The GIMPd%n IDATxڬ-mQ\_D]0;Y"̞ P? @U={R߿^ǞTg ?]HU{e>+so{=zA/)PM_}# 5̭*|?r}a7[]ʺwVt m~'¯8>]I B@/WH~޳tSxݏ=? 百*C35noARN[]Ϊ3M~eϞk!Ss6ϝ=vO~i(~ 0qs?'Ҁo,ApYu>t{{ݫ<׽EQo=[7_ɧn<%] }Him>#.#EE5cOw}{ w65-ҁ?`Oץuw kpK#[}բ~eWEXeJZ9OkCgAQyuJ?eX~k5"g- k\w[n-]'[^[%ee_*Z.[~F]Q(ܝ~}7?+'d9' \]>ˮU 8_iVCWr^h=OJ^mZgˠ| A$Ef~k_#G [ߪ[z :/~rZ>=O|.}9Ր|/ݡe}ڤ%Wȣs jSOsқ_/<ߚz~ ?+K߯9GOǿLz9+.p퀔()A+^gkDso}yB=&7NIG}{suiZ_V׌~9zmYGQo6J>s|GYH?Wg<^.zt3Ő&sMݷyV߇B- } sdd)CϖZUzEQ \%VӚ(UB+λ#<_m*P]mXY'G^-#EGzS򙈾3`"vBj^Z=&szJk ' Mjx=[j<ngBzHH3ANG?ٵpHw)&YxTI0XqROY/uhOʹwԝrkH3.kR4~~^=P:U}Wyb3ӧ]TRѻ>[!KR>2ʩh{>!sY/ר(Yk* uV2Ƶm n&=J/} tLlA5&ISo2Lt'\Hvi[{]vJm;O~0)l=/kIkNۈ9-k%؆Q8OE[`z74nѤz)])NMgfOӡ{}o?ͼBM{9ACM{'Z~wm8{]DH+wF̎;ν=eggL/m>TX~M[+Ogus- v D "qd;d+2L=6Ǥ jݶ~oҷ'C.cQUVf:@]iWL@޼7z3ٙ÷KM-޻sn<-<7ӳ45ޅ=/wqPͿd`w4(b<{:/"ClbAhV |Jr[nvǩ ׹oБ튯P߰{ۗo*#nKOƗ[.7jW9S@h1Um|߿4=*&.zOQF6nYpyv$Qa7f(SBܭGL#;T3tlb+FxT{rWYVG~$ΩvM/vBǯ|t6Ys_X_Z۟.X2C=t"H{.w+ |l;!+.t<\Y\Y _pIS;wv*F(:g(][eqQѲ*mQToJcQثHڼ+lAu%w.3zl9O;DH"APGݬfUX+ {AzvW *uT-=dR հWKvǸNnBS jLtN^VKJ5mGf*UF5E*}LlT?:Vt-39hȸ`}`^CmXml݈:-ݸ O9iKzATZsL,@ҋNly?n30U.?(?/"< 6b -)}uv슎 =薂*s[Ut5|Nv C&cw퓨S-?kwJG@N-}EOfǺx^T~ /36Vk\SMOk w 'WKs8gd-ԆYĻ# /0ϿP=]Zt4\+#r^/. އuk/ BKR/s TZfKڡO:?}^Hv^ O'UͪGjzO ٰ͇^"PiG|)Z;s~i8l mO#6C 3]#*% )EkU)w;@6Q3hۇ\fzvoWo_ʠU2㡐>QIxsO)#Q>><9¥r<h&nU ^KnS}JYG:ơ ‹|G2OZ*)x']!cuNowP݈I S;Yi"|iϟG 6`iQ9D G  )\x:y"b_uT6w&TЭ.j;`,xx?Av+9!'9斍NndnzZ2?#:j{ CĆ֕7&n).v>m*1̳@ė9k#dMo_jq# jWxG#!;=eҰ7% GvI$v'WVyL*޷HzEƒ3̡=XHXkx*z¹6e@; 74eR `:"k̏DZΦ>*DusP,G*$)5tF4*Ĕ">l!(,GaDⷹa$  F9BٙkG^*9qL^p9퐂E#o6v;$҂!ޝ]D|`"'}p kTb礛I^@KQQrudA$9`RR*rV[_ѝk.o h 9ȥn a&郵*/R@ЮCܖ][6%0X`o,ڀ^-'IzV㝼>wDSy}ۥŸE#Eb>Д쏶T=Km+HFYj56ͦr6Qup )YվUU길?MJD 3>vHئز4_$JjΕ%+۬ы/򦶠bKKI*- byZ#uӓ{c!է>(BsEto*+>YH GuKK*:48Vn &p ̚KI*a!eT$ txڣ.zP bP8 "J`zV-Ob!x@g;%c@vgR( U9KVА#[|\WgiO<(m>%$sh5gnyOrܗ].v?ﴶE3.)u_/X#]sBr{#Vjב.f,و۝w)3#|G%]sdI15턚uynD9CCirj~{w'Pr Owm38$ f3pf;nK=cLyڧb+Ľ}5kw,g+{o|]8s1SBWmtgMX4a3'v͢KtʶL̲qWOlqITT!KUz15NMFߖ}&|5*wW;LLdt|qm %Iâ&5PS޿gIwSnāsI;@9Sݳޢ$UTFxzEձOJEOJq%aar);״d渴B-=+*rw9]^(% WOpGe !'nj2ے\UDF9?h[{Ly%c52 ;qRRV guyMa,3P5@KJٖPLGv:eb^mּScloߢQ׶8TfW К y8x;.QtJVsbWs'kßbB+@Gma?:4<7p<} b1uzF- eK|"D (L.[XڅB4?P%O2\NN3mZ1vgtyҮƉ3B# ^dlS'zZQ,O5PKHZXM-˯muJ3ܴK^a"L m: Yt'ŭș Sr6m-yZ݋O_o4Kb쉈>28=xvܛqB'mkEl7jre IDAT{t!~,Ћ邤^OO>ZQܩ ֿhX?I&wE-Ùh˲*Gıwh&6Y'hŕۃ@7<</);-^4eTG.ȟiv k̈́dߩ/smN/( EDG:ȬoQz .j)݃s•.߇:;b=m#q*g5z!;"Hc $LFM7LwB?ґyCr Th(nt%gJJoy(>%QVUaYu9> )zW5?HkԻnOZ*g~D_m琊kU 6Ք2~NY'')$&\emؐ-^Z Ӱ0@(]pf84kўU/z4nÔ>7,hubM$iBM#s8b5A)^m`slG G/nw)!v!Tn,F+jnxFc` q2mK%;?1_˓[;x plPL!&>Q[furk2)tp 8M?iƵǙd(e`L zfH$ϼc\a!3۾Dvq+%[dnᡯSn1om6xϐzSh)v̉5 ICGGa~a7q {q\k5$d}Y24=82JţhMJiCh*bY+̊FEH@Yv J{6 kY$Q(rhje@*nѶB{V} 4#PY?F>%\ S uJ5z3 mWnBLaQ(2[->"楽i:8x ݛSV۲9$>m¸6#E[]zHRh 1EU2Tx9AVn)m~N!rA0wINZ҆X1m}œ*H5^@?Z5|2 hbiP I"2W3.9!tA䥙 OH7^o){z3FIҹ+Ă-Z; }ƊkkJ|>3>t\D=v.%OjE29̤p#aKv)W"2Dg܇ |Sp"$t$ y@n\u |ӵbnL9?|(u+t'@A7g ޥ' L/e1gcw:!ٷjoHz,6 =?+tTKHJ*u$Ŗ Mg|7_֚9́> Lap`e'<'-[o W8ZF}ű D4OiFgvSVct^?Wo Z86Ӟ;ykM2ߣZLe ɒΦ&movV᪛.`GgpꮫVT`R1\Gg9ckb6s#f'}qԳM/rV&&MݔV+6\"+έQ=)p&te'BT .)zOQB"^oa8:im,@ىGE:1Pz=*且/he@TEOl!`4֓(UJu[YӓfPh&'5S%ayɔ$$Kl̮XD@9)6D;s JtϠfXG J裼ݗ1[3zr:VCh 烔UfOnEUVl/EtAC[L"eA$2EiC-Q 错0La>чеLb"ySEl;(zGiB>u졅~K3=@%Ju|WqJLPQɶ82"/tB8Jk k(6"hDU:o=5YZ0ZEH0`]ӍLAϵ߂]8Q`꒲ј'Qrob ߚuYR2zdIBu֐^͝Rz*eg8(q< 7RjfysS{9MHV?@ 83"#ETE^H<>d!4mi[yJuvxDhZK,^;1ΏET&ҙe]jkTM[U9g7chH*98i"4\zd0EƄʊwQVU6&z*D*S~w X*޽Ke}|j >qQBI047;GmbrH]Z^4*O JJPaaòQSd@(4qm@;sa*m 9%2\\k&M95AjzI:\ ɾa'T;.+qvYxcgzۨ,5${$mYh'|B͢ygT>9FaaውσKoz!;i]y2H[=t~(ҮV6Vvp]@(g== #8H*)~`<jr䞺DsrMv‹7"vu28+$a!)N(`~T AI[K|kٸuX :Ku4iU^nVޣ4IuPmr:ut𖌂yCBL>Ve)u}u|#23(jy}or5m 񵈭)ǔ^(ߕH<ȱ5W>2g2ND$7}|SU6&~]RW'V1F%%Fƹr6;n\ƏwYI+E 0#7&FAX6aT3j{NU@m Hٜfkq]mn9P=52 ] ޙBq!ޤH|0Nf (eb 2|_^ RP:W*gC7$rP<90^C†LGKe_AB,/\L-ek[7 ٟ(t6DȅG:BA;IWLK]Lk0j3ɦOR٠/,a)(FPXE.k=fpBŧ K ZPe)I3j%!&`nS˺;^[#ms57k4dz`{XB MŹ K$h=4r Y|Oy f}$v#?Ckj>na:O\3ڜ=yxzfKY鮠] !KN WAK51b+sjw̬-Ȝ]|, ϓ'ѣ ,hshN{k~:rz24uUx`ľPd .kಜeiUy| $B|O*/M['3Z:Epq`BsM7բz}81R|m@Z7C _#TG ly!]h%8-&t&"l2yi ZJ֯VG,;ryyrB$~M 0ʚyWz I' 7HaSlx9QP<9,.k 8 X'PΗS!:}z:Y?[gkC9#uܸUcDs{4mv' 4f"S;e[,&sB3=Fe" JDLA-D$x^_#kĿ/a 1 # ȱ-Y7m"bAB{X \Ń =qFFCWw(W<#rW?ip40;6T^-:HIJ.\OGVQ.xc[YTny͗'Ղ唓VL Oι;Ey`XO)ãh2b?hb8ЌMJ"f#ԟC{%ű!^KAW$D:&})p]Y -tf]e \MO nRsg<'(|Ե03ǖUj"9x|U!Kg+͟1m!2j`̕o6iGu&QmqMeܩH˧B(U= >`a#)nS#b)YTSF fڲ,{R'8?HI.R?9ĉfGe6iNs\]q):$v(SD}>86̒oߗhL ") {Uh#}feBc}䤏"1)ˢ8^*ҡ x\/\UTtY+Dvd :ÙƠ3yP4_16F/f zƣPR Y2_w7JU(`G;Bh&(TZ^D 3#v؊8Sh$0<С\2hf@X\E"/N BS|s¹#я55ew*4:\q$(Kb9~ѕ amfN'k{J6VKL!r;&6}Y؉+Ϯ 9VQ0oKdyԠ ]7 ;C0s\AooɿfBtLG3c%:0=E]<\%hxe47)}{[].0iڿeE %+Xa gڌH@{6ou+x.>L8#@w%-#FqTGQ&֡lB4 rAd hq{A;٦QyЖda/YT5WHφku}.HP3P!BO5Us*;ʒ+^DO)hI 4Zbj u)Χ=ߪ =pn]HRͦZ-{cqed!eK{XQ!%ӻ1H0YAȲ|ۢƪ6ʬ"85jA%dRrvLq_ onݨ<,H$Tҹc˳gisb-j;xlTfT6Q ~2X/9ؠveXF7\ h0q}<昫0%縫W{ߏlyD|w3lA# X/}*\ڛm+nٚu&H[Eq#VzhdVy[4`ӶbI5_#l$"p+Y ? }àQ;#Ox.V{'.h͠#c-B9+ajz=V\}&1l猞9u'[{k: nY$򩷌pcCmsC7WSwiP E?aMi:>Ft?NYzZ.j8e))H8F?=˭UN M[<8z! u[mϠOxs4v!ƃgB~z0l8^ t+aIu(_ڟr Ф_3[xC*bΚ65sPs}ֽۋ x(FXC]}mZ4DFBX"܈حHkyQH< VV7,^{娹]Sy6.';ZpO|9I }FZSad`svqDG`M[5iCu}Sz˟Ţq.3GT5J3س\eOB j(}ǺEؖT+B[)gr jt5ʽ4zzNR~9"FmC95c3v"3 /܁=^dݫR@3%!$^(XfgR) 2%K;l,1Zؠ/{eE1e>ONʠZo_~\ 6-m@_.-K.j.!zﰅ+XA m 7 mB4T<6d9ށ GlXGKI71dxC,lk-#žvڜnʮ 5C U)Ryb+#`,7;2-8=*#")Yʪ>/m;p%#?,6ېňgQV H!Z8D|܁1(v!9)RƘ5H%Y2 W0vXM/#.2e  b+*?? ~5Tc-8jsBd"M*747e=E7{9PSh?1B'_=&V]|O,akKXfS`e^:9}9ӹxc͔ XkPnDa+qG]I=H4sʎ$N}&?9dBQ=}-JA:XX4)gmKEޅȓtA>fq}wPnS7XA~,k jsYR4O'_:c }+5,E[HVD&W`AKB2dv/-X}d}:p0<Ņm|r6+L]45y$< Tfg]49/,M_lg+ty@Uı}L<גJY 7tQ &EeDj!$; \5m1S: FL}YG%.xI̗RG:Z0? K|vKE-p9Jʉ.kUc2 -j-K%Uꑌ xV >4XZt ݱ][1_ѓ| jJMk]Z!T#O|=˰3BArOVy z)vqf$MVWivNYRy+@,&!FSk yn_v>g =ˤ̲W)jtk3Unnwvzdl/>ogCᛈ;z`{g}wf0; GcAbz򿤲ovʳo)4ڬ':~f ?X'Xl}ں$tOnE$T&2g}F`].rьԴ58gb2` vNp;Aω2u~P`{I@~o$\` rD|fn@6Z+vrOBcPPK!,S ?-he,%%ɯ#CG^O^}{LFib h7mDTِ ?O29&с%[i-.-^0Ô_فMYӺiŌrӞhBtxE(|-1{gh-QGRк)̪wK^H۰\I{g.߅ zeC]AQyD&_|l/ }#bjgALqQ}BK׆ tQѨ ZhL&pQOnN/f&z#l'(ە7#ȑ8ۯ0|5[N8*!r"la" ZyrNG0 ~Nso7݁()4.º0Y񴟖hhL;i&ѷ^\۸nۉ 0!6_wHy51?6.N#"fuQST"kz+Y;,&ic}{Iˆ9z6q>~0W+)SotgoYܹx)zT)&g2zPt:LF ۪"XQgfJ6S 8JHS"x.&Gʠz+cV9wN;v>!nz/^Xʆ~w{XL\$yC~e ϕ%Jv=OC2,AҼeǿ߬l E0|ޱؚlID-Ѿ}ȂV暠(V͊|#rQQX (`QI$q*]sb%'okmRo)A~-*6qDp#HV!h"IyyXN4"[Z)jEJ;#rȯ"4ɧ̨mFr/k4xFK11RZ}]F`$0f9EzZDVG-&;pk4mh_UzJl\/R b]WApQgqT}`_G{?SFZN7­ɠp+5ꦼ2jϘh0QȌv (OR..1zrޫYc4e+BCv^ׁĪ~sxp/OD`iņZNh]嶺2Z` 03UQ0pQ.Ni¦#׎%f;5Ým `k=FSJ/m;ү,] i+cm,<`FbKF:s] -yalN/JVf"+{!|e\E7E/s ܵ*O*4`gYK2g,(,6 Ao?̹6OmծaZ^B+-Y˯$pLj> -`&Zr#R*n{KSJJɀsEv&aԖd `9%1.ADH"< Ih\K318D^Nl([6mXDeUJ:S|VY.`r*1"T#7cwau˼w"AGVPfOӵ.& PŦ'w* Y2Q$2&Tբ&|-|Ѕ4y~{Uj}[,.uj~r1yq]jc5龬fD-20O++ZF]L?^Mx+n4ov ,54wV!@e%-e%QK7(蛞3yja) 9/jeiyăAݬ  +~%Ryr4W-0kJ|:X0S<7A4`(Y jhn8E܎Ҥk/NS(r80R^l'ÍJUn]"撗Gó֠:ækFPJ&<$7xֻ1\!bT-<94oIZtS8`qRvN65Cy|._\bsVogx6-4[j ~Fg R 5rs*3& Me9)-Klde>YZְM,7~͸]peg2;Uã RO;>xZ>,s#CLZ_GOUTi$N|ZlDk~9GϬaXC>kIė͇(? ǵpSr/U}:ĈJG/ u #>.Gr´ƮdUgl)׊vdp!/V̐9@4jˡqWYy2"zM?EOwU?]vkYG br4KbuWr[17M'7f@4\+s*׏=,SqZhh`Àx)͆Q [w/{6lvzYe*f¢ںn?}GQ}j2~ ʅē\8f =nw:U ϻNIsb٭=W PZfLݩ&< zR%.{:b}L_x l.AD_ίdl5 w9Y2*;Oμ4NTD5Ua@؈n.j˒.yc龩m prLuS(K2*f _f>֖[{ǖ5-TƗKدMZilpGb=I8nr[o*^7,[>ͺ ^(G U 9`Y2ݤ%ENJ_b;P1Ǽܢٌ*m!Pgo/ߚAgF?W.i yrSK#)(L栠VG%(IDATQXT7TX6YfU=>wƫt>@m6ùu5s;l W?(GwK ]L\!o /1:ETudj\HqGJul$XA?>MlW`!gr#١vbˡ5mu*@y E-W*f龫 @={EFf Ձ[QꮖbY.81>}:^Fб.`fR]mE͍+ɤ^;VՊLze9[I/ ;D LbV %e2)AH޵i?ޅAg>?@Jߕ1Tꎱxfֹl!|,:1@᪱tvRx:^bA\T^Я%5NBN禵$=Z;GѲu{ܑh+@m,hcy]ND 6s#cG 'G!w5 -B೹KcTϛO!B?&AY\VYSIO[0/sUW:s\jv[ f(mA`%z3A0&U,KPVMΌUmfPXlw mDkrN:h2f(Feևu-Ma;2o|#SF7)ǁR=A ?li)= \^eAu$4#c^Ү b8_ӷtKSDgkro~K(E۠'AUfB lMd1ț .weUAef" ՘!E*â>sR-X{)1 y>hUR*kJ-U%|ړ }6/LD2Plcs!3VPC$(_?"`C˝ Xޛxq<𳼝]f U[;a*gVG QjyFg.p!Y[Xs=A%ڏ gq/,dTxzTsd!D(K*/ͽ%05B=:X#؀ݕxX>|fr:_Q VƃJBK>k!#-r-7Y E|piKC 8C蠢L, kKu[擜˯.,n?- ")+.- d|!ɝ e̯%Ie8G*#eSalYE$1T]ɡw!/Bx{qء$Ա { /nl8V*oH*ߕ:whV9}t))3{Vh\Vy]}wרKUXBr t+*C+GʀΥQUFA#wZ_PV/i*04^4X8@7b4* rL~Ib\a|n1 G_1 eA!=֝pQ D$Zժx ^n,% bND%Q!ʍúo4,cї63\\-mqP0SuBE̝~8[S' 1O|3ڳ1($kğ,#K (ͺ)G6ګ~Ȋ9u8)22_373c;| &h Լ^[N ؔQ 3nQnGhUBgT|mџghqݬe& w@l$V3"Z9dc{Mם Z_|$P&ڶ G *Q1hcls ͦ;#|>玖'2"ڶK-i)0"?NT *Iq0rjE 9bWM<5J8t'izc4dD (Z[g`Х*]ǰk9bJ-=9$1}N[&Dѝw[vMPggDv OBYR,M*>ȣ,hk^ʖpv<{ {bo2s!a.6WijW,]IT &(EIަBW } \ kufͻgMlpYWAz``qC:JK;JyJrqRH膊]} <{ÔnTV4nvhԱт{XfGn.$Zph4]Hzw` %VvXO%5 ݟ DŽҔ 0nL^"$MP}!E'^mN-˰m^Oǡl*!?$` X]"`)h=AI3^,$4%ym"ʋ>\2q٩_, _}RrFEx N*-0YPL_2EG >R{3h Sn4ad숂Kt5rbPPeHU/ Au;RYXoLZL jp6}'*O8OќW#l+eF(IP!լ (m Y Rdz)1,hbGiMXcF|Z2]FZLw=kS@F`T C\˄Z::u()Ʈ ٵ1:`)J Rr\y|uX@6nC y(M]A9-ޣ!Kb748i2"h6;6BxɠF\#E +!(',JMQxIU-I |h2G>C5l<O6iD-2t ~fm;!EٔP0!}B{H*FǶLǥjŠUdkA˶ "{DG=-?9ow+9*o^{I䨋dtGHؕƑG%n9,"E9Z6冃67fhtSl$r=%0)] ~))+ `[x[ W5 ϼ'ti٫ĞlLP&g~ǎGrm:W>b`R2 2KhﺔOe̼sj8zn":#_(sSuSriU&f{G_tJS) J{)}`v17*Quz枇_x&(T<>SHji9|/w(u3n>O،ތmb}٤~B`6vtq҂ĸ})sgRYcUEP2$%E[D+"(|9wAD*e1cci~؆AO*LW4ڵ氦æS5M AJv/\1O D+̮͘ kȃs%_u`x Xd[z*LנwsPd+T!TC7d Ѷl8j`woeEY/(x GڞO҆y-/x.zm0 omtM0v!Bڽ4"]CA_JZ&c"SnA'2J]h?$Nl',m21 v XL:z Lm*Ex֥jߔB>u0MFO5 MKޗwgIȜU; |!QQB6܋>j|zBrp)n#ƶA&ٕbjE$wO':ftҠAY}ba6;ZM|v*gIAܤcZK2灾8j+iGf^oA_ iU\4uWLFbJ0Wf-aKR jt8C\l!@IY2[Q}-Hd> h;{=[Vw/W퉸OUXJzBk NJQeK-d8\f]#^K` 9;f2[MsA9|vЃ};w /,MN`j \o{t =o}feq>IjYr 5^]#HsT\ \M@:~ʌE>Q tyoe?*GvI* ,jMhM U*1% ~}VhCluᄒa3CE`mIW`TLyvn\@ARUF# $сL2زz{2j#|N}^h5Aa m[&̏V0[ Pԏ;-HzvOVNW Qm2cyq`k_HpLeJ", %1PAg8еRuْ09%qg ԫ=Mi)LWmJ,NuDt GZ7f5Z6FX^y[dwh1O.}iєP#Q땑R/i-KK4~HZ[J]oz9[2/@$4T3FhI1?w4ш ch׉5To-EX2;ԗ+5(#JB-I|;[Ћ["}Ts/DLh {[("~Tv:kIf@IiV*?d6xIENDB`PK9F F*xmantissa/benchmarks/benchmark_fulltext.py """ Fulltext index a message a fixed number of times with PyLucene via the Mantissa fulltext indexing API. """ from zope.interface import implements from epsilon.scripts import benchmark from axiom import store from xmantissa import ixmantissa, fulltext class Message(object): implements(ixmantissa.IFulltextIndexable) def uniqueIdentifier(self): return str(id(self)) def textParts(self): return [ u"Hello, how are you. Please to be " u"seeing this message as an indexer test." * 100] def keywordParts(self): return {u'foo': "A Keyword"} def documentType(self): return u'message' def sortKey(self): return u'' def main(): s = store.Store("lucene.axiom") indexer = fulltext.PyLuceneIndexer(store=s) benchmark.start() writer = indexer.openWriteIndex() for i in xrange(10000): writer.add(Message()) writer.close() benchmark.stop() if __name__ == '__main__': main() PK9F4P&&'xmantissa/benchmarks/benchmark_stats.pyimport datetime from axiom import store from xmantissa import stats, webadmin from epsilon import extime from epsilon.scripts import benchmark def main(): s = store.Store("stats.axiom") sampler = stats.StatSampler(store=s) t = extime.Time() svc = stats.StatsService(store=s, currentMinuteBucket=100) for x in range(100): scope = stats.Statoscope("") scope._stuffs = {'bandwidth_http_down': 0, u'_axiom_query:SELECT main.item_xquotient_grabber_pop3uid_v1.[value] FROM main.item_xquotient_grabber_pop3uid_v1 WHERE (main.item_xquotient_grabber_pop3uid_v1.[grabberID] = ?) ': 0, 'autocommits': 0, u'_axiom_query:SELECT main.item_login_system_v1.oid, main.item_login_system_v1.[failedLogins], main.item_login_system_v1.[loginCount] FROM main.item_login_system_v1 ': 0, u'_axiom_query:SELECT main.item_axiom_powerup_connector_v1.oid, main.item_axiom_powerup_connector_v1.[interface], main.item_axiom_powerup_connector_v1.[item], main.item_axiom_powerup_connector_v1.[powerup], main.item_axiom_powerup_connector_v1.[priority] FROM main.item_axiom_powerup_connector_v1 WHERE ((main.item_axiom_powerup_connector_v1.[interface] = ?) AND (main.item_axiom_powerup_connector_v1.[item] = ?)) ORDER BY main.item_axiom_powerup_connector_v1.[priority] DESC ': 0, u'_axiom_query:SELECT main.item_xmantissa_stats_statsservice_v1.oid, main.item_xmantissa_stats_statsservice_v1.[currentMinuteBucket], main.item_xmantissa_stats_statsservice_v1.[currentQuarterHourBucket], main.item_xmantissa_stats_statsservice_v1.[installedOn] FROM main.item_xmantissa_stats_statsservice_v1 LIMIT 2': 0, 'Imaginary logins': 0, 'bandwidth_pop3-grabber_down': 0, u'pop3uid_check': 0, 'bandwidth_pop3s_down': 0, 'actionExecuted': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2, main.item_login_method_v2 WHERE ((main.item_login_method_v2.[domain] = ?) AND (main.item_login_method_v2.[localpart] = ?) AND (main.item_login_v2.[disabled] = ?) AND (main.item_login_method_v2.[account] = main.item_login_v2.oid)) ': 0, 'bandwidth_pop3_down': 0, u'_axiom_query:SELECT main.item_login_method_v2.oid, main.item_login_method_v2.[account], main.item_login_method_v2.[domain], main.item_login_method_v2.[internal], main.item_login_method_v2.[localpart], main.item_login_method_v2.[protocol], main.item_login_method_v2.[verified] FROM main.item_login_method_v2 ': 0, 'bandwidth_ssh_up': 0, 'cursor_execute_time': 0, 'actionDuration': 0, 'bandwidth_smtps_up': 0, 'messages_grabbed': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2 ': 0, 'bandwidth_smtps_down': 0, u'_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[type] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND ((main.item_xmantissa_stats_statbucket_v1.[index] >= ?) OR (main.item_xmantissa_stats_statbucket_v1.[index] <= ?))) ': 0, u'_axiom_query:SELECT main.item_timed_event_v1.oid, main.item_timed_event_v1.[runnable], main.item_timed_event_v1.[time] FROM main.item_timed_event_v1 ORDER BY main.item_timed_event_v1.[time] ASC LIMIT 1': 0, 'page_renders': 0, 'bandwidth_sip_up': 0, u'_axiom_query:SELECT main.item_persistent_session_v1.oid, main.item_persistent_session_v1.[authenticatedAs], main.item_persistent_session_v1.[lastUsed], main.item_persistent_session_v1.[sessionKey] FROM main.item_persistent_session_v1 WHERE (main.item_persistent_session_v1.[sessionKey] = ?) ': 0, 'cache_hits': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2 WHERE (main.item_login_v2.[disabled] != ?) ': 0, 'athena_messages_sent': 0, u'_axiom_query:SELECT main.item_axiom_batch__reliablelistener_v1.oid, main.item_axiom_batch__reliablelistener_v1.[backwardMark], main.item_axiom_batch__reliablelistener_v1.[forwardMark], main.item_axiom_batch__reliablelistener_v1.[lastRun], main.item_axiom_batch__reliablelistener_v1.[listener], main.item_axiom_batch__reliablelistener_v1.[processor], main.item_axiom_batch__reliablelistener_v1.[style] FROM main.item_axiom_batch__reliablelistener_v1 WHERE ((main.item_axiom_batch__reliablelistener_v1.[processor] = ?) AND (main.item_axiom_batch__reliablelistener_v1.[style] = ?) AND main.item_axiom_batch__reliablelistener_v1.[listener] NOT IN ()) ORDER BY main.item_axiom_batch__reliablelistener_v1.[lastRun] ASC ': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2, main.item_login_method_v2 WHERE ((main.item_login_method_v2.[domain] IS NULL) AND (main.item_login_method_v2.[localpart] = ?) AND (main.item_login_v2.[disabled] = ?) AND (main.item_login_method_v2.[account] = main.item_login_v2.oid)) ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[index] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?)) ': 0, 'bandwidth_telnet_up': 0, u'_axiom_query:SELECT main.item_mantissa_installed_offering_v1.oid, main.item_mantissa_installed_offering_v1.[application], main.item_mantissa_installed_offering_v1.[offeringName] FROM main.item_mantissa_installed_offering_v1 WHERE (main.item_mantissa_installed_offering_v1.[offeringName] = ?) LIMIT 1': 0, 'bandwidth_pop3_up': 0, u'_axiom_query:SELECT main.item_imaginary_wiring_realm_playercredentials_v1.oid, main.item_imaginary_wiring_realm_playercredentials_v1.[actor], main.item_imaginary_wiring_realm_playercredentials_v1.[password], main.item_imaginary_wiring_realm_playercredentials_v1.[username] FROM main.item_imaginary_wiring_realm_playercredentials_v1 WHERE (main.item_imaginary_wiring_realm_playercredentials_v1.[actor] = ?) ': 0, 'bandwidth_sip_down': 0, 'bandwidth_http_up': 0, u'_axiom_query:SELECT main.item_axiom_subscheduler_parent_hook_v1.oid, main.item_axiom_subscheduler_parent_hook_v1 main.item_xmantissa_search_searchresult_v1.[identifier], main.item_xmantissa_search_searchresult_v1.[indexedItem] FROM main.item_xmantissa_search_searchresult_v1 WHERE (main.item_xmantissa_search_searchresult_v1.[indexedItem] = ?) ': 0, u'_axiom_query:SELECT main.item_timed_event_v1.oid, main.item_timed_event_v1.[runnable], main.item_timed_event_v1.[time] FROM main.item_timed_event_v1 WHERE (main.item_timed_event_v1.[time] < ?) ORDER BY main.item_timed_event_v1.[time] ASC LIMIT 1': 0, 'POP3 logins': 0, 'cursor_blocked_time': 0, 'bandwidth_smtp_up': 0, 'messagesReceived': 0, u'_axiom_query:SELECT main.item_imaginary_objects_exit_v1.oid, main.item_imaginary_objects_exit_v1.[fromLocation], main.item_imaginary_objects_exit_v1.[name], main.item_imaginary_objects_exit_v1.[sibling], main.item_imaginary_objects_exit_v1.[toLocation] FROM main.item_imaginary_objects_exit_v1 WHERE (main.item_imaginary_objects_exit_v1.[toLocation] = ?) ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[type] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[time] = ?)) ': 0, 'bandwidth_telnet_down': 0, u'_axiom_query:SELECT main.item_mantissa_installed_offering_v1.[offeringName] FROM main.item_mantissa_installed_offering_v1 ': 0, 'bandwidth_smtp_down': 0, 'mimePartsCreated': 0, u'_axiom_query:SELECT main.item_imaginary_objects_exit_v1.oid, main.item_imaginary_objects_exit_v1.[fromLocation], main.item_imaginary_objects_exit_v1.[name], main.item_imaginary_objects_exit_v1.[sibling], main.item_imaginary_objects_exit_v1.[toLocation] FROM main.item_imaginary_objects_exit_v1 WHERE (main.item_imaginary_objects_exit_v1.[fromLocation] = ?) ': 0, 'bandwidth_pop3-grabber_up': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[index] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[type] = ?)) ': 0, u'_axiom_query:SELECT main.item_developer_site_v1.oid, main.item_developer_site_v1.[administrators], main.item_developer_site_v1.[developers] FROM main.item_developer_site_v1 ': 0, 'Web logins': 0, 'cache_misses': 0} sampler.service = svc sampler.doStatSample(s, scope, t, []) t = t + datetime.timedelta(minutes=1) asf = webadmin.AdminStatsFragment() asf._initializeObserver = lambda : None asf.svc = svc benchmark.start() asf.buildPie() benchmark.stop() if __name__ == '__main__': main() PK9F&C*C*(xmantissa/benchmarks/benchmark_stats2.pyimport datetime from axiom import store from xmantissa import stats, webadmin from epsilon import extime from epsilon.scripts import benchmark def main(): s = store.Store("stats.axiom") sampler = stats.StatSampler(store=s) t = extime.Time() svc = stats.StatsService(store=s, currentMinuteBucket=100) for x in range(100): scope = stats.Statoscope("") scope._stuffs = {'bandwidth_http_down': 0, 'bandwidth_pop3_up': 0, 'bandwidth_sip_up': 0, 'Imaginary logins': 0, 'bandwidth_pop3-grabber_down': 0, 'SMTP logins': 0, 'actionExecuted': 0, 'bandwidth_pop3_down': 0, 'bandwidth_ssh_up': 0, 'cursor_execute_time': 0.15660834312438965, 'actionDuration': 0, 'bandwidth_smtps_up': 0, 'messages_grabbed': 0, 'bandwidth_smtps_down': 0, 'athena_messages_sent': 0, 'page_renders': 0, 'autocommits': 0, 'cache_hits': 7, 'bandwidth_telnet_up': 0, 'bandwidth_smtp_up': 0, 'bandwidth_sip_down': 0, 'bandwidth_http_up': 0, 'commits': 9, 'messagesSent': 0, 'bandwidth_ssh_down': 0, 'athena_messages_received': 0, 'bandwidth_https_up': 0, 'bandwidth_https_down': 0, 'cache_misses': 9, 'bandwidth_pop3s_down': 0, 'POP3 logins': 0, 'cursor_blocked_time': 0.0, 'messagesReceived': 0, 'bandwidth_telnet_down': 0, 'bandwidth_smtp_down': 0, 'mimePartsCreated': 0, 'bandwidth_pop3-grabber_up': 0, 'Web logins': 0, 'bandwidth_pop3s_up': 0} queryscope = stats.Statoscope("") queryscope._stuffs = {u'_axiom_query:SELECT main.item_axiom_batch__reliablelistener_v1.oid, main.item_axiom_batch__reliablelistener_v1.[backwardMark], main.item_axiom_batch__reliablelistener_v1.[forwardMark], main.item_axiom_batch__reliablelistener_v1.[lastRun], main.item_axiom_batch__reliablelistener_v1.[listener], main.item_axiom_batch__reliablelistener_v1.[processor], main.item_axiom_batch__reliablelistener_v1.[style] FROM main.item_axiom_batch__reliablelistener_v1 WHERE ((main.item_axiom_batch__reliablelistener_v1.[processor] = ?) AND (main.item_axiom_batch__reliablelistener_v1.[style] = ?) AND main.item_axiom_batch__reliablelistener_v1.[listener] NOT IN ()) ORDER BY main.item_axiom_batch__reliablelistener_v1.[lastRun] ASC ': 0, u'_axiom_query:SELECT main.item_xquotient_grabber_pop3uid_v1.[value] FROM main.item_xquotient_grabber_pop3uid_v1 WHERE (main.item_xquotient_grabber_pop3uid_v1.[grabberID] = ?) ': 0, u'_axiom_query:SELECT main.item_axiom_powerup_connector_v1.oid, main.item_axiom_powerup_connector_v1.[interface], main.item_axiom_powerup_connector_v1.[item], main.item_axiom_powerup_connector_v1.[powerup], main.item_axiom_powerup_connector_v1.[priority] FROM main.item_axiom_powerup_connector_v1 WHERE ((main.item_axiom_powerup_connector_v1.[interface] = ?) AND (main.item_axiom_powerup_connector_v1.[item] = ?)) ORDER BY main.item_axiom_powerup_connector_v1.[priority] DESC ': 0, u'_axiom_query:SELECT main.item_xmantissa_stats_statsservice_v1.oid, main.item_xmantissa_stats_statsservice_v1.[currentMinuteBucket], main.item_xmantissa_stats_statsservice_v1.[currentQuarterHourBucket], main.item_xmantissa_stats_statsservice_v1.[installedOn] FROM main.item_xmantissa_stats_statsservice_v1 LIMIT 2': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2, main.item_login_method_v2 WHERE ((main.item_login_method_v2.[domain] = ?) AND (main.item_login_method_v2.[localpart] = ?) AND (main.item_login_v2.[disabled] = ?) AND (main.item_login_method_v2.[account] = main.item_login_v2.oid)) ': 0, u'_axiom_query:SELECT main.item_login_method_v2.oid, main.item_login_method_v2.[account], main.item_login_method_v2.[domain], main.item_login_method_v2.[internal], main.item_login_method_v2.[localpart], main.item_login_method_v2.[protocol], main.item_login_method_v2.[verified] FROM main.item_login_method_v2 ': 0, u'_axiom_query:SELECT main.item_timed_event_v1.oid, main.item_timed_event_v1.[runnable], main.item_timed_event_v1.[time] FROM main.item_timed_event_v1 WHERE (main.item_timed_event_v1.[time] < ?) ORDER BY main.item_timed_event_v1.[time] ASC LIMIT 1': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2 ': 0, u'_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[type] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND ((main.item_xmantissa_stats_statbucket_v1.[index] >= ?) OR (main.item_xmantissa_stats_statbucket_v1.[index] <= ?))) ': 0, u'_axiom_query:SELECT main.item_timed_event_v1.oid, main.item_timed_event_v1.[runnable], main.item_timed_event_v1.[time] FROM main.item_timed_event_v1 ORDER BY main.item_timed_event_v1.[time] ASC LIMIT 1': 0, u'_axiom_query:SELECT main.item_persistent_session_v1.oid, main.item_persistent_session_v1.[authenticatedAs], main.item_persistent_session_v1.[lastUsed], main.item_persistent_session_v1.[sessionKey] FROM main.item_persistent_session_v1 WHERE (main.item_persistent_session_v1.[sessionKey] = ?) ': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2 WHERE (main.item_login_v2.[disabled] != ?) ': 0, u'_axiom_query:SELECT main.item_login_v2.oid, main.item_login_v2.[avatars], main.item_login_v2.[disabled], main.item_login_v2.[password] FROM main.item_login_v2, main.item_login_method_v2 WHERE ((main.item_login_method_v2.[domain] IS NULL) AND (main.item_login_method_v2.[localpart] = ?) AND (main.item_login_v2.[disabled] = ?) AND (main.item_login_method_v2.[account] = main.item_login_v2.oid)) ': 0, u'_axiom_query:SELECT main.item_mantissa_installed_offering_v1.oid, main.item_mantissa_installed_offering_v1.[application], main.item_mantissa_installed_offering_v1.[offeringName] FROM main.item_mantissa_installed_offering_v1 WHERE (main.item_mantissa_installed_offering_v1.[offeringName] = ?) LIMIT 1': 0, u'_axiom_query:SELECT main.item_imaginary_wiring_realm_playercredentials_v1.oid, main.item_imaginary_wiring_realm_playercredentials_v1.[actor], main.item_imaginary_wiring_realm_playercredentials_v1.[password], main.item_imaginary_wiring_realm_playercredentials_v1.[username] FROM main.item_imaginary_wiring_realm_playercredentials_v1 WHERE (main.item_imaginary_wiring_realm_playercredentials_v1.[actor] = ?) ': 0, u'_axiom_query:SELECT main.item_login_system_v1.oid, main.item_login_system_v1.[failedLogins], main.item_login_system_v1.[loginCount] FROM main.item_login_system_v1 ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[type] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[time] = ?)) ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[type] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[time] = ?)) ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[index] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?)) ': 0, u'_axiom_query:SELECT main.item_imaginary_objects_exit_v1.oid, main.item_imaginary_objects_exit_v1.[fromLocation], main.item_imaginary_objects_exit_v1.[name], main.item_imaginary_objects_exit_v1.[sibling], main.item_imaginary_objects_exit_v1.[toLocation] FROM main.item_imaginary_objects_exit_v1 WHERE (main.item_imaginary_objects_exit_v1.[toLocation] = ?) ': 0, u'_axiom_query:SELECT main.item_mantissa_installed_offering_v1.[offeringName] FROM main.item_mantissa_installed_offering_v1 ': 0, u'_axiom_query:SELECT main.item_imaginary_objects_exit_v1.oid, main.item_imaginary_objects_exit_v1.[fromLocation], main.item_imaginary_objects_exit_v1.[name], main.item_imaginary_objects_exit_v1.[sibling], main.item_imaginary_objects_exit_v1.[toLocation] FROM main.item_imaginary_objects_exit_v1 WHERE (main.item_imaginary_objects_exit_v1.[fromLocation] = ?) ': 0, u'_axiom_query:SELECT main.item_axiom_subscheduler_parent_hook_v1.oid, main.item_axiom_subscheduler_parent_hook_v1 main.item_xmantissa_search_searchresult_v1.[identifier], main.item_xmantissa_search_searchresult_v1.[indexedItem] FROM main.item_xmantissa_search_searchresult_v1 WHERE (main.item_xmantissa_search_searchresult_v1.[indexedItem] = ?) ': 0, '_axiom_query:SELECT main.item_xmantissa_stats_statbucket_v1.oid, main.item_xmantissa_stats_statbucket_v1.[index], main.item_xmantissa_stats_statbucket_v1.[interval], main.item_xmantissa_stats_statbucket_v1.[time], main.item_xmantissa_stats_statbucket_v1.[type], main.item_xmantissa_stats_statbucket_v1.[value] FROM main.item_xmantissa_stats_statbucket_v1 WHERE ((main.item_xmantissa_stats_statbucket_v1.[index] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[interval] = ?) AND (main.item_xmantissa_stats_statbucket_v1.[type] = ?)) ': 0, u'_axiom_query:SELECT main.item_developer_site_v1.oid, main.item_developer_site_v1.[administrators], main.item_developer_site_v1.[developers] FROM main.item_developer_site_v1 ': 0} sampler.service = svc sampler.doStatSample(s, scope, t, []) sampler.doStatSample(s, queryscope, t, [], bucketType=stats.QueryStatBucket) t = t + datetime.timedelta(minutes=1) asf = webadmin.AdminStatsFragment() asf._initializeObserver = lambda : None asf.svc = svc benchmark.start() asf.buildPie() benchmark.stop() if __name__ == '__main__': main() PK9F 5++xmantissa/js/Fadomatic.js // Fade interval in milliseconds // Make this larger if you experience performance issues Fadomatic.INTERVAL_MILLIS = 50; // Creates a fader // element - The element to fade // speed - The speed to fade at, from 0.0 to 100.0 // initialOpacity (optional, default 100) - element's starting opacity, 0 to 100 // minOpacity (optional, default 0) - element's minimum opacity, 0 to 100 // maxOpacity (optional, default 0) - element's minimum opacity, 0 to 100 function Fadomatic (element, rate, initialOpacity, minOpacity, maxOpacity) { this._element = element; this._intervalId = null; this._rate = rate; this._isFadeOut = true; // Set initial opacity and bounds // NB use 99 instead of 100 to avoid flicker at start of fade this._minOpacity = 0; this._maxOpacity = 99; this._opacity = 99; if (typeof minOpacity != 'undefined') { if (minOpacity < 0) { this._minOpacity = 0; } else if (minOpacity > 99) { this._minOpacity = 99; } else { this._minOpacity = minOpacity; } } if (typeof maxOpacity != 'undefined') { if (maxOpacity < 0) { this._maxOpacity = 0; } else if (maxOpacity > 99) { this._maxOpacity = 99; } else { this._maxOpacity = maxOpacity; } if (this._maxOpacity < this._minOpacity) { this._maxOpacity = this._minOpacity; } } if (typeof initialOpacity != 'undefined') { if (initialOpacity > this._maxOpacity) { this._opacity = this._maxOpacity; } else if (initialOpacity < this._minOpacity) { this._opacity = this._minOpacity; } else { this._opacity = initialOpacity; } } // See if we're using W3C opacity, MSIE filter, or just // toggling visiblity if(typeof element.style.opacity != 'undefined') { this._updateOpacity = this._updateOpacityW3c; } else if(typeof element.style.filter != 'undefined') { // If there's not an alpha filter on the element already, // add one if (element.style.filter.indexOf("alpha") == -1) { // Attempt to preserve existing filters var existingFilters=""; if (element.style.filter) { existingFilters = element.style.filter+" "; } element.style.filter = existingFilters+"alpha(opacity="+this._opacity+")"; } this._updateOpacity = this._updateOpacityMSIE; } else { this._updateOpacity = this._updateVisibility; } this._updateOpacity(); } // Initiates a fade out Fadomatic.prototype.fadeOut = function () { this._isFadeOut = true; this._beginFade(); } // Initiates a fade in Fadomatic.prototype.fadeIn = function () { this._isFadeOut = false; this._beginFade(); } // Makes the element completely opaque, stops any fade in progress Fadomatic.prototype.show = function () { this.haltFade(); this._opacity = this._maxOpacity; this._updateOpacity(); } // Makes the element completely transparent, stops any fade in progress Fadomatic.prototype.hide = function () { this.haltFade(); this._opacity = 0; this._updateOpacity(); } // Halts any fade in progress Fadomatic.prototype.haltFade = function () { clearInterval(this._intervalId); } // Resumes a fade where it was halted Fadomatic.prototype.resumeFade = function () { this._beginFade(); } // Pseudo-private members Fadomatic.prototype._beginFade = function () { this.haltFade(); var objref = this; this._intervalId = setInterval(function() { objref._tickFade(); },Fadomatic.INTERVAL_MILLIS); } Fadomatic.prototype._tickFade = function () { if (this._isFadeOut) { this._opacity -= this._rate; if (this._opacity < this._minOpacity) { this._opacity = this._minOpacity; this.haltFade(); } } else { this._opacity += this._rate; if (this._opacity > this._maxOpacity ) { this._opacity = this._maxOpacity; this.haltFade(); } } this._updateOpacity(); } Fadomatic.prototype._updateVisibility = function () { if (this._opacity > 0) { this._element.style.visibility = 'visible'; } else { this._element.style.visibility = 'hidden'; } } Fadomatic.prototype._updateOpacityW3c = function () { this._element.style.opacity = this._opacity/100; this._updateVisibility(); } Fadomatic.prototype._updateOpacityMSIE = function () { this._element.filters.alpha.opacity = this._opacity; this._updateVisibility(); } Fadomatic.prototype._updateOpacity = null; PK9Fn%xmantissa/js/Mantissa/Admin.js // import Divmod.Runtime // import Mantissa.ScrollTable // import Mantissa.LiveForm Mantissa.Admin = {}; /** * Trivial L{Mantissa.ScrollTable.Action} subclass which sets a handler that * calls L{Mantissa.Admin.LocalUserBrowser.updateUserDetail} on the instance * that the action was activated in. */ Mantissa.Admin.EndowDepriveAction = Mantissa.ScrollTable.Action.subclass( 'Mantissa.Admin.EndowDepriveAction'); Mantissa.Admin.EndowDepriveAction.methods( function __init__(self, name, displayName) { Mantissa.Admin.EndowDepriveAction.upcall( self, "__init__", name, displayName, function(localUserBrowser, row, result) { return localUserBrowser.updateUserDetail(result); }); }); /** * Action for removing ports. In addition to deleting them from the database * the server, delete them from the local view. * * XXX - In poor form, I have not written automated tests for this code. The * only excuse I can muster is that the thought of adding to the twenty minutes * it takes to run nit for this meagre amount of code turns my stomach. May my * ancestors forgive me for the shame I do to their name. -exarkun */ Mantissa.Admin.DeleteAction = Mantissa.ScrollTable.Action.subclass('Mantissa.Admin.DeleteAction'); Mantissa.Admin.DeleteAction.methods( function __init__(self, name, displayName) { Mantissa.Admin.DeleteAction.upcall( self, "__init__", name, displayName, function deleteSuccessful(scrollingWidget, row, result) { var index = scrollingWidget.model.findIndex(row.__id__); scrollingWidget.removeRow(index); }); }); /** * Scrolling widget with a delete action. * * XXX See XXX for DeleteAction. */ Mantissa.Admin.PortBrowser = Mantissa.ScrollTable.FlexHeightScrollingWidget.subclass('Mantissa.Admin.PortBrowser'); Mantissa.Admin.PortBrowser.methods( function __init__(self, node, metadata) { self.actions = [Mantissa.Admin.DeleteAction("delete", "Delete")]; Mantissa.Admin.PortBrowser.upcall(self, "__init__", node, metadata, 10); }); /** * Scrolltable with support for retrieving additional detailed information * about particular users from the server and displaying it on the page * someplace. */ Mantissa.Admin.LocalUserBrowser = Mantissa.ScrollTable.FlexHeightScrollingWidget.subclass('Mantissa.Admin.LocalUserBrowser'); Mantissa.Admin.LocalUserBrowser.methods( function __init__(self, node, metadata) { self.actions = [Mantissa.Admin.EndowDepriveAction("installOn", "Endow"), Mantissa.Admin.EndowDepriveAction("uninstallFrom", " Deprive"), Mantissa.Admin.EndowDepriveAction("suspend", " Suspend"), Mantissa.Admin.EndowDepriveAction("unsuspend", " Unsuspend") ]; Mantissa.Admin.LocalUserBrowser.upcall(self, "__init__", node, metadata, 10); }, function _getUserDetailElement(self) { if (self._userDetailElement == undefined) { var n = document.createElement('div'); n.setAttribute('class', 'user-detail'); self.node.appendChild(n); self._userDetailElement = n; } return self._userDetailElement; }, /** * Called by L{Mantissa.Admin.EndowDepriveAction}. Retrieves information * about the clicked user from the server and dumps it into a node * (created for this purpose, on demand). Removes the existing content of * that node if there is any. */ function updateUserDetail(self, result) { var D = self.addChildWidgetFromWidgetInfo(result); return D.addCallback( function(widget) { var n = self._getUserDetailElement(); while(0 < n.childNodes.length) { n.removeChild(n.firstChild); } n.appendChild(widget.node); }); }); PK9F?bb'xmantissa/js/Mantissa/Authentication.js// import Mantissa Mantissa.Authentication = Nevow.Athena.Widget.subclass("Mantissa.Authentication"); Mantissa.Authentication.methods( function submitNewPassword(self, form) { if (form.newPassword.value != form.confirmPassword.value) { alert('Passwords do not match. Try again.'); } else { var curPass = null; if (form.currentPassword) { curPass = form.currentPassword.value; form.currentPassword.value = ''; } var newPass = form.newPassword.value; form.newPassword.value = form.confirmPassword.value = ''; var D = self.callRemote('changePassword', curPass, newPass); D.addBoth(alert); } }, function cancel(self, sessionId) { self.callRemote('cancel', sessionId).addBoth(alert); }); PK9FȎ00%xmantissa/js/Mantissa/AutoComplete.js// Copyright (c) 2006 Divmod. // See LICENSE for details. /** * This class contains the autocomplete logic * * L{appendCompletion} and L{complete} expect commas to be a meaningful * delimiter in the text that is being autocompleted, and L{complete} only * tries to complete the text after the last comma in the string it gets. If * there is a use case for another kind of autocomplete, maybe the delimiter * specific stuff should go in a subclass of something */ Mantissa.AutoComplete.Model = Divmod.Class.subclass('Mantissa.AutoComplete.Model'); Mantissa.AutoComplete.Model.methods( /** * @param possibilities: sequence of possible completions. The default * implementations of L{isCompletion} and L{complete} expect the * completions to be of type C{String} */ function __init__(self, possibilities) { self.possibilities = possibilities; }, /** * Figure out whether C{haystack} is a completion of C{needle} * * @type needle: C{String} * @type haystack: C{String} * @rtype: C{Boolean} */ function isCompletion(self, needle, haystack) { return (0 < needle.length && (haystack.toLowerCase().slice(0, needle.length) == needle.toLowerCase())); }, /** * @param s: comma-delimited string * @type s: C{String} * @return: last datum in C{s}, stripped of any leading or trailing * whitespace * @rtype: C{String} */ function _getLastItemStripped(self, s) { var values = s.split(/,/), last = values[values.length - 1]; return last.replace(/^\s+/, '').replace(/\s+$/, ''); }, /** * Find all the possible completions of C{s} * * @type s: C{String} * * @return: all of the strings in C{self.possibilities} which * L{isCompletion} thinks are completions of C{text} * @rtype: C{Array} of C{String} * * XXX We could be a lot smarter here if performance becomes a problem */ function complete(self, s) { var text = self._getLastItemStripped(s), completions = []; for(var i = 0; i < self.possibilities.length; i++) { if(self.isCompletion(text, self.possibilities[i])) { completions.push(self.possibilities[i]); } } return completions; }, /** * Append the completion C{completionText} to the text C{text}, stripping * off the prefix which is already present at the end of C{text}. * e.g. appendCompletion('x, y, z', 'zyz') => 'x, y, zyz, ' * * @type text: C{String} * @type completionText: C{String} * @rtype: C{String} */ function appendCompletion(self, text, completionText) { var last = self._getLastItemStripped(text); text = text.slice(0, text.length - last.length); return text + completionText + ", "; }); /** * I contain the portions of autocomplete functionality which need to * communicate with the DOM. * * FIXME: Maybe the model should know what the selection is */ Mantissa.AutoComplete.View = Divmod.Class.subclass('Mantissa.AutoComplete.View'); Mantissa.AutoComplete.View.methods( /** * @param textbox: The node that will be monitored for keypresses, and * below which the list of completions will be displayed * @type textbox: A textbox, or anything that might be a source of * keypress events * * @param completionsNode: the node which is to contain any user-visible * completions of the value of C{textbox} at any given time * @type completionsNode: block element */ function __init__(self, textbox, completionsNode) { self.textbox = textbox; self.completionsNode = completionsNode; }, /** * Attach function C{f} as a listener of keypress events originating from * our textbox node */ function hookupKeypressListener(self, f) { self.textbox.onkeypress = f; }, /** * Figure out if we are currently displaying any completions to the user * * @rtype: C{Boolean} */ function displayingCompletions(self) { return self.completionsNode.style.display != "none"; }, /** * Figure out what the selected completion is. See also * L{_selectCompletion}, L{moveSelectionUp} and L{moveSelectionDown} * * @rtype: C{null} or a C{Object} with C{offset} and C{value} members, * where C{offset} is the position of the selected completion in the * completions list, and C{value} is the value of the completion, i.e. the * actual text */ function selectedCompletion(self) { var children = self.completionsNode.childNodes; for(var i = 0; i < children.length; i++) { if(children[i].className == "selected-completion") { return {offset: i, value: children[i].firstChild.nodeValue}; } } return null; }, /** * Select the completion at offset C{offset} in the completions list, and * deselect whatever completion was previously selected * * @type offset: C{Number} */ function _selectCompletion(self, offset) { var children = self.completionsNode.childNodes, node = children[offset]; selected = self.selectedCompletion(); if(selected != null) { children[selected.offset].className = "completion"; } node.className = "selected-completion"; }, /** * Figure out the number of completions we are currently displaying * * @type: C{Number} */ function completionCount(self) { return self.completionsNode.childNodes.length; }, /** * Select the completion below the currently selected completion, or the * first completion if the currently selected completion is the last */ function moveSelectionDown(self) { var seloffset = self.selectedCompletion().offset; seloffset++; if(self.completionCount() == seloffset) { seloffset = 0; } self._selectCompletion(seloffset); }, /** * Select the completion above the currently selected completion, or the * last if the currently selected completion is the first */ function moveSelectionUp(self) { var seloffset = self.selectedCompletion().offset; seloffset--; if(seloffset == -1) { seloffset = self.completionCount()-1; } self._selectCompletion(seloffset); }, /** * Get the value of the textbox * * @rtype: C{String} */ function getValue(self) { return self.textbox.value; }, /** * Set the value of the textbox * * @param value: the new value * @type value: C{String} */ function setValue(self, value) { self.textbox.value = value; }, /** * Remove all completions from the completions list */ function emptyCompletions(self) { while(self.completionsNode.firstChild) { self.completionsNode.removeChild( self.completionsNode.firstChild); } }, /** * Remove all completions from the completions list, and hide the * completions list */ function emptyAndHideCompletions(self) { self.emptyCompletions(); self.completionsNode.style.display = "none"; }, /** * Display C{completions} as the completions of the current value of our * textbox, removing any completions that were previously visible * * @type completions: C{Array} of C{String} */ function showCompletions(self, completions) { self.emptyCompletions(); for(var i = 0; i < completions.length; i++) { self.completionsNode.appendChild( self.makeCompletionNode(completions[i])); } self._selectCompletion(0); self.completionsNode.style.top = Divmod.Runtime.theRuntime.findPosY(self.textbox) + Divmod.Runtime.theRuntime.getElementSize(self.textbox).h + "px"; self.completionsNode.style.left = Divmod.Runtime.theRuntime.findPosX(self.textbox) + "px"; self.completionsNode.style.display = ""; }, /** * Make a node suitable for displaying C{completion} * * @type completion: C{String} * @rtype: node */ function makeCompletionNode(self, completion) { var node = document.createElement("div"); node.appendChild(document.createTextNode(completion)); return node; }); /** * I coordinate L{Mantissa.AutoComplete.Model} and * L{Mantissa.AutoComplete.View}, and respond to events */ Mantissa.AutoComplete.Controller = Divmod.Class.subclass('Mantissa.AutoComplete.Controller'); Mantissa.AutoComplete.Controller.methods( /** * @type model: L{Mantissa.AutoComplete.Model} * @type view: L{Mantissa.AutoComplete.View} * @param scheduler: function which takes a function and a number of * milliseconds, and executes the function in that many milliseconds. If * undefined, defaults to C{setTimeout} */ function __init__(self, model, view, scheduler/*=setTimeout*/) { view.hookupKeypressListener( function(event) { return self.onkeypress(event); }); self.model = model; self.view = view; if(scheduler == undefined) { scheduler = function(f, ms) { setTimeout(f, ms); } } self.scheduler = scheduler; }, /** * Respond to a keypress event. * * @type event: Something with a C{keyCode} member */ function onkeypress(self, event) { /* non alnum key pressed */ if(0 < event.keyCode) { /* we only care if the completions are visible, because we want to * help the user navigate the list with the keyboard */ if(!self.view.displayingCompletions()) { return true; } var TAB = 9, ENTER = 13, UP = 38, DOWN = 40; /* tab & enter mean "pick this one" */ if(event.keyCode == ENTER || event.keyCode == TAB) { self.selectedCompletionChosen(); return false; } else if(event.keyCode == DOWN) { self.view.moveSelectionDown(); } else if(event.keyCode == UP) { self.view.moveSelectionUp(); /* it was delete or something */ } else { self.scheduleCompletion(); } /* otherwise an alnum key, and we should try to complete what has been typed */ } else { self.scheduleCompletion(); } return true; }, /** * Schedule a re-evaluation of the currently completions. Called when the * keypress event handled by L{onkeypress} is suspected to have changed * the value of the view's input node * * XXX: Since we're usually responding to DOM events, the value of the * textbox hasn't actually been updated until all of the handlers for the * last keypress event have been called, so we hack it with the scheduler */ function scheduleCompletion(self) { self.scheduler( function() { self.complete(self.view.getValue()); }, 0); }, /** * Ask the model what the completions for string C{s} are, and ask the * view to display them, or ask the view to display nothing if there are * no completions * * @type s: C{String} */ function complete(self, s) { var completions = self.model.complete(s); if(0 < completions.length) { self.view.showCompletions(completions); } else { self.view.emptyAndHideCompletions(); } }, /** * Called when the last keypress event handled by L{onkeypress} indicates * that the currently selected completion is the one we want. */ function selectedCompletionChosen(self) { var value = self.model.appendCompletion( self.view.getValue(), self.view.selectedCompletion().value); self.view.setValue(value); self.view.emptyAndHideCompletions(); }); PK9F!ml#xmantissa/js/Mantissa/DOMReplace.js// -*- test-case-name: xmantissa.test.test_javascript -*- // Copyright (c) 2007-2010 Divmod. // See LICENSE for details. /** * Functionality for performing string substitution on the content of DOM * nodes. */ /** * Split C{str} where it matches C{pattern}, applying C{replacer} to the * matching portions, and replacing them with its return value. * * @param str: a string. * @type str: C{String} * * @param pattern: a regular expression. * @type pattern: C{RegExp} * * @param replacer: a function which knows how to turn strings into DOM nodes. * @type replacer: C{Function} * * @return: a list of strings and DOM nodes (returned by C{replacer}). * @rtype: C{Array} or C{null}, if C{pattern} does not match anywhere. */ Mantissa.DOMReplace._intermingle = function _intermingle(str, pattern, replacer) { var piece; var pieces = [str]; var match; while(true) { piece = pieces.pop(); match = pattern.exec(piece); if(!match) { /* If nothing matched so far, then we can just stop now. The * check at the end of the function will convert the empty result * array to a null result which saves us a little bit of DOM * munging later. But if anything else has matched, then we have * to push back the string we're checking: it represents part of * the original input, and if it's not in the pieces array, it * won't end up in the final document. That would be bad. */ if (pieces.length) { pieces.push(piece); } break; } if(0 < match.index) { pieces.push(piece.slice(0, match.index)); } pieces.push(replacer(match[0])); if(match.index + match[0].length < piece.length) { pieces.push(piece.slice(match.index + match[0].length, piece.length)); } else { break; } } if(pieces.length == 0) { return null; } return pieces; } /** * Enact the series of node-content transformations described by the objects * in C{pendingChanges}. * * @param pendingChanges: A series of objects describing nodes which should be * replaced with other nodes. * @type pendingChanges: C{Array} of C{Object}, each having a I{reference} key (the * old node), and a I{replacements} key (C{Array} of nodes or of C{String}s to replace * I{reference} with). * * @rtype: C{undefined} */ Mantissa.DOMReplace._rewrite = function _rewrite(pendingChanges) { for(var i = 0; i < pendingChanges.length; i++) { for(var j = 0; j < pendingChanges[i].replacements.length; j++) { var replacement = pendingChanges[i].replacements[j]; if(replacement.nodeType == undefined) { replacement = document.createTextNode(replacement); } pendingChanges[i].reference.parentNode.insertBefore( replacement, pendingChanges[i].reference); } pendingChanges[i].reference.parentNode.removeChild(pendingChanges[i].reference); } } /** * Replace all instances of C{pattern} that occur within the values of text * nodes beneath C{node} with the result of calling C{replacer} on the match * text. * * @param node: the node at which to begin the search. * @type node: nodey thing * * @param pattern: the text we want to look for. * @type pattern: C{RegExp} * * @param replacer: a function which knows how to turn strings into DOM nodes. * @type replacer: C{Function} * * @param descender: (optional) a function which knows what kinds of nodes * it's worth descending into. if specified, this function takes a node and * returns a L{Divmod.Runtime.Platform.DOM_*} constant indicating the desired * traverser action. the default will descend into all nodes. * @type descender: function * @return: nothing. C{node} is mutated. * @rtype: C{undefined} */ Mantissa.DOMReplace.replace = function replace(node, pattern, replacer, /*optional*/descender) { var pendingChanges = []; if(descender === undefined) { descender = function(node) { return Divmod.Runtime.Platform.DOM_DESCEND; } } Divmod.Runtime.theRuntime.traverse( node, function(node) { if(node.nodeType == node.TEXT_NODE) { var replacements = Mantissa.DOMReplace._intermingle( node.nodeValue, pattern, replacer); if(replacements !== null) { pendingChanges.push({reference: node, replacements: replacements}); } return Divmod.Runtime.Platform.DOM_CONTINUE; } return descender(node); }); Mantissa.DOMReplace._rewrite(pendingChanges); } /** * Turn C{url} in into a DOM node which will render as a link to C{url}. * * @param url: a url. * @type url: C{String}. * * @return: an "A" node. * @rtype: node. */ Mantissa.DOMReplace._urlToLink = function _urlToLink(url) { var linkTarget; if(url.slice(0, 3).toLowerCase() == "www") { linkTarget = "http://" + url; } else { linkTarget = url; } var link = document.createElement('a'); link.href = linkTarget; link.target = '_blank'; link.appendChild(document.createTextNode(url)); return link; } /** * Turn the bits of text nodes inside C{node} which look like URLs into "A" * nodes linking to those urls. * * @param node: the node at which to begin the search. * @type node: nodey thing * * @return: nothing. C{node} is mutated. * @rtype: C{undefined} */ Mantissa.DOMReplace.urlsToLinks = function urlsToLinks(node) { Mantissa.DOMReplace.replace( node, /(?:\w+:\/\/|www\.)[^\s\<\>\'\(\)\"]+[^\s\<\>\(\)\'\"\?\.]/, Mantissa.DOMReplace._urlToLink, function(node) { /* who cares about urls already in links */ if(node.tagName.toLowerCase() == "a") { return Divmod.Runtime.Platform.DOM_CONTINUE; } return Divmod.Runtime.Platform.DOM_DESCEND; }); } PK9FA[[[!xmantissa/js/Mantissa/LiveForm.js// import Divmod // import Divmod.Runtime // import Nevow // import Nevow.Athena // import Mantissa Mantissa.LiveForm.RepeatedLiveFormWrapper = Nevow.Athena.Widget.subclass( 'Mantissa.LiveForm.RepeatedLiveFormWrapper'); /** * Widget which wraps a L{Mantissa.LiveForm.FormWidget}. * * @type identifier: string * @ivar identifier: A value which will be inserted into the inputs mapping for * the key C{__repeated-liveform-id__}. * * @type formName: string * @ivar formName: The name of the form which is being wrapped. * SubFormWidget.gatherInputs uses this as a key in the "mapping" it * submits to the server, with the results of this widget's gatherInputs as * the associated value. It must be the same as the wrapped form's * formName in order for the values to arrive at the server correctly. It * would probably be better if the server were not required to supply * exactly the correct value here; instead this class could get it from its * wrapped form somehow. */ Mantissa.LiveForm.RepeatedLiveFormWrapper.methods( function __init__(self, node, formName, identifier) { Mantissa.LiveForm.RepeatedLiveFormWrapper.upcall( self, '__init__', node); self.formName = formName; self.identifier = identifier; }, /** * Remove our node from the DOM */ function dom_unrepeat(self) { self.detach(); self.node.parentNode.removeChild(self.node); return false; }, /** * Gather inputs from the wrapped child liveform and insert * C{self.identifier} in the resulting mapping before returning it. */ function gatherInputs(self) { var result = self.childWidgets[0].gatherInputs(); result['__repeated-liveform-id__'] = self.identifier; return result; }, /** * Defer to our child liveform. */ function submitSuccess(self, result) { self.childWidgets[0].submitSuccess(result); }); Mantissa.LiveForm.RepeatableForm = Nevow.Athena.Widget.subclass( 'Mantissa.LiveForm.RepeatableForm'); /** * Widget which knows how to ask its server for a copy of a liveform, and how * to insert the liveform into the document. */ Mantissa.LiveForm.RepeatableForm.methods( function __init__(self, node, formName) { Mantissa.LiveForm.RepeatableForm.upcall(self, '__init__', node); self.formName = formName; }, /** * Override this hook to tell our children * (L{Mantissa.LiveForm.RepeatedLiveFormWrapper}s) that they have been * submitted successfully. */ function submitSuccess(self, result) { for(var i = 0; i < self.childWidgets.length; i++) { self.childWidgets[i].submitSuccess(result); } }, /** * Implement C{gatherInputs} so we can pretend to be a liveform, by * accumulating the result of broadcasting the method call to all of our * child widgets. */ function gatherInputs(self) { var inputs = []; for(var i = 0; i < self.childWidgets.length; i++) { inputs.push(self.childWidgets[i].gatherInputs()); } return inputs; }, /** * Request a copy of the form we are associated with, and insert the resulting * widget's node into the DOM. */ function repeat(self) { var result = self.callRemote('repeatForm'); result.addCallback( function(widgetInfo) { return self.addChildWidgetFromWidgetInfo(widgetInfo); }); result.addCallback( function(widget) { var repeater = self.firstNodeByAttribute( 'class', 'liveform-repeater'); repeater.parentNode.appendChild(widget.node); }); return result; }, /** * DOM event handler which calls L{repeat}. */ function dom_repeat(self) { self.repeat(); return false; }); /** * Error, generally received from the server, indicating that some input was * rejected and the form action was not taken. * * At some future point, the attributes offered by this class (or another class * which may supercede it) should be expanded to indicate more precisely which * input was rejected (eg, by supplying a key or list of keys for input nodes * corresponding to the rejected values). * * @ivar message: A string giving an explaination of why the input was * rejected. */ Mantissa.LiveForm.InputError = Divmod.Error.subclass("Mantissa.LiveForm.InputError"); Mantissa.LiveForm.InputError.methods( function __init__(self, message) { self.message = message; }); Mantissa.LiveForm.BadInputName = Divmod.Error.subclass("Mantissa.LiveForm.BadInputName"); /** * Thrown when there is an input name passed to one of L{FormWidget}'s methods * doesn't correspond to an input in the widget's document */ Mantissa.LiveForm.BadInputName.methods( function __init__(self, name) { self.name = name; }, function toString(self) { return "no element with name " + self.name; }); Mantissa.LiveForm.NodeCountMismatch = Divmod.Error.subclass("Mantissa.LiveForm.NodeCountMismatch"); /** * Thrown where there is a mismatch between the number of values in one of the * input lists passed to L{FormWidget.setInputValues} and the number of actual * nodes in the document for that key */ Mantissa.LiveForm.NodeCountMismatch.methods( function __init__(self, nodeName, givenNodes, actualNodes) { self.nodeName = nodeName; self.givenNodes = givenNodes; self.actualNodes = actualNodes; }, function toString(self) { return "you supplied " + self.givenNodes + " values for input " + self.nodeName + " but there are " + self.actualNodes + " nodes"; }); Mantissa.LiveForm.MessageFader = Divmod.Class.subclass("Divmod.Class.MessageFader"); /** * Fade a node in, then out again. */ Mantissa.LiveForm.MessageFader.methods( function __init__(self, node) { self.node = node; self.timer = null; self.inRate = 1.0; // in opacity / second self.outRate = 0.5; self.messageDelay = 5.0; // number of seconds message is left on-screen }, /* * Cause the referenced Node to become fully opaque. It must currently be * fully transparent. Returns a Deferred which fires when this has been * done. */ function fadeIn(self) { var currentOpacity = 0.0; var TICKS_PER_SECOND = 30.0; var fadedInDeferred = Divmod.Defer.Deferred(); var inStep = function () { currentOpacity += (self.inRate / TICKS_PER_SECOND); if (currentOpacity > 1.0) { self.node.style.opacity = '1.0'; self.timer = null; fadedInDeferred.callback(null); } else { self.node.style.opacity = currentOpacity; self.timer = setTimeout(inStep, 1000 * 1.0 / TICKS_PER_SECOND); } }; /* XXX TODO - "block" is not the right thing to do here. The wrapped * node might be a table cell or something. */ self.node.style.display = 'block'; inStep(); return fadedInDeferred; }, /* * Cause the referenced Node to become fully transparent. It must * currently be fully opaque. Returns a Deferred which fires when this * has been done. */ function fadeOut(self) { var fadedOutDeferred = Divmod.Defer.Deferred(); var currentOpacity = 0.0; var TICKS_PER_SECOND = 30.0; var outStep = function () { currentOpacity -= (self.outRate / TICKS_PER_SECOND); if (currentOpacity < 0.0) { self.node.style.display = 'none'; self.timer = null; fadedOutDeferred.callback(null); } else { self.node.style.opacity = currentOpacity; self.timer = setTimeout(outStep, 1000 * 1.0 / TICKS_PER_SECOND); } }; outStep(); return fadedOutDeferred; }, /* * Go through one fade-in/fade-out cycle. Return a Deferred which fires * when both steps have finished. */ function start(self) { // kick off the timer loop return self.fadeIn().addCallback(function() { return self.fadeOut(); }); }); Mantissa.LiveForm.SubFormWidget = Nevow.Athena.Widget.subclass('Mantissa.LiveForm.SubFormWidget'); /** * Represent a distinct part of a larger form. */ Mantissa.LiveForm.SubFormWidget.methods( function __init__(self, node, formName) { Mantissa.LiveForm.SubFormWidget.upcall(self, '__init__', node); self.formName = formName; }, /** * Go through our child widgets and find things that look like * L{Mantissa.LiveForm.FormWidget}s. * * @return: a list of form widgets. * @type: C{Array} */ function _getSubForms(self) { var subForms = []; for (var i = 0; i < self.childWidgets.length; i++) { var wdgt = self.childWidgets[i]; if ((wdgt.formName !== undefined) && (wdgt.gatherInputs !== undefined)) { subForms.push(wdgt); } } return subForms; }, /** * Callback invoked with the result of the form submission after the server * has responded successfully. */ function submitSuccess(self, result) { var subForms = self._getSubForms(); for(var i = 0; i < subForms.length; i++) { subForms[i].submitSuccess(result); } }, /** * Returns a mapping of input node names to arrays of input node values */ function gatherInputs(self) { var inputs = {}; var pushOneValue = function(name, value) { if (inputs[name] === undefined) { inputs[name] = []; } inputs[name].push(value); }; // First we gather our widget children. var subForms = self._getSubForms(); for(var i = 0; i < subForms.length; i++) { pushOneValue(subForms[i].formName, subForms[i].gatherInputs()); } var accessors = self.gatherInputAccessors(); for(var nodeName in accessors) { for(i = 0; i < accessors[nodeName].length; i++) { pushOneValue(nodeName, accessors[nodeName][i].get()); } } return inputs; }, /** * Utility which passes L{node} to L{Divmod.Runtime.theRuntime.traverse}. */ function traverse(self, visitor) { return Divmod.Runtime.theRuntime.traverse(self.node, visitor); }, /** * Helper function which returns an object with C{get} and C{set} members * for getting and setting the value(s) of a with single selection, the return value of C{get} and the * argument passed to C{set} will be atoms. For multiple-select nodes, * they will be lists. For multiple-select nodes, it will unselect all * selected

liveform_
PK9F,#xmantissa/themes/base/liveform.html
liveform_
liveform_
PK9FRP xmantissa/themes/base/login.html PK9F;>>.xmantissa/themes/base/mugshot-upload-form.html
PK9Ff(81xmantissa/themes/base/offering-configuration.html
Offerings
  • :
  • :
PK9F+e` ` +xmantissa/themes/base/people-organizer.html
Filter People Add / Import Person
PK9F#&&Exmantissa/themes/base/person-contact-read-only-phone-number-view.html
Phone Number: ()
PK9F8xmantissa/themes/base/person-contact-read-only-view.html
:
PK9Fk6(xmantissa/themes/base/person-detail.html
PK9F#BB*xmantissa/themes/base/person-extracts.html
PK9F.,*xmantissa/themes/base/person-fragment.html
 
   
 
PK9FvBDD)xmantissa/themes/base/person-plugins.html
PK9F{gg8xmantissa/themes/base/person-read-only-contact-info.html
PK9F5 WW)xmantissa/themes/base/person-summary.html
PK9F*EiNN-xmantissa/themes/base/port-configuration.html
TCP Services
SSL Services
New Service
PK9F=I,xmantissa/themes/base/preference-editor.html
PK9F)0xmantissa/themes/base/product-configuration.html
PK9Fev,xmantissa/themes/base/repeated-liveform.html PK9F9V,xmantissa/themes/base/reset-check-email.html

Email has been sent

Please check your email for further instructions.

PK9F9/!~~%xmantissa/themes/base/reset-done.html

Password has been reset

You can now login.

PK9F=r )xmantissa/themes/base/reset-step-two.html
 
 
Enter your new password below.
New Password:
Confirm:
 
PK9FB xmantissa/themes/base/reset.html
 
 
To have your password reset, please enter the
username for your account.
Account Name:
Alternatively, you can enter your email address.
Email Address
 
PK9F*O#xmantissa/themes/base/scroller.html
PK9Fay!xmantissa/themes/base/search.html
PK9F!#xmantissa/themes/base/settings.html
PK9Fؠ(( xmantissa/themes/base/shell.html <script type="text/javascript" src="/Mantissa/js/shell.js"></script> <base><nevow:attr name="href" nevow:render="rootURL" /><!-- IE dislikes self-closing base tags --></base> </head> <body> <table style="width: 100%;" cellspacing="0" cellpadding="0" class="mantissa-global-menubar"> <thead /> <tbody> <tr valign="center"> <td class="mantissa-global-navigation-spacer" /> <td class="search-container" align="right"> <table cellpadding="0" cellspacing="0" style="height: 100%"> <thead /> <tbody> <tr valign="center"> <td> <span class="mantissa-global-username" nevow:render="username"> You are logged in as </span> <span class="mantissa-global-navigation-link" nevow:render="logout"> <span class="mantissa-global-navigation-separator">|</span> <a class="mantissa-global-navigation-link"> <nevow:attr name="href" nevow:render="settingsLink" /> Settings </a> <span class="mantissa-global-navigation-separator">|</span> <a href="/__logout__"> Log out </a> </span> </td> <td nevow:render="search"> <a href="#" onclick="MantissaShell.searchButtonClicked(this); return false" id="search-button"> <img src="/Mantissa/images/search-button-unselected.png" border="0" /> </a> <form id="search-form" style="display: none"> <nevow:attr name="action"><nevow:slot name="form-action" /></nevow:attr> <table> <tr valign="center"> <td> <input name="term" class="search-text" type="text" /> <input type="hidden" name="_charset_" /> </td> <td> <a href="#" onclick="document.forms['search-form'].submit(); return false"> <img src="/Mantissa/images/search-button-small.png" border="0" /> </a> </td> </tr> </table> </form> </td> </tr> </tbody> <tfoot /> </table> </td> <td class="mantissa-global-navigation-links" nevow:render="authenticateLinks"> <a class="mantissa-global-navigation-link" href="/login/private">Log in</a> <a class="mantissa-global-navigation-link" nevow:pattern="signup-link"> <span class="mantissa-global-navigation-separator">|</span> <nevow:attr name="href"><nevow:slot name="url" /></nevow:attr> <nevow:slot name="prompt" /> </a> </td> </tr> </tbody> </table> <table style="width: 100%;" cellspacing="0" cellpadding="0" class="mantissa-application-menubar"> <thead /> <tbody> <tr> <td style="height: 2em;" /> <td class="app-nav-container" nevow:render="applicationNavigation"> <nevow:invisible nevow:pattern="subtab"> <nevow:slot name="tab-contents" /> </nevow:invisible> <a nevow:pattern="subtab-contents"> <nevow:attr name="href"><nevow:slot name="href"/></nevow:attr> <nevow:slot name="name" /> </a> <nevow:slot name="tabs" /> <div nevow:pattern="app-tab" class="tab-button"> <nevow:slot name="tab-contents" /> <div class="subtabs" style="z-index: 1000; display: none; position: absolute" onmouseover="MantissaShell.subtabHover(this);" onmouseout="MantissaShell.tabUnhover(this);" ><nevow:slot name="subtabs" /></div> </div> <div nevow:pattern="selected-app-tab" class="selected-tab-button"> <nevow:slot name="tab-contents" /> <div class="subtabs" style="z-index: 1000; display: none; position: absolute; " onmouseover="MantissaShell.subtabHover(this);" onmouseout="MantissaShell.tabUnhover(this);" ><nevow:slot name="subtabs" /></div> </div> <table nevow:pattern="selected-tab-contents" onmouseover="MantissaShell.tabHover(this, false)" onmouseout="MantissaShell.tabUnhover(this)" cellpadding="0" cellspacing="0"> <tr class="tab-button-row"> <td class="tab-tl-corner" /> <td class="tab-top-edge" /> <td class="tab-tr-corner" /> </tr> <tr class="tab-text-row"> <td class="tab-left-edge" /> <td class="tab-text-cell"> <div> <a> <nevow:attr name="href"><nevow:slot name="href" /></nevow:attr> <nevow:slot name="name" /> </a> </div> </td> <td class="tab-right-edge" /> </tr> </table> <table nevow:pattern="tab-contents" onmouseover="MantissaShell.tabHover(this, false)" onmouseout="MantissaShell.tabUnhover(this)" cellpadding="0" cellspacing="0"> <tr class="tab-button-row"> <td class="tab-tl-corner" /> <td class="tab-top-edge" /> <td class="tab-tr-corner" /> </tr> <tr class="tab-text-row"> <td class="tab-left-edge" /> <td class="tab-text-cell"> <a onmouseover="MantissaShell.tabHover(this, false)" onmouseout="MantissaShell.tabUnhover(this)"> <nevow:attr name="href"><nevow:slot name="href" /></nevow:attr> <nevow:slot name="name" /> </a> </td> <td class="tab-right-edge" /> </tr> <tr class="tab-button-row"> <td class="tab-bl-corner" /> <td class="tab-bottom-edge" /> <td class="tab-br-corner" /> </tr> </table> </td> </tr> </tbody> </table> <div class="private-fragment-content" nevow:render="content" /> <div id="mantissa-footer" nevow:render="footer" /> <nevow:invisible nevow:render="urchin"> <script src="http://www.google-analytics.com/urchin.js" type="text/javascript"> </script> <script type="text/javascript"> _uacct = "<nevow:slot name='urchin-key' />"; urchinTracker(); </script> </nevow:invisible> </body> </html> PK�����9FJ?X����/���xmantissa/themes/base/signup-configuration.html<div xmlns:nevow="http://nevow.com/ns/nevow/0.1"> <div nevow:render="liveFragment"> <nevow:invisible nevow:render="signupConfigurationForm" /> </div> <div nevow:render="sequence" nevow:data="configuredSignupMechanisms"> <div nevow:pattern="item" nevow:render="mapping"> <span><nevow:slot name="typeName" /></span> <span><nevow:slot name="createdBy" /></span> <span><nevow:slot name="createdOn" /></span> </div> </div> </div> PK�����9FB;Ux��x��!���xmantissa/themes/base/signup.html<div xmlns:nevow="http://nevow.com/ns/nevow/0.1"> <script type="text/javascript" src="/Mantissa/js/signup.js" /> <form onsubmit="signup(this); return false;"> Email: <input id="input_email" type="text" name="email" /> <input type="submit" value="Create Account" name="SIGN UP NOW! I GOVERN YOUR THOUGHTS!!" /> </form> <div id="signup-status"> </div> </div> PK�����9F9=��=��+���xmantissa/themes/base/single-endowment.html<div xmlns:nevow="http://nevow.com/ns/nevow/0.1"> <input type="checkbox"> <nevow:attr name="name"><nevow:invisible nevow:render="name" /></nevow:attr> </input> <nevow:invisible nevow:render="name" /> <nevow:invisible nevow:render="description" /> <nevow:invisible nevow:render="configuration" /> </div> PK�����9Fm k���k���"���xmantissa/themes/base/suspend.html <nevow:invisible xmlns:nevow="http://nevow.com/ns/nevow/0.1"> Account suspended. </nevow:invisible> PK�����9F�������xmantissa/themes/base/tdb.html<div xmlns="http://www.w3.org/1999/xhtml" xmlns:nevow="http://nevow.com/ns/nevow/0.1" nevow:render="liveFragment"> <div nevow:render="navigation" /> <div class="tdb-table"> <div nevow:render="table" /> </div> <table style="border-spacing: 0px" nevow:pattern="navigation" class="tdb-navigation-panel"> <tr valign="center"> <td> <!-- set display: none on half of the navigation buttons so that they dont appear and then dissappear if the javascript takes a while to load --> <a style="display: none;" class="first-page" href="#" onclick="return Mantissa.TDB.Controller.get(this).firstPage();"> <img src="/Mantissa/images/first.png" border="0" /> </a> <img class="first-page-disabled" src="/Mantissa/images/first_disabled.png" border="0" /> <a style="display: none;" class="prev-page" href="#" onclick="return Mantissa.TDB.Controller.get(this).prevPage();"> <img src="/Mantissa/images/prev.png" border="0" /> </a> <img class="prev-page-disabled" src="/Mantissa/images/prev_disabled.png" border="0" /> <a style="display: none;" class="next-page" href="#" onclick="return Mantissa.TDB.Controller.get(this).nextPage();"> <img src="/Mantissa/images/next.png" border="0" /> </a> <img class="next-page-disabled" src="/Mantissa/images/next_disabled.png" border="0" /> <a style="display: none;" class="last-page" href="#" onclick="return Mantissa.TDB.Controller.get(this).lastPage()"> <img src="/Mantissa/images/last.png" border="0" /> </a> <img class="last-page-disabled" src="/Mantissa/images/last_disabled.png" border="0" /> <img style="visibility: hidden;" class="throbber" src="/Mantissa/images/circle-throbber.gif" /> </td> <td class="tdb-action-result"></td> <td class="tdb-control-panel" style="display: none"> viewing items <span class="tdb-item-start" /> to <span class="tdb-item-end" /> out of <span class="tdb-total-items" /> </td> </tr> </table> <td nevow:pattern="cell"> <nevow:attr name="class"><nevow:slot name="class" /></nevow:attr> <nevow:attr name="onclick"><nevow:slot name="onclick" /></nevow:attr> <nevow:slot name="value" /> </td> <th nevow:pattern="column-header"> <nevow:attr name="width"><nevow:slot name="width" /></nevow:attr> <nevow:slot name="name" /> </th> <th nevow:pattern="sortable-column-header" class="tdb-clickable-header"> <nevow:attr name="onclick"><nevow:slot name="onclick" /></nevow:attr> <nevow:attr name="width"><nevow:slot name="width" /></nevow:attr> <nevow:slot name="name" /> </th> <th nevow:pattern="sorted-column-header-ascending" class="tdb-clickable-header"> <nevow:attr name="onclick"><nevow:slot name="onclick" /></nevow:attr> <nevow:attr name="width"><nevow:slot name="width" /></nevow:attr> <nevow:slot name="name" /> <span class="sort-arrow">↑</span> </th> <th nevow:pattern="sorted-column-header-descending" class="tdb-clickable-header"> <nevow:attr name="onclick"><nevow:slot name="onclick" /></nevow:attr> <nevow:attr name="width"><nevow:slot name="width" /></nevow:attr> <nevow:slot name="name" /> <span class="sort-arrow">↓</span> </th> <tr nevow:pattern="row"> <nevow:attr name="class"><nevow:slot name="class" /></nevow:attr> <nevow:slot name="cells" /> </tr> <div class="tdb-no-rows-dialog" nevow:pattern="no-rows"> No <nevow:slot name="items-called" /> To Display <span class="tdbtbody" /> </div> <table xmlns="http://www.w3.org/1999/xhtml" class="tdb" nevow:pattern="table"> <nevow:attr name="width"><nevow:slot name="width" /></nevow:attr> <thead> <tr> <nevow:slot name="column-headers" /> </tr> </thead> <tfoot></tfoot> <tbody class="tdbtbody"> <nevow:slot name="rows" /> </tbody> </table> </div> PK�����9Fk`w���w���+���xmantissa/themes/base/traceback-viewer.html<div xmlns:nevow="http://nevow.com/ns/nevow/0.1"> Tracebacks: <nevow:invisible nevow:render="tracebacks" /> </div> PK�����9F xƩ����/���xmantissa/themes/base/uninstalled-offering.html<li xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="athenaID" athena:class="Mantissa.UninstalledOffering" class="uninstalled" onclick="Mantissa.UninstalledOffering.get(this).install(); return false;"> <nevow:invisible nevow:render="mapping"> <nevow:slot name="name" />: <nevow:slot name="description" /> </nevow:invisible> </li> PK�����9FQ۾������&���xmantissa/themes/base/user-detail.html<div xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> <nevow:invisible nevow:render="productForm" /> </div> PK�����9Fj w��w��+���xmantissa/themes/base/user-info-signup.html<form xmlns="http://www.w3.org/1999/xhtml" xmlns:nevow="http://nevow.com/ns/nevow/0.1" nevow:render="liveElement" action="#" onsubmit="Nevow.Athena.Widget.get(this).submit(); return false" class="user-info-signup-form" > <div class="progress-message" style="display: none"> Signing Up... </div> <div class="success-message" style="display: none"> <div> Congratulations! You've signed up. </div> <div> <a href="/login">Now, to start using the site, click here to log in.</a> </div> </div> <div style="display: none" class="failure-message"> Your attempt to sign up has failed on the server for some reason. This should really never happen: please report this error to <a href="mailto:support@divmod.com">Divmod support</a>. </div> <table cellpadding="0" cellspacing="0" align="center" class="divmod-table"> <tr class="divmod-table-header"> <td class="divmod-table-header-left-block" /> <td class="divmod-table-header-stretcher"> </td> <td class="divmod-table-header-right-block" align="right"> <img src="/Mantissa/images/divmod-table-header-right-block.png" /> </td> </tr> <tr class="divmod-table-body-container"> <td colspan="3"> <table cellpadding="0" cellspacing="0" style="width: 100%"> <tr class="divmod-table-body"> <td class="divmod-table-body-left-edge"> </td> <td> <table cellpadding="0" cellspacing="0" style="width: 100%"> <tr> <td class="divmod-table-body-content"> <table> <tr> <td class="label">Real Name</td> <td class="verified-field"> <input type="text" name="realName" class="text-input" onkeyup="Nevow.Athena.Widget.get(this).verifyNotEmpty(this)" onblur="Nevow.Athena.Widget.get(this).verifyNotEmpty(this)" onfocus="Nevow.Athena.Widget.get(this).focus(this)" /> <img src="/Mantissa/images/neutral-small.png" class="verify-status" /> </td> </tr> <tr> <td class="label">User Name</td> <td class="verified-field"> <input type="text" name="username" class="text-input" onkeyup="Nevow.Athena.Widget.get(this).verifyUsernameAvailable(this)" onblur="Nevow.Athena.Widget.get(this).verifyUsernameAvailable(this)" onfocus="Nevow.Athena.Widget.get(this).focus(this)" /> <img src="/Mantissa/images/neutral-small.png" class="verify-status" /> </td> </tr> <tr> <td class="label">Password</td> <td class="verified-field"> <input type="password" name="password" class="password-input" onkeyup="Nevow.Athena.Widget.get(this).verifyStrongPassword(this)" onblur="Nevow.Athena.Widget.get(this).verifyStrongPassword(this)" onfocus="Nevow.Athena.Widget.get(this).focus(this)" /> <img src="/Mantissa/images/neutral-small.png" class="verify-status" /> </td> </tr> <tr> <td class="label">Confirm Password</td> <td class="verified-field"> <input type="password" name="confirmPassword" class="password-input" onkeyup="Nevow.Athena.Widget.get(this).verifyPasswordsMatch(this)" onblur="Nevow.Athena.Widget.get(this).verifyPasswordsMatch(this)" onfocus="Nevow.Athena.Widget.get(this).focus(this)" /> <img src="/Mantissa/images/neutral-small.png" class="verify-status" /> </td> </tr> <tr> <td class="label">Email Address</td> <td class="verified-field"> <input type="text" name="emailAddress" class="text-input" onkeyup="Nevow.Athena.Widget.get(this).verifyValidEmail(this)" onblur="Nevow.Athena.Widget.get(this).verifyValidEmail(this)" onfocus="Nevow.Athena.Widget.get(this).focus(this)" /> <img src="/Mantissa/images/neutral-small.png" class="verify-status" /> </td> </tr> <tr> <td /> <td class="validation-message">Please enter your details</td> </tr> <tr> <td /> <td class="divmod-table-body-button-container"> <input type="submit" name="__submit__" value="Create Account" disabled="true" /> </td> </tr> </table> </td> </tr> </table> </td> <td class="divmod-table-body-right-edge"> </td> </tr> <tr class="divmod-table-footer-container"> <td colspan="3"> <table cellpadding="0" cellspacing="0"> <tr class="divmod-table-footer"> <td class="divmod-table-footer-left-corner"><div style="width: 10px" /></td> <td class="divmod-table-footer-bottom-edge" /> <td class="divmod-table-footer-right-corner"><div style="width: 10px" /></td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </form> PK�����9F& "�� "�����axiom/plugins/mantissacmd.py import sys, os, struct from zope.interface import directlyProvides from twisted.python import util from twisted.cred import portal from twisted.plugin import IPlugin from axiom import errors as eaxiom from axiom.scripts import axiomatic from axiom.attributes import AND from axiom.dependency import installOn from axiom.iaxiom import IVersion from xmantissa.ixmantissa import IOfferingTechnician from xmantissa import webadmin, publicweb, stats from xmantissa.web import SiteConfiguration from xmantissa.terminal import SecureShellConfiguration from xmantissa.port import TCPPort, SSLPort from xmantissa.plugins.baseoff import baseOffering # PortConfiguration isn't used here, but it's a plugin, so it gets discovered # here. from xmantissa.port import PortConfiguration #version also gets registered as a plugin here. from xmantissa import version from epsilon.asplode import splode from epsilon.scripts import certcreate directlyProvides(version, IPlugin, IVersion) def gtpswd(prompt, confirmPassword): """ Temporary wrapper for Twisted's getPassword until a version that supports customizing the 'confirm' prompt is released. """ try: return util.getPassword(prompt=prompt, confirmPrompt=confirmPassword, confirm=True) except TypeError: return util.getPassword(prompt=prompt, confirm=True) def genSerial(): """ Generate a (hopefully) unique integer usable as an SSL certificate serial. """ return abs(struct.unpack('!l', os.urandom(4))[0]) class Mantissa(axiomatic.AxiomaticCommand): """ Create all the moving parts necessary to begin interactively developing a Mantissa application component of your own. """ # Throughout here we use findOrCreate rather than raw creation so that # duplicate installations of these components do not create garbage # objects. # Yea? Where's the unit tests for that? And who re-runs "axiomatic # mantissa" on the same Store multiple times anyway? Better that it should # give an error than transparently... do... something... maybe... I'm # actively not preserving the idempotency behavior in my changes to this # code. -exarkun name = 'mantissa' description = 'Blank Mantissa service' longdesc = __doc__ optParameters = [ ('admin-user', 'a', 'admin@localhost', 'Account name for the administrative user.'), ('admin-password', 'p', None, 'Password for the administrative user ' '(if omitted, will be prompted for).'), ('public-url', None, '', 'URL at which to publish the public front page.')] def postOptions(self): siteStore = self.parent.getStore() if self['admin-password'] is None: pws = u'Divmod\u2122 Mantissa\u2122 password for %r: ' % (self['admin-user'],) self['admin-password'] = gtpswd((u'Enter ' + pws).encode(sys.stdout.encoding, 'ignore'), (u'Confirm ' + pws).encode(sys.stdout.encoding, 'ignore')) publicURL = self.decodeCommandLine(self['public-url']) adminUser = self.decodeCommandLine(self['admin-user']) adminPassword = self['admin-password'] adminLocal, adminDomain = adminUser.split(u'@') siteStore.transact(self.installSite, siteStore, adminDomain, publicURL) siteStore.transact( self.installAdmin, siteStore, adminLocal, adminDomain, adminPassword) def installSite(self, siteStore, domain, publicURL, generateCert=True): """ Create the necessary items to run an HTTP server and an SSH server. """ certPath = siteStore.filesdir.child("server.pem") if generateCert and not certPath.exists(): certcreate.main([ '--filename', certPath.path, '--quiet', '--serial-number', str(genSerial()), '--hostname', domain]) # Install the base Mantissa offering. IOfferingTechnician(siteStore).installOffering(baseOffering) # Make the HTTP server baseOffering includes listen somewhere. site = siteStore.findUnique(SiteConfiguration) site.hostname = domain installOn( TCPPort(store=siteStore, factory=site, portNumber=8080), siteStore) installOn( SSLPort(store=siteStore, factory=site, portNumber=8443, certificatePath=certPath), siteStore) # Make the SSH server baseOffering includes listen somewhere. shell = siteStore.findUnique(SecureShellConfiguration) installOn( TCPPort(store=siteStore, factory=shell, portNumber=8022), siteStore) # Install a front page on the top level store so that the # developer will have something to look at when they start up # the server. fp = siteStore.findOrCreate(publicweb.FrontPage, prefixURL=u'') installOn(fp, siteStore) def installAdmin(self, s, username, domain, password): # Add an account for our administrator, so they can log in through the # web. r = portal.IRealm(s) try: acc = r.addAccount(username, domain, password, internal=True, verified=True) except eaxiom.DuplicateUser: acc = r.accountByAddress(username, domain) accStore = acc.avatars.open() accStore.transact(webadmin.endowAdminPowerups, accStore) class Generate(axiomatic.AxiomaticCommand): name = "project" # This will show up next to the name in --help output description = "Generate most basic skeleton of a Mantissa app" optParameters = [ ('name', 'n', None, 'The name of the app to deploy'), ] def postOptions(self): if self['name'] is None: proj = '' while( proj == ''): try: proj = raw_input("Please provide the name of the app to deploy: " ) except KeyboardInterrupt: raise SystemExit() else: proj = self.decodeCommandLine(self['name']) proj = proj.lower() capproj = proj.capitalize() print "Creating", capproj, "in", capproj fObj = file(util.sibpath(__file__, 'template.txt')) splode(fObj.readlines(), proj, capproj) class RemoteStatsAdd(axiomatic.AxiomaticSubCommand): optParameters = [ ("host", "h", None, "The host accepting statistical data."), ("port", "p", None, "The port to connect to."), ] def postOptions(self): s = self.parent.parent.getStore() s.transact(self.installCollector, s, self['host'], int(self['port'])) def installCollector(self, s, host, port): ss = portal.IRealm(s).accountByAddress(u'mantissa', None).avatars.open() stats.RemoteStatsObserver(store=ss, hostname=host, port=port) class RemoteStatsList(axiomatic.AxiomaticSubCommand): def postOptions(self): s = self.parent.parent.getStore() ss = portal.IRealm(s).accountByAddress(u'mantissa', None).avatars.open() for i, obs in enumerate(ss.query(stats.RemoteStatsObserver)): print "%s) %s:%s" % (i, obs.hostname, obs.port) class RemoteStatsRemove(axiomatic.AxiomaticSubCommand): optParameters = [ ("host", "h", None, "The hostname of the observer to remove."), ("port", "p", None, "The port of the observer to remove."), ] def postOptions(self): s = self.parent.parent.getStore() ss = portal.IRealm(s).accountByAddress(u'mantissa', None).avatars.open() for obs in ss.query(stats.RemoteStatsObserver, AND(stats.RemoteStatsObserver.hostname==self['host'], stats.RemoteStatsObserver.port==int(self['port']))): obs.deleteFromStore() class RemoteStats(axiomatic.AxiomaticCommand): name = "stats" description = "Control remote statistics collection" subCommands = [("add", None, RemoteStatsAdd, "Submit Mantissa statistical data to another server"), ("list", None, RemoteStatsList, "List remote targets for stats delivery"), ("remove", None, RemoteStatsRemove, "Remove a remote stats target")] __all__ = [ PortConfiguration.__name__, Mantissa.__name__, Generate.__name__, RemoteStats.__name__, RemoteStatsAdd.__name__, RemoteStatsList.__name__, RemoteStatsRemove.__name__] PK�����9Fi΅�������axiom/plugins/offeringcmd.py# -*- test-case-name: xmantissa.test.test_offering -*- # Copyright 2008 Divmod, Inc. See LICENSE file for details """ Axiomatic commands for manipulating Mantissa offerings. """ from twisted.python import usage from axiom.scripts import axiomatic from xmantissa import offering, publicweb class Install(axiomatic.AxiomaticSubCommand): synopsis = "<offering>" def parseArgs(self, offering): self["offering"] = self.decodeCommandLine(offering) def postOptions(self): for o in offering.getOfferings(): if o.name == self["offering"]: offering.installOffering(self.store, o, None) break else: raise usage.UsageError("No such offering") class List(axiomatic.AxiomaticSubCommand): def postOptions(self): for o in offering.getOfferings(): print "%s: %s" % (o.name, o.description) class SetFrontPage(axiomatic.AxiomaticSubCommand): """ Command for selecting the site front page. """ def parseArgs(self, offering): """ Collect an installed offering's name. """ self["name"] = self.decodeCommandLine(offering) def postOptions(self): """ Find an installed offering and set the site front page to its application's front page. """ o = self.store.findFirst( offering.InstalledOffering, (offering.InstalledOffering.offeringName == self["name"])) if o is None: raise usage.UsageError("No offering of that name" " is installed.") fp = self.store.findUnique(publicweb.FrontPage) fp.defaultApplication = o.application class OfferingCommand(axiomatic.AxiomaticCommand): name = "offering" description = "View and accept the offerings of puny mortals." subCommands = [ ("install", None, Install, "Install an offering."), ("list", None, List, "List available offerings."), ("frontpage", None, SetFrontPage, "Select an application for the front page."), ] def getStore(self): return self.parent.getStore() PK�����9FNG7V��V�����axiom/plugins/webcmd.py# -*- test-case-name: xmantissa.test.test_webcmd -*- import os import sys from twisted.python import reflect from twisted.python.usage import UsageError from axiom import item, attributes from axiom.dependency import installOn, onlyInstallPowerups from axiom.scripts import axiomatic from xmantissa.web import SiteConfiguration from xmantissa.website import StaticSite, APIKey from xmantissa import ixmantissa, webadmin from xmantissa.plugins.baseoff import baseOffering class WebConfiguration(axiomatic.AxiomaticCommand): name = 'web' description = 'Web. Yay.' optParameters = [ ('http-log', 'h', None, 'Filename (relative to files directory of the store) to which to log ' 'HTTP requests (empty string to disable)'), ('hostname', 'H', None, 'Canonical hostname for this server (used in URL generation).'), ('urchin-key', '', None, 'Google Analytics API key for this site')] def __init__(self, *a, **k): super(WebConfiguration, self).__init__(*a, **k) self.staticPaths = [] didSomething = 0 def postOptions(self): siteStore = self.parent.getStore() # Make sure the base mantissa offering is installed. offeringTech = ixmantissa.IOfferingTechnician(siteStore) offerings = offeringTech.getInstalledOfferingNames() if baseOffering.name not in offerings: raise UsageError( "This command can only be used on Mantissa databases.") # It is, we can make some simplifying assumptions. Specifically, # there is exactly one SiteConfiguration installed. site = siteStore.findUnique(SiteConfiguration) if self['http-log'] is not None: if self['http-log']: site.httpLog = siteStore.filesdir.preauthChild( self['http-log']) else: site.httpLog = None if self['hostname'] is not None: if self['hostname']: site.hostname = self.decodeCommandLine(self['hostname']) else: raise UsageError("Hostname may not be empty.") if self['urchin-key'] is not None: # Install the API key for Google Analytics, to enable tracking for # this site. APIKey.setKeyForAPI( siteStore, APIKey.URCHIN, self['urchin-key'].decode('ascii')) # Set up whatever static content was requested. for webPath, filePath in self.staticPaths: staticSite = siteStore.findFirst( StaticSite, StaticSite.prefixURL == webPath) if staticSite is not None: staticSite.staticContentPath = filePath else: staticSite = StaticSite( store=siteStore, staticContentPath=filePath, prefixURL=webPath, sessionless=True) onlyInstallPowerups(staticSite, siteStore) def opt_static(self, pathMapping): webPath, filePath = self.decodeCommandLine(pathMapping).split(os.pathsep, 1) if webPath.startswith('/'): webPath = webPath[1:] self.staticPaths.append((webPath, os.path.abspath(filePath))) def opt_list(self): self.didSomething = 1 s = self.parent.getStore() for ws in s.query(SiteConfiguration): print 'The hostname is', ws.hostname if ws.httpLog is not None: print 'Logging HTTP requests to', ws.httpLog break else: print 'No configured webservers.' def powerupsWithPriorityFor(interface): for cable in s.query( item._PowerupConnector, attributes.AND(item._PowerupConnector.interface == unicode(reflect.qual(interface)), item._PowerupConnector.item == s), sort=item._PowerupConnector.priority.descending): yield cable.powerup, cable.priority print 'Sessionless plugins:' for srp, prio in powerupsWithPriorityFor(ixmantissa.ISessionlessSiteRootPlugin): print ' %s (prio. %d)' % (srp, prio) print 'Sessioned plugins:' for srp, prio in powerupsWithPriorityFor(ixmantissa.ISiteRootPlugin): print ' %s (prio. %d)' % (srp, prio) sys.exit(0) opt_static.__doc__ = """ Add an element to the mapping of web URLs to locations of static content on the filesystem (webpath%sfilepath) """ % (os.pathsep,) class WebAdministration(axiomatic.AxiomaticCommand): name = 'web-admin' description = 'Administrative controls for the web' optFlags = [ ('admin', 'a', 'Enable administrative controls'), ('developer', 'd', 'Enable developer controls'), ('disable', 'D', 'Remove the indicated options, instead of enabling them.'), ] def postOptions(self): s = self.parent.getStore() didSomething = False if self['admin']: didSomething = True if self['disable']: for app in s.query(webadmin.AdminStatsApplication): app.deleteFromStore() break else: raise UsageError('Administrator controls already disabled.') else: installOn(webadmin.AdminStatsApplication(store=s), s) if self['developer']: didSomething = True if self['disable']: for app in s.query(webadmin.DeveloperApplication): app.deleteFromStore() break else: raise UsageError('Developer controls already disabled.') else: installOn(webadmin.DeveloperApplication(store=s), s) if not didSomething: raise UsageError("Specify something or I won't do anything.") PK�����9F������!���nevow/plugins/mantissa_package.py from twisted.python import util from nevow import athena import xmantissa mantissaPkg = athena.AutoJSPackage(util.sibpath(xmantissa.__file__, 'js')) PK�����[7$G^- ��� ���(���Mantissa-0.8.2.dist-info/DESCRIPTION.rstUNKNOWN PK�����[7$G}%R��R��&���Mantissa-0.8.2.dist-info/metadata.json{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", "Framework :: Twisted", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: JavaScript", "Programming Language :: Python", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Terminals"], "extensions": {"python.details": {"contacts": [{"email": "mithrandi@mithrandi.net", "name": "Tristan Seligmann", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/twisted/mantissa"}}}, "extras": [], "generator": "bdist_wheel (0.24.0)", "license": "MIT", "metadata_version": "2.0", "name": "Mantissa", "platform": "any", "run_requires": [{"requires": ["Twisted (>=14.0.0)", "PyOpenSSL (>=0.13)", "Axiom (>=0.7.0)", "Vertex (>=0.2.0)", "PyTZ", "Pillow", "cssutils (>=0.9.5)", "Nevow (>=0.9.5)", "PyCrypto"]}], "summary": "A multiprotocol application deployment platform", "version": "0.8.2"}PK�����Z7$Gcw&/���/���!���Mantissa-0.8.2.dist-info/pbr.json{"is_release": false, "git_version": "ae6fe52"}PK�����Z7$G'������&���Mantissa-0.8.2.dist-info/top_level.txtaxiom nevow xmantissa PK�����[7$G4\���\������Mantissa-0.8.2.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.24.0) Root-Is-Purelib: true Tag: py2-none-any PK�����[7$G2����!���Mantissa-0.8.2.dist-info/METADATAMetadata-Version: 2.0 Name: Mantissa Version: 0.8.2 Summary: A multiprotocol application deployment platform Home-page: https://github.com/twisted/mantissa Author: Tristan Seligmann Author-email: mithrandi@mithrandi.net License: MIT Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Framework :: Twisted Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: JavaScript Classifier: Programming Language :: Python Classifier: Topic :: Internet Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Classifier: Topic :: Terminals Requires-Dist: Twisted (>=14.0.0) Requires-Dist: PyOpenSSL (>=0.13) Requires-Dist: Axiom (>=0.7.0) Requires-Dist: Vertex (>=0.2.0) Requires-Dist: PyTZ Requires-Dist: Pillow Requires-Dist: cssutils (>=0.9.5) Requires-Dist: Nevow (>=0.9.5) Requires-Dist: PyCrypto UNKNOWN PK�����[7$GDI��I�����Mantissa-0.8.2.dist-info/RECORDxmantissa/_recordattr.py,sha256=NWSCv3mBlLjWdB6BlFL61kymv2sfVYmPODXM3JfwtOE,6833 xmantissa/_webidgen.py,sha256=P-7n-BprPRSWDPq9bqgeikqEdKJci1dUdytd0C7UhYs,1802 xmantissa/_webutil.py,sha256=TGgHD2o10dV5CoTrwUb7WUfvydYxtx2vW0Dd6o9zzVM,8700 xmantissa/ampserver.py,sha256=rsfVJy2ALNnp3E5NyVtLolQw27aFu-iKyEXaBthZkIM,9394 xmantissa/cachejs.py,sha256=gVQPno_DFVR8KJT7M34y8T9d_8bsv9xHcZrZkL7wVmM,3888 xmantissa/endpoint.py,sha256=a8KKOZK0sAVm4GHPMsjt99GkFb74_aX7WbUCGqGcfKE,2032 xmantissa/error.py,sha256=1FLKGurSOX4qwRtC0f9GhvCZ_FN0jUU9tq5XTyQyeWk,3841 xmantissa/fragmentutils.py,sha256=Uim7Rmwgnkz8Q9L5wcqJG0FYGa-fW_HfbPQ55I172gc,1585 xmantissa/fulltext.py,sha256=8HD8zroFko6JVTY72OGfT8Dro0v2X_cAaR8n6Cqwuz0,26164 xmantissa/interstore.py,sha256=xfBFjkqOv1szqoJKGeYyFTucsUVyn0kTRLo1qdieXdI,37432 xmantissa/ixmantissa.py,sha256=tJwmy0AFGoYbjpzDskFB7bDR8w_GDmBawEv7a56gr5w,43809 xmantissa/liveform.py,sha256=oVwQCpxV_MtfXBnwWfHDlitc0JUr_bxwI8cWbkoxVz8,43101 xmantissa/myaccount.py,sha256=JVnbgLk59nfOznnxsYc39S1ga6O5HOacw1xyVfsUptI,444 xmantissa/offering.py,sha256=6eysoAYv8FJO3uC2TX37fZ1gW97sTaJPs10vWa7JnI4,10858 xmantissa/people.py,sha256=9ciqakRCMV0mABnfHh31G00_S1KMAb0mHyvEYrAh7Sg,85523 xmantissa/prefs.py,sha256=vfo9LDTLoYs96txORUuRHWSAGCPGXL3jm-LZQvPF420,7621 xmantissa/product.py,sha256=tqTB8H7iies57Aj3NLkbYE6oKoHKfDt_gMLIGYk1TEs,7403 xmantissa/publicresource.py,sha256=TTLCLUw1AOAMSI573DHO-y_cqhGXX5ZemFL7gWLEyUM,128 xmantissa/publicweb.py,sha256=87mVbDEygPEOOyXsdyFmbuSc-gj_bijwnkpmOwn8pys,37097 xmantissa/scrolltable.py,sha256=PQJ65aVOo6Xb6EvzsIyKkVILsHht-BV-m-8SSVC7SSE,33151 xmantissa/search.py,sha256=3Kgx0jUmbFt0hjhAOw0gMnW_HsHKHMc0IxqHhBs49Vk,4378 xmantissa/settings.py,sha256=u4n4FEJx1Mjlk4reoCf2PnEnXUuTtF3leatLY8sVb3M,425 xmantissa/sharing.py,sha256=clJJwL77JApENqJF5MY4DJKG4b4FH0zzI85xVce5lyc,30864 xmantissa/signup.py,sha256=KPKOJzPjfZs8ToSqYwWXXaX3HGgrunhFW2j8bgNQ58Y,47649 xmantissa/smtp.py,sha256=KtIza5Jmmihv8ZBEyeIcsd9_cpZUBoB-eXx0B8A7JrM,5968 xmantissa/stats.py,sha256=0FnjQyukk9wypu5gYL77dR3PofAv0Ouwr1Q9l2S-qWQ,9193 xmantissa/suspension.py,sha256=e9_blLMy8QZKfqVfksMlkRNezV9A9uma0Txum29E9Vo,2499 xmantissa/tdb.py,sha256=U2E00myCw0WI20_l6wfTvtLvaF2kA8WVvyITWv_YHzw,11548 xmantissa/tdbview.py,sha256=dw0lONKsWsMXFrYPdU_VSH35Fh7QqOUrKpIJi5oR2-g,11391 xmantissa/terminal.py,sha256=vlMgPFaPUx5kmm5KCqBu8oDhpWkOiSk42xhObMB2dCI,11140 xmantissa/web.py,sha256=u26KwJlIqs2EobrPsc4PBdvOsRWaTrm5eK-67lLQdcI,14921 xmantissa/webadmin.py,sha256=YL723zaCCu5IckoVld8Tjyiigjbq37F58lE81RGd3tc,29580 xmantissa/webapp.py,sha256=Lr-WmjqzFLmmR_WwWJU7h88wBcO6BBZSouwg0Y1DX5A,26373 xmantissa/webgestalt.py,sha256=byaQzM4evycrWmF1Cn3mhYkdH1ygHTHCRXPwF5aN2Mw,4826 xmantissa/webnav.py,sha256=AF99NQ-8dYFACJWuTJJ5NB9h_hlU4P0GbwPGXeK-NKc,10046 xmantissa/websession.py,sha256=HTjC3YmkeqpeFBPeY1Ei2hcX3n_9NI3lzb_EsZLd6xg,11180 xmantissa/websharing.py,sha256=de_jKIYAW3CzsUFFCbG5XTtBQdZKQTevQG_wu-rpmhw,11209 xmantissa/webtheme.py,sha256=RQtJWqz-iLY2Tk130j_OqkB9WvwhmiP4pIQuIRtI8lA,13534 xmantissa/__init__.py,sha256=V4zXtjfyKXY7_uwq3Daq2TmjWz-1Ulq47Cwjcxheaxg,304 xmantissa/website.py,sha256=XAV6IdEG53XcUV24YL9u6xU9XLS3OpEJLegPvZuO8Vo,29562 xmantissa/port.py,sha256=4jl-TNnpE4kmDGBZkwel9UYbtLXtbKENIk5CKAVrl7Y,17349 xmantissa/_version.py,sha256=B7GiO0rd49YwtLYjvPg4lmCZEDlMTonslQKdSImaMJk,22 xmantissa/reset.rfc2822,sha256=i_UiGa7bTnJj5jhhNy-IvR-wwnu9L4ClTxcK0c5va2w,302 xmantissa/signup.rfc2822,sha256=2oHW9McxobLuDQ1DcpEY3ECOyeX4D2iE6ghtmE9HpXU,287 xmantissa/statcollector.tac,sha256=8reEuYmk7mh21N4H8oQ-anR8odtOkLWLhBUGZ3KSiwo,320 xmantissa/plugins/__init__.py,sha256=4zpMmpKxe8njybQyOCubTlFRXj_MsybYR7bwxDdc7X8,181 xmantissa/plugins/adminoff.py,sha256=1XEOn14FXHS49RoclLMQgqelbMOQELmnh52zGEQuIZE,1465 xmantissa/plugins/baseoff.py,sha256=5bJ58hQBIc7dXSZzwPLooMVpVFoxPC6sM0xVmszGLyA,2191 xmantissa/plugins/free_signup.py,sha256=aDxG89d4ugMKIGvTAJ3QBq_xZ7eKA2SnCA0j9skA9lk,1059 xmantissa/plugins/offerings.py,sha256=V2a5yNnpzw7A_Nbk4SBskecxuuyhzTNI28wxTSNUuys,364 xmantissa/plugins/dropin.cache,sha256=z9VwyB8yeO1kJfMu-KDTHeMu8Xk-JQSg4nG8YCeTL4I,1488 xmantissa/test/__init__.py,sha256=PhCMNGKJyYjua6QPsjDaTAvmZK5_fV_BySac-By7kis,41 xmantissa/test/fakes.py,sha256=CkElB4zkp3EAm17fR7nx5ii8P2QevG8iB80ZV9-XNY4,9354 xmantissa/test/livetest_forms.py,sha256=Hy18Yxi4hg02AUZBD8aebrVwUl2jbAVqgBygp5UpRlQ,8120 xmantissa/test/livetest_people.py,sha256=w9378-3YYxCLCREljF9uvm0XssIiaMkmAU1qlNIK0Ro,4914 xmantissa/test/livetest_prefs.py,sha256=4VXBBHq4UEsDJnXSYZczU4auRrk1zMJAUYmxkhemPkg,1536 xmantissa/test/livetest_regions.py,sha256=RKpAexGCjTtROM-Z9neqMTK7Ae-RzZU-oi1kH7fYxt0,1929 xmantissa/test/livetest_scrolltable.py,sha256=spvQMwe-huTM2pocvguPF4NeuWW5ZK7Qe61--oVCLZQ,2547 xmantissa/test/livetest_signup.py,sha256=qlSO9fhkSrzklxyHKHyKRvcf8HYn2IOjCQnuceeeTB0,1146 xmantissa/test/livetest_stats.py,sha256=RVowzawpDxxGCdSnOByjEoy2ln-ixcokSgdVX7Sakag,1755 xmantissa/test/peopleutil.py,sha256=zJZOspEfFZGgfHwIHNUyk0P2QhzE9xZYpliCgod4GzU,15013 xmantissa/test/rendertools.py,sha256=U4IGV44QoHvo_ntJDiTElNhfmbJzUHfA-7AFIiXD9bE,2072 xmantissa/test/test_admin.py,sha256=V9x52YP0EefcmNcBH-fQYpPsbhSP_Y1pu5wt2Q_UgRg,6667 xmantissa/test/test_ampserver.py,sha256=r-bPrY7-KilGEFsVt3sf_Oty2-w5aYOKOjYgd9zUgWQ,14809 xmantissa/test/test_cachejs.py,sha256=KrvEg4V1h3ik_wjR3xDWJaNnp94ShXeGdWU79FGjWHA,3106 xmantissa/test/test_fulltext.py,sha256=jyci_1kRgi2Mmvu0Y9AKiUX5JR0fxYlXMFzP6pHAcVE,30712 xmantissa/test/test_interstore.py,sha256=FcuZdzUyEBJWLag3SDpsrl7zO8ypLT1vujX7FiUiDQ4,54535 xmantissa/test/test_javascript.py,sha256=c7rv_ZDb3P01TWzRU4NG8DoeclZc6b98TG8uYqxNg38,1910 xmantissa/test/test_liveform.py,sha256=CgL1cMCNbirmMOkHgd3MrmB0z4p57jxARW_BgY883bQ,57279 xmantissa/test/test_mantissacmd.py,sha256=0q2LZ0OHC0UZ6y7XR7C3NYRDjBNLWCiVv5kCQAUmxzE,4940 xmantissa/test/test_offering.py,sha256=hY4Un-hDqHi2Fvyg9WX82OcXo0Z9kg3NSBNHzJJYxBU,12563 xmantissa/test/test_password_reset.py,sha256=nGqA6RPIHkTyiTJ7YZ7MjYUQOl8J74kUOGh6IxmaBsE,11619 xmantissa/test/test_prefs.py,sha256=ZXmduNvRTfTHPpJWdrjNaiiP7uTSqbvvNVHvhiESIEQ,2586 xmantissa/test/test_product.py,sha256=AG-bWp_cTfeGhJVcJXX8AnS9TISeFXqxrH9rm417NiY,3767 xmantissa/test/test_q2q.py,sha256=zHB_ratgsoCgIWGEDTerqqJyDP8-9mg5tuMAymZlmLo,375 xmantissa/test/test_recordattr.py,sha256=oL3YD3Uc2KpUgfvtvydggoUv5gXneKM9ihra3zOKFnY,4528 xmantissa/test/test_rendertools.py,sha256=GaJbRayH9sL1J0DPATVNKYsv1gU6KNGO6ve0F57LlQ0,1212 xmantissa/test/test_search.py,sha256=ubRBNsRr8UA2S_fsYBANe3dCPZyP3KswJR7ZkwMXI6E,5267 xmantissa/test/test_sharing.py,sha256=UgiNm-XeUeuIRIOMmi5IJauV78JkTR_GZKVZ32ezx98,32051 xmantissa/test/test_signup.py,sha256=-FTyUqQnYSS3b1NwoATimZ7OitelJtaFqt67PgJhmbE,8937 xmantissa/test/test_siteroot.py,sha256=DvtwhRf9OKiwvliE7Kz2m_W2nQ3fU76bZfbjARxcMUs,1802 xmantissa/test/test_smtp.py,sha256=LzUyotKBoUxrChQD4cb09FZryOaUoksyIUxVqZKyqzc,3818 xmantissa/test/test_tdb.py,sha256=eFVct0jvn9bH_pYSF6RjRDhptEnn_UQrHOdvUwPxhfU,14386 xmantissa/test/test_terminal.py,sha256=qRWaH_XyeCPiPUmIv7aylDk0mk4CqGL6LXqzfaqE8cw,18496 xmantissa/test/test_theme.py,sha256=LmCdy_LQuoIfi-Ef2ZVVhSUWpbCghFTLtB5hiwr8lNY,15505 xmantissa/test/test_webapp.py,sha256=IiHzUpNmIzHOeuKtGmipDqUD4JoovkKsjDYjumxWOCA,15713 xmantissa/test/test_webcmd.py,sha256=SVDllgmVwRu4fshkaC2mNmnV8hzKxO0ysG9thcpJdP8,4624 xmantissa/test/test_webnav.py,sha256=YtPCNwXlD-75HeX3QG_1JozkewWSWoz0ANVTurPAoBw,11389 xmantissa/test/test_websession.py,sha256=GddsgTx_LGt14CoDPp9YKocRxQws9UZRKeDptCqj7Xg,5953 xmantissa/test/test_websharing.py,sha256=n4wajKdg8vIbAkmsudBPNa3Hbfue162Hlo8jIcY9uAU,19991 xmantissa/test/test_webshell.py,sha256=CndVAi-g3Yi6VvZc2Zw6HuhslHIHr0C08zEUdaa9Crs,11660 xmantissa/test/test_website.py,sha256=P2NZQbfr3hM670QUURIm4jagA9WfqdQ1NwfUN4bCM3E,48595 xmantissa/test/validation.py,sha256=TfWwgQtJuBCQLkohJdddIPf0zWerEb9r37rRO_bgx80,1305 xmantissa/test/test_scrolltable.py,sha256=Y5spR7wdThU44Q8k21EtfvsQ2L1iAaQBInRj-XKWUAY,33753 xmantissa/test/test_people.py,sha256=7fk3QAEW8dl2rY5RZw1-7yCt3LlfUpPjU5jdjaRM938,124957 xmantissa/test/test_publicweb.py,sha256=eR1IKUUuRyq1T4TDdYefCHUBW37Bwgx5UHHAeMD8XgI,38576 xmantissa/test/test_stats.py,sha256=uiNj1fQsvFS1aBTzJY6HKHe8T9VnY8BKXsiH4m6XFGk,4757 xmantissa/test/test_port.py,sha256=a7RZibRcXgVHTlX2fRdqxoYR2TNw4uBl_AfroVfTO-o,35877 xmantissa/test/test_howtolistings.py,sha256=8w10IFHZJxiXzfwTtg0Zqe2Msu4xLi9sXsQ_raEi4w4,5987 xmantissa/test/test_autocomplete.js,sha256=6y3WOqRsqkxj0yFNngf_AZTuGVHVdYGwaNN5XoKqdpk,9226 xmantissa/test/acceptance/__init__.py,sha256=nDKZYGzCKxvN-2j3UMfHBs7t1FicQTPOWA8FszSgIIk,162 xmantissa/test/acceptance/liveform.py,sha256=OwBudmjEIN17QUQuNLleW9fC204UEqRp-hrmVH1fUCs,3578 xmantissa/test/acceptance/people.py,sha256=K955l5M_YE0HengTyc9c-yipLKrmZ6NXWTB1l-vgi_E,863 xmantissa/test/acceptance/scrolltable.py,sha256=UUm6fuNZe3_G5fiEyIFd9fWm7mgaMkFKgQZ5hP9r3yQ,3892 xmantissa/test/historic/__init__.py,sha256=1JFaqmmGiKixjnTK2DUqFEV6H1gKV-K8T7S2YPue-EY,50 xmantissa/test/historic/stub_addPerson1to2.py,sha256=8f-ZYMqlCRZRHqN4Jnw9FZ7pE9r8YJ8afUPABX22xLA,202 xmantissa/test/historic/stub_adminstatsapplication1to2.py,sha256=McIf0iCze0-lkt0anOlgFb55C7hgo_R-dbMv0LijDxA,228 xmantissa/test/historic/stub_ampConfiguration1to2.py,sha256=-YlQ7jSn0TOg7iCokOQQzxO-yfCr6cvnPINXqD0EoIo,603 xmantissa/test/historic/stub_anonsite1to2.py,sha256=Q3PtI1Lyo52tZc8J4cwqec2PmcMqfEQw7IjHebk3SSU,329 xmantissa/test/historic/stub_defaultPreferenceCollection1to2.py,sha256=7mOzCXwB-ayXoJtGrnuL03G3osHeP1RwiZdQhP_0l_U,210 xmantissa/test/historic/stub_developerapplication1to2.py,sha256=hdbBLA8n61Gz_xyt11C7qi2kJN_JZUroGTwVirZRqPQ,226 xmantissa/test/historic/stub_emailAddress1to2.py,sha256=eHu1DWJpFA0n4COaVkdUdI3Byj_jjKv6paw4UTSUaKg,378 xmantissa/test/historic/stub_freeTicketSignup3to4.py,sha256=t9sArAqNpYhh5IBCYG-limyeclP3T2dgcq2GOuqDK3c,370 xmantissa/test/historic/stub_freeTicketSignup4to5.py,sha256=DpW8SSpfcAIooVt0t_HuRf53uGNmY0RHoqa3lTYeXoA,564 xmantissa/test/historic/stub_freeTicketSignup5to6.py,sha256=Ylv9J5x16i9Rou5cgE2WGEllOiy1779RhtadvNp0WAM,565 xmantissa/test/historic/stub_frontpage1to2.py,sha256=Nh7UQsIr_EXyNGMLaigEEzVtgOW2M7NOCq2lVte5ySY,405 xmantissa/test/historic/stub_messagequeue1to2.py,sha256=duZCjztGRNq7MHO-BHZWAle_RMIt3ZZNHgTZdkXWxjg,402 xmantissa/test/historic/stub_mugshot1to2.py,sha256=F5EkQ_GegK1b4kTb3uvsK9d21yjSJzyeExC1PrCxbsk,555 xmantissa/test/historic/stub_mugshot2to3.py,sha256=LV3JNVNjgc5HiLYq1sXoTcc3_HSf_IgBodrjxBxumvQ,1078 xmantissa/test/historic/stub_organizer2to3.py,sha256=SqWOO7PXGleBkgONC1K8_mJ2fe2IQqBfSqTUkD8Wgow,258 xmantissa/test/historic/stub_passwordReset1to2.py,sha256=KsMhnc9oEPF0NWGMGZBRb-ZUYrv3aS5z30YgY8LsbiA,224 xmantissa/test/historic/stub_people1to2.py,sha256=MPQue_FuWfKYoMMGt9m2_XvmMxV7RfaZle9Pbhv4a-s,202 xmantissa/test/historic/stub_person1to2.py,sha256=ZoSk-D-YVAo_D9yi3wsyGpXaJr-BR9MQxwcjfSXjWo8,610 xmantissa/test/historic/stub_person2to3.py,sha256=xG5TbmLys7qNOYksJWKbeHT8KlZHg9QpLfQbceD5NnA,611 xmantissa/test/historic/stub_phoneNumber1to2.py,sha256=REBo-QipI7CCxUCA8u4unki-6jt3qH4KTii5CIyjXmw,365 xmantissa/test/historic/stub_port1to2.py,sha256=L-OOLGY5Ttb_cWRbUNljUh2dqR_enZ5iT_dDIRtBchs,1300 xmantissa/test/historic/stub_privateApplication2to3.py,sha256=Uao3UcXPM85QPT3SLooktPyrqK0-TVWtdgGncU3BcQ0,220 xmantissa/test/historic/stub_privateApplication3to4.py,sha256=KjOE4Z8FWVT3z6m4VB5QD3ETQ9Bvm8IoQpsLPFvNIvE,1063 xmantissa/test/historic/stub_privateApplication4to5.py,sha256=-MOyVk46F5h0-FxcYlv-pGoCZCMa8XhowXio0Y4-bjw,1035 xmantissa/test/historic/stub_pyLuceneIndexer3to4.py,sha256=XidR64ztYbzIEteFLOiOFYcVNPsr9MrKmoFNbzbxY6Q,393 xmantissa/test/historic/stub_pyLuceneIndexer4to5.py,sha256=1Wr2G2-4fk4b_HQptFu6IHPUrlZq9L71nJKe-SoonNk,416 xmantissa/test/historic/stub_realname1to2.py,sha256=hoiZ032Zud4QVivioCwoPMbWqcFVzWDloR3LR3x8058,446 xmantissa/test/historic/stub_remoteIndexer1to2.py,sha256=NKEhPnMosojDvOTWsWEaFK-FPj1shYUNmhnQnfXE0aA,934 xmantissa/test/historic/stub_remoteIndexer2to3.py,sha256=2QnDiLdvxFqKC_oUPVkte5mr6FaEFBQJiSDZBO9CNt8,634 xmantissa/test/historic/stub_searchresult1to2.py,sha256=IdbltYT-uguRhIRdfYqdBH7luJGOevwMK80OFzpdixw,310 xmantissa/test/historic/stub_settings1to2.py,sha256=9nRVK55kdbHcEc2zgtNNUCzm6qG8e7M41XqG42v1lvM,216 xmantissa/test/historic/stub_statBucket1to2.py,sha256=cIZEhsBarMIN3nKakhSCQoC5uzjj7Kxus8ddytgQlo0,312 xmantissa/test/historic/stub_ticket1to2.py,sha256=iLpTPfkafwFc39HCnfvfrumJgGAhyeFE7HcFBs8UDxQ,867 xmantissa/test/historic/stub_userInfoSignup1to2.py,sha256=2eZ1y38ssXaF-KGzaZ7rZxi55S_y_9O8Z1x4DFAEdco,551 xmantissa/test/historic/stub_userinfo1to2.py,sha256=3phc7N3ND9Uc87vZIcpXzbMiZ9sAqHBfJsoBOHtTd_I,517 xmantissa/test/historic/stub_website3to4.py,sha256=dGwmt41-Vmy2MbOrmjgPEofUONfQpZ-oYkpEa1HO3KI,3152 xmantissa/test/historic/stub_website4to5.py,sha256=0HbHeeiNr3wgb6ToQC00XMxRyo9jFtBDtIIX-89c6Gs,3114 xmantissa/test/historic/stub_website5to6.py,sha256=EnkoBrIyJS3FtWgJNXjmqTE68OxIl8xLv1SSvSOmdpo,3249 xmantissa/test/historic/test_addPerson1to2.py,sha256=NDh8lN9E0I9bygFZVZiZw8L91KXwGdrhgYdbQUFpNXg,321 xmantissa/test/historic/test_adminstatsapplication1to2.py,sha256=_Xm2aHSOtdiLKCYxXP-roj5uwIw7etkH6Lfs1DNA3HY,452 xmantissa/test/historic/test_ampConfiguration1to2.py,sha256=sW8lDQbkfjxYyoR0VOIWkakPhZPtBDzwxchb4UO2DsU,970 xmantissa/test/historic/test_anonsite1to2.py,sha256=MWfkTjKcrC2emaPtcg3LgiHJ4CVW2l_Ms83BwCiu8ig,1026 xmantissa/test/historic/test_defaultPreferenceCollection1to2.py,sha256=RvIG_j2e5Wz8z0aevRdc_MeGoM3JZi_gR_gEI7qa5vQ,311 xmantissa/test/historic/test_developerapplication1to2.py,sha256=V56v32zyQ5mYUzYyQgUgTikXebpuLIa7duuF2UKcPWU,449 xmantissa/test/historic/test_emailAddress1to2.py,sha256=naZrkRSrpVP_pUREv9CyM0_vmF1ng6Z2XSK_n35Mlj8,373 xmantissa/test/historic/test_freeTicketSignup3to4.py,sha256=MhiChyrjs_nWgH6B8RrtAW11qCEVZmG6kFr12kFGf8w,403 xmantissa/test/historic/test_freeTicketSignup4to5.py,sha256=x9luPjpRZcaHkndF1UAt5hL1bOyLIHsdZs6GZ91viUg,231 xmantissa/test/historic/test_freeTicketSignup5to6.py,sha256=0BSpiH6A30JZhhuvjguEDrl7zGqKKa3ulP93qaVlCnc,279 xmantissa/test/historic/test_frontpage1to2.py,sha256=gGB2NH4kg-jwOkHjI_6sVzEC9YOn_KiAJqLukqVEg_Y,590 xmantissa/test/historic/test_messagequeue1to2.py,sha256=a7-c5wAUXxbE0VxmTcFI-LzKc9xYz63x6wJehGOhr2Q,949 xmantissa/test/historic/test_mugshot1to2.py,sha256=MgX4ZBL7n_4G7EU2xuxFWKxQR31BQEl7vkGNDeZ3wDo,617 xmantissa/test/historic/test_mugshot2to3.py,sha256=1tY0iX7B21AoIECKokeH1vfSfmFHleAzYu8ggoaiLJg,1818 xmantissa/test/historic/test_organizer2to3.py,sha256=30-IxMsTb6gojKOxuVvxY6e8OVilEDcKG5MHYGtV7K0,937 xmantissa/test/historic/test_passwordReset1to2.py,sha256=EsZ2d_eMX2EsnyH46N3Jgvj9VYkD10rU1OwgpzBfrfc,228 xmantissa/test/historic/test_people1to2.py,sha256=2j3qaU9Byt2dcm2q8Fzm81mN41Q_Z-qiSHaJi0RADTg,322 xmantissa/test/historic/test_person1to2.py,sha256=428g7XVDhae7BPmLVVRjo4aeF8M9ac7Gpjt0K3Gt5YU,868 xmantissa/test/historic/test_person2to3.py,sha256=SfpeXglQpS_z0_MmLDQpshiAIB3pQeskqe7N3Kw08-U,919 xmantissa/test/historic/test_phoneNumber1to2.py,sha256=SrYpH35XAw9t8tXcHF8Xhhw_XqzGTWsNYCkSdpp_vFY,372 xmantissa/test/historic/test_port1to2.py,sha256=bm1RGwSvZehJEzOIzL2e2N7fw7D1mp0obriRGp3hAAQ,1157 xmantissa/test/historic/test_privateApplication2to3.py,sha256=niZobQ2SFMnHVfsvI87eyKU8vBbttutPsZW6pPpSq2Q,2050 xmantissa/test/historic/test_privateApplication3to4.py,sha256=lr58grjh8D6IZ9bto54gXV0wpXYnkGvWVKyFHin87tQ,3405 xmantissa/test/historic/test_privateApplication4to5.py,sha256=1iXUhTCnnNAzvIkMNNIS_mSqd5nhwrB97eSrfH1VCOI,407 xmantissa/test/historic/test_pyLuceneIndexer3to4.py,sha256=u94n03QXt2NbpGK_jCxtZv4q9EvtM0OPHAsWn2jbf4Y,451 xmantissa/test/historic/test_pyLuceneIndexer4to5.py,sha256=m999gX3yUWAmIG07kpr95bsGsNbc7A15T_oHiuwr_iY,831 xmantissa/test/historic/test_realname1to2.py,sha256=anub1F85RajY06-J-d2N_q_zNfin-uKMxd5gn-Pjpxw,421 xmantissa/test/historic/test_remoteIndexer1to2.py,sha256=UWQ23tvpzefH8XAo1ImMEzv9FVmp6vCaX5dQXF6GKTI,1043 xmantissa/test/historic/test_remoteIndexer2to3.py,sha256=PjvKGzVRfUpjRdIaSDREUB-C2q4SSqd5kW-J7YLKuNE,1413 xmantissa/test/historic/test_searchresult1to2.py,sha256=5CdMUGByubEa1Hh1VE0XBYQYEjzINRpbliyJcraqfiI,409 xmantissa/test/historic/test_settings1to2.py,sha256=QB9Me9N6A6hf865UVzEFFnP3xkhlN_qc0TvMQ5inibk,216 xmantissa/test/historic/test_statBucket1to2.py,sha256=hcWsveVqAAoP8PZwJwpYIQYgUb0pOlbhOdQKsXM_W6s,278 xmantissa/test/historic/test_ticket1to2.py,sha256=-1Tp9GpwsvbYeL1Fm5aavQvP9EBc4aEuTYqjnLEklUA,591 xmantissa/test/historic/test_userInfoSignup1to2.py,sha256=qoGkTpn4rZzL7HuIUETpWNn7puP1Dz6vGbXlhr3l5mw,275 xmantissa/test/historic/test_userinfo1to2.py,sha256=OAYf5GYRdVwYfO06ImSJ3enZlgEpmvGYWcFqpewVGps,708 xmantissa/test/historic/test_website3to4.py,sha256=bHtO-qBcXNJHpXtHigIDSZaEr66upp9oxBxVoYpqaQg,563 xmantissa/test/historic/test_website4to5.py,sha256=HsMNWIegGaSFfhbk9qCENX7BRClrRIqawKz87mytuQE,3855 xmantissa/test/historic/test_website5to6.py,sha256=EfDdvYGXf625Wnoocg09KPuDG6wK40T1v8l_yuaP-kI,459 xmantissa/test/historic/addPerson1to2.axiom.tbz2,sha256=zJoxluQCDOVjPPBWbr9rJCcyBkWF8z9bvdSVi38I8os,890 xmantissa/test/historic/adminstatsapplication1to2.axiom.tbz2,sha256=LYWBQNqYhwPD7NcVbH_mEPkLBwN5j4pWzyxI1Yb6g5s,936 xmantissa/test/historic/ampConfiguration1to2.axiom.tbz2,sha256=MM7MCf5MU4jo5wijaaS-UrgdKtn8yJc7xn7xxtLK3mo,2483 xmantissa/test/historic/anonsite1to2.axiom.tbz2,sha256=vmuQQ5RlUQq5jMsrK1qg_LuLYVYctm5kw1hUHE2SG3s,2503 xmantissa/test/historic/defaultPreferenceCollection1to2.axiom.tbz2,sha256=U2LXLAQY31UbfH2smTKLE8AahdHj5WMAnGiEQPRxucU,943 xmantissa/test/historic/developerapplication1to2.axiom.tbz2,sha256=LlxWcwkcCB0tEzWKAdYb_EccXCYhiDh61YcgnATJrJc,799 xmantissa/test/historic/emailAddress1to2.axiom.tbz2,sha256=_2lPs4YSbO_-OOTWGz4lBXjS_rj_K9sQO_4_APR740E,1234 xmantissa/test/historic/freeTicketSignup3to4.axiom.tbz2,sha256=ga1nnrXVQYmUFDH9CKDxwM-oHHbqqNFC-flgEYXerg8,1496 xmantissa/test/historic/freeTicketSignup4to5.axiom.tbz2,sha256=L1fFBZr1z3J29K3pXAQ9Nt4EGUEFJCQAUBA09M2NbYo,1526 xmantissa/test/historic/freeTicketSignup5to6.axiom.tbz2,sha256=yzuVdkvBDBsBg2CuerFKCL6BbFRZnSHhz766l_haWAM,1515 xmantissa/test/historic/frontpage1to2.axiom.tbz2,sha256=ig1LW1K07Ui0ZoH24Je8xNRaiYtimP7Fxs2nwG5znVQ,1047 xmantissa/test/historic/messagequeue1to2.axiom.tbz2,sha256=X9z3ci1BiwPRwkK4zmmxnmVf8_Bz1wdW0y3hlsOpsBY,1841 xmantissa/test/historic/mugshot1to2.axiom.tbz2,sha256=M36G_EXpn6ts-nDj6m6ObjaxQ5-DcpyCVB-Vwo2fnoY,41439 xmantissa/test/historic/mugshot2to3.axiom.tbz2,sha256=W8bV0R_Jg9tbwB5xTG223wm04aKXVid3y-NCfd7gqK4,33945 xmantissa/test/historic/organizer2to3.axiom.tbz2,sha256=5wl3zxOm4MJXgAmt6wqP1u6VuBr2f-PmqtB7FTTS1aQ,3725 xmantissa/test/historic/passwordReset1to2.axiom.tbz2,sha256=sVCze31BcoOi31ltsbUJlqZ61iPhjiSBI0Ui4u6bEFw,989 xmantissa/test/historic/people1to2.axiom.tbz2,sha256=DSAcQ14w4oO7xRIlGJtZVTeiWj1FDWCi0DuQHfXwsk0,841 xmantissa/test/historic/person1to2.axiom.tbz2,sha256=5kSxhrHUZGArPZHcT13yUk3NzILCN8iHCpsUneF9PZk,1124 xmantissa/test/historic/person2to3.axiom.tbz2,sha256=hhfD16cpW0JVOSYD_lD0jdmB2M_YcUs4mfFNomP0LnA,1554 xmantissa/test/historic/phoneNumber1to2.axiom.tbz2,sha256=0g24M_iYwGiUaRV_immacMt9QTcqBbVOZ_HMOZoPTc8,1248 xmantissa/test/historic/port1to2.axiom.tbz2,sha256=m6hiqvIkEMm60XD4_-sQq1muMaBPEhXkqZUK2M8AJEA,3759 xmantissa/test/historic/privateApplication2to3.axiom.tbz2,sha256=QcPNvj61a3899dUZqLU4H8XCN8egILtjXps6Ha7zpk0,1082 xmantissa/test/historic/privateApplication3to4.axiom.tbz2,sha256=HTkHrKBbHvZIgKKxyFfHZcaZ-Xb1k1Wn_HcnyKMqdI0,5333 xmantissa/test/historic/privateApplication4to5.axiom.tbz2,sha256=0P3cIF_HYqVPorm3HaLx4uEEjemwtKhXSF4pfZeSBaI,5920 xmantissa/test/historic/pyLuceneIndexer3to4.axiom.tbz2,sha256=87j4qctrvnGnDhVG43qehIdtDlgww5h3bhujQ1KqJH4,991 xmantissa/test/historic/pyLuceneIndexer4to5.axiom.tbz2,sha256=icAeuR8HPCvCuF4MOpHpfdwB8umi4H0CCXiHOHy1Csg,1001 xmantissa/test/historic/realname1to2.axiom.tbz2,sha256=ntDatM63FwuKmSukhK3_bwTKWicXxRUwdCzQCAUXJBM,1329 xmantissa/test/historic/remoteIndexer1to2.axiom.tbz2,sha256=Q5649Ech57_9zaClP9iRCywpXJ8ueT8LiIkge93u5Zk,2535 xmantissa/test/historic/remoteIndexer2to3.axiom.tbz2,sha256=ojxoAcVk_e1d7RwlHS3B49ZPG4a4pWL0hT0LTcG0iDU,1840 xmantissa/test/historic/searchresult1to2.axiom.tbz2,sha256=wYBL3gB2zfHxAnUIb9yOGjfLf2pjMKRTCu6vmLF_LQ4,1055 xmantissa/test/historic/settings1to2.axiom.tbz2,sha256=aAQMSVkR2XHRYe6Fsf7HFoR6ZCXg-0SGGbi4PsF-V1g,837 xmantissa/test/historic/statBucket1to2.axiom.tbz2,sha256=3wPt0oTTUqR0ujDMowo27luhcCqECn5zGgtQiOwj5So,1254 xmantissa/test/historic/ticket1to2.axiom.tbz2,sha256=6MOC1W03K9JJbd3FRMf2lTvT8U0N0eiRfAXvjWXiNiY,2678 xmantissa/test/historic/userInfoSignup1to2.axiom.tbz2,sha256=CIplT3dHCpuJsrkm3ETZeSbzMSFDUA0GNEj_Bh2PqFA,1534 xmantissa/test/historic/userinfo1to2.axiom.tbz2,sha256=jP9RlGNeJYfDKuYnqGx2lTS4IAaBkDvVfw0vggI3h60,901 xmantissa/test/historic/website3to4.axiom.tbz2,sha256=2G9nzOStA9HbdxOIMR72gwCg9pC7_3EfoY28n07-OT4,5636 xmantissa/test/historic/website4to5.axiom.tbz2,sha256=pe9mKRiRnibyG3_kjHDl-7wC1vxqF78EtYdzriB7ykw,6278 xmantissa/test/historic/website5to6.axiom.tbz2,sha256=cKjxo0jyK7x8VeyZ25eV6Zfx3wlW0vkbGMnHp21Q9Ik,6877 xmantissa/test/integration/__init__.py,sha256=GrGcbgzq1zL7KUbjbtlOeA-iRPxijMTh_1DXL6jXeEU,103 xmantissa/test/integration/test_web.py,sha256=uCxa37XM0gMFGnmfHSqhFCjdqazw-M-fi3qg-qzEXfY,25690 xmantissa/test/resources/square.png,sha256=Y4mPVZfsqJc93rWxnD4_PMR7-xxdEqnr48_VtAjLxWY,31702 xmantissa/benchmarks/benchmark_fulltext.py,sha256=hT8kPc7lVR0JVeD0xrjOC8DOe1e1BTNrkgINR9vflVQ,1016 xmantissa/benchmarks/benchmark_stats.py,sha256=0bOTQ37Q5xRlZ6NQ3-3ewKuFirPKM93kMzA6ZQGLW3Q,9937 xmantissa/benchmarks/benchmark_stats2.py,sha256=YVe0zYIPeIxUL-k_dg_o1Jil1CJgiIr754wYY_t92r0,10819 xmantissa/js/Fadomatic.js,sha256=KCWY11kdnSeQU6y29owYc2Yyz2x-jZCFah_LGLW5A0M,4395 xmantissa/js/Mantissa/Admin.js,sha256=waCtjHZFAcvHtSjpY1WQzlZOErttzlkyf64SbB7O6Ug,4042 xmantissa/js/Mantissa/Authentication.js,sha256=M2GgfNmJ_nuTayaI5VvfIqcVoJS4jr2WhB62PkR5iMU,866 xmantissa/js/Mantissa/AutoComplete.js,sha256=q9lFSCvnioi0-v6HLRXRLjE_tv9BfpfJLiOkKS9RpII,12430 xmantissa/js/Mantissa/DOMReplace.js,sha256=etzpCWhKeu-oPr7Y5AbFalmsA5SZC5zzVgPCzMWfjU4,6280 xmantissa/js/Mantissa/LiveForm.js,sha256=wtQfUeFlSBjNN2HulB4BqhMLk01IhIZmPIyCIZxxGvI,23464 xmantissa/js/Mantissa/Offering.js,sha256=dd4Y2PVbacCfJO6KUV6pAb84aoLDxMPFS1q8fn8Yxyk,1067 xmantissa/js/Mantissa/People.js,sha256=G1TTcqtvuoNbERUQuc8j5U5HtYAbldDOe3yp-nceyeE,28543 xmantissa/js/Mantissa/Preferences.js,sha256=qW62JXC2Cpldbfk91BhI9yZRkyuam1ru5GkM71ay21A,585 xmantissa/js/Mantissa/ScrollTable.js,sha256=NGb1SU8S5AocKqEVyq0Ck-YyHbvpcAWUVJr9DQJWLLs,130289 xmantissa/js/Mantissa/StatGraph.js,sha256=JT-aFS1-1hrKzENFsubb_UPs6jQTGaARa3MefmH9aR0,11596 xmantissa/js/Mantissa/TDB.js,sha256=fARCnqYrfl634CHqxQ5irV5mdsMuTMIN4YFExkKzpwc,5487 xmantissa/js/Mantissa/Validate.js,sha256=9hSFCF3ZBO3Xc1bZmS4cytMiZgOwOiHc4ieDutj1CZA,9428 xmantissa/js/Mantissa/__init__.js,sha256=O5KyReSfz6mQWxKTGdPUcm_8IoLlZonX0PhN12x0B4M,8014 xmantissa/js/Mantissa/Test/Dummy.js,sha256=Hw32NAzLFts7wX3OMsZXj5aw6vgMFwLDcqD1FOEqB90,207 xmantissa/js/Mantissa/Test/TestAutoComplete.js,sha256=_OOmuvbeNbPSn0fTf1BKm-pgvv5ulSKl4dpeyqthS6Q,9844 xmantissa/js/Mantissa/Test/TestDOMReplace.js,sha256=33JOGndPYmOMeKk9M5s2wgiSobrTOs_-l_I9g2ngsAc,8180 xmantissa/js/Mantissa/Test/TestLiveForm.js,sha256=3i-YibNPhZKZez4C52NUAp8-rQWJglb00siQaKp-KO8,19539 xmantissa/js/Mantissa/Test/TestOffering.js,sha256=Zt-FrxFST1J5IAE-URJej1aa6igTRJxCL_qWDigAtc8,4597 xmantissa/js/Mantissa/Test/TestPeople.js,sha256=yxXDPMAcu7xiOnuVB7b-5Y7PkR8u4PE8lu0UCf0uceU,56638 xmantissa/js/Mantissa/Test/TestPlaceholder.js,sha256=z2qj7AVUBgF3HwkatHq71P8XbZLCYAkGXp-8vkKu5tk,11891 xmantissa/js/Mantissa/Test/TestRegionLive.js,sha256=P26RAYZcgBDHKZD39iOItLt7WEwuXDY4hGGCCgygpIk,13984 xmantissa/js/Mantissa/Test/TestRegionModel.js,sha256=lBzRI1Vcsjh3e9ReQjFgb1U1iPA4Im-igkt4Q565tE8,122591 xmantissa/js/Mantissa/Test/TestScrollModel.js,sha256=-9_Cb-cgy3TwFTx8p10RL_LPTiA2X9eLzeVrCZVp96Y,10850 xmantissa/js/Mantissa/Test/TestValidate.js,sha256=B0OYvwFz_WtsRNtncE9HQz1KhwQEsr2lWvHXQDwgvR8,3918 xmantissa/js/Mantissa/Test/__init__.js,sha256=8RzIpi9cqIldjoTW3gU8Mz7r_mLlRfayqH_pR4gr4uM,52150 xmantissa/js/MochiKit/Async.js,sha256=xMxGSUFNnED7r7dn111ny2-5S6wGl02WuEzsUMQo0GY,19712 xmantissa/js/MochiKit/Base.js,sha256=Cmt-BfoxAvwMPy1Dakf8OKxdCCaEAXAb6AZMN4t6Ay0,40420 xmantissa/js/MochiKit/Color.js,sha256=jG5n1ADeXnlMTwApdr7oKFfjrrW87bVS6g7vu30Vc8s,22379 xmantissa/js/MochiKit/DOM.js,sha256=WFPpOPApxfxjV1oo9LQRZ9xqX-fDaG3pwba63EdWpwA,28879 xmantissa/js/MochiKit/DateTime.js,sha256=D0BlmSOBFo0JKIuLIyL-6igisv5VAFmiA0wUneLxRyA,7050 xmantissa/js/MochiKit/Format.js,sha256=E66tbmz2M2jiPt2TpFsrE0MkwFqcrxz8l8z9Yd7saqw,8948 xmantissa/js/MochiKit/Iter.js,sha256=0BHlSsAc3TBnQH6Sn56xSi1F9Lt-AgGJb9W7QW1Ytbk,27353 xmantissa/js/MochiKit/Logging.js,sha256=EJQ54HN_v075DjTEQvIWyTPOWVx0O849-bu6bh6auPM,10128 xmantissa/js/MochiKit/LoggingPane.js,sha256=1_fMT4ki6PMWZ_V7Ehy03uTWOSi3YsAHOUGJuVxM2oI,10755 xmantissa/js/MochiKit/MochiKit.js,sha256=FflJn3VcxOU6EnMUeKoKZCg0rccsTV8hviwSa2f-C9E,4154 xmantissa/js/MochiKit/MockDOM.js,sha256=D2vK5tCcfudWVtZhLEGKyddmw2IR0OWy8_FuXT8BJC4,1463 xmantissa/js/MochiKit/Test.js,sha256=kEePUof8ER6tV6HMd-pJWYcJ2LJGHZ9YjqrjYnLej8E,4528 xmantissa/js/MochiKit/Visual.js,sha256=XX1cSnf6_PjmV8eUXfYFFcEYomqy292xRezr3qA9UGM,11671 xmantissa/js/MochiKit/__package__.js,sha256=4lJFgSkVU7QBtFJ1OjX3puQidB43vXXmW-H0sXp5Z3g,395 xmantissa/js/PlotKit/Base.js,sha256=NT4cpDsffQ4PjSWsxCDLxZb4Yz1BDXwLl9AQTIrAloE,9749 xmantissa/js/PlotKit/Canvas.js,sha256=f_rRziYmob4NSNZTsnWc_PuGidTHDJ33XhuxneP1PgQ,24547 xmantissa/js/PlotKit/Layout.js,sha256=Itbg97v8UP02phYhxZo0kXliNvU9ROaotRCWG5lPJnY,19172 xmantissa/js/PlotKit/SVGRenderer.js,sha256=kKAhmZIOcObV9wIMnDLe3noUc6hmayiLyzoNxheNVBQ,25202 xmantissa/js/PlotKit/SweetCanvas.js,sha256=ETpE1lz_Xg2VRGbIpWjpc1OEQ6tc9q4MMaRR1Oovcrw,9421 xmantissa/js/PlotKit/SweetSVG.js,sha256=1F1WlioKgXNRI0JmCOHZvCp_Nh6kC2123B5DHIdozLQ,7168 xmantissa/static/mantissa.css,sha256=JFqIcSwUFJwW_ILpiGkqIyxX7hOH7Bz0rv3TNDPsffg,27264 xmantissa/static/images/EmailAddress-icon.png,sha256=fq5EM5BpGau9CP832AuK0IqbtgSAIohxtbxQnOAy8oE,269 xmantissa/static/images/Notes-icon.png,sha256=cVZD38oTNuVlae5gbFUdhqdspDbPezmPIuvEhGNC7FQ,403 xmantissa/static/images/PhoneNumber-icon.png,sha256=2CTh6m8iZyrCKO1y4u5zcR4WhHh644jy_Hqk8zxpOdU,368 xmantissa/static/images/PostalAddress-icon.png,sha256=fq5EM5BpGau9CP832AuK0IqbtgSAIohxtbxQnOAy8oE,269 xmantissa/static/images/circle-throbber.gif,sha256=LNpAxrA4Y0sY1zoydZrRW2916V36ZLDuxL3QVUtDjrc,734 xmantissa/static/images/delete.png,sha256=FDwLTP_qH0GxA85nT9yx8yPan0SzvAGZDnK0UIb2_OU,419 xmantissa/static/images/divmod-table-body-left-edge.png,sha256=Dy_71_PVujvOIa4FbCxienjyUFB8-4AlJtZJyyEKog8,127 xmantissa/static/images/divmod-table-body-right-edge.png,sha256=1rvHgaPHZhv_S-cznpwcYNFuNLVIMPq5xwP8n0G55Ms,124 xmantissa/static/images/divmod-table-footer-bottom-edge.png,sha256=i9p_x7JXVbVzd_jzScuGp8hJmQqGKx3edfcomO8uhyY,138 xmantissa/static/images/divmod-table-footer-left-corner.png,sha256=3YCw5GQ4tsphywZXERjfpJIURzlf2JRcDMg_wiDZnI0,215 xmantissa/static/images/divmod-table-footer-right-corner.png,sha256=y2-ZgsP5lkxxlQBgk7a2LLqlMfzvptQgO7hmhcb29xw,227 xmantissa/static/images/divmod-table-header-left-edge.png,sha256=zLfsWKYW0wE5AAPrEVfhwxPMPP5Yx9GnA0EbU0aJkmw,197 xmantissa/static/images/divmod-table-header-right-block.png,sha256=wHaVLpMilhIEeagK5pSFw_U_eKcnJyE9PNSGUmq2xds,2849 xmantissa/static/images/divmod-table-header-tileable-stretcher.png,sha256=k3UBdiFKjCy2zrUrfk5VamoTdUymZVIHt0Rn0tKRawg,145 xmantissa/static/images/divorg-divmod-table-bg.png,sha256=-HGS6KO2t7IWGiy4PD0UJJlLaiJ7gaXP6Z94SeOfhJk,5055 xmantissa/static/images/divorg-large.png,sha256=qXw1_UGTAk4WykBx9Ds8otLWu3MZEX5feXHk53PXdqs,32287 xmantissa/static/images/divorg-menu.png,sha256=F7cfUtdIdTH3-HDiNFZ02AsnNPLOEPQIRXNC5T3Pw5E,1546 xmantissa/static/images/dropdown.png,sha256=VAxIuZ1fH_t-ij4ANQye2AR9V4Ilo4BWrRlD4Vj9hfE,2870 xmantissa/static/images/ellipsis.png,sha256=n-cv5FKVO_pyYSeEthzIKCMlscJAuL3M9Q7idVljR7g,362 xmantissa/static/images/error-small.png,sha256=VnMCGBcVsv3D7eWbfmyoHMEzQrME1eP8hSeKN0Wve-4,607 xmantissa/static/images/first.png,sha256=QwR6c1_2L7ICnKgvczdS137aPXxW-a0kDBiI1IEBQP8,656 xmantissa/static/images/first_disabled.png,sha256=1Li4PG5qak8_e3MEttHCVbbTe25h5wqlVYIgQlyoWW0,609 xmantissa/static/images/header_bg.png,sha256=uLQQ9TX2E228wDag4G8u47hCocvxa0kHfv4jvNJaigw,4701 xmantissa/static/images/header_bg_hover.png,sha256=A67ovtf_MOMGJeVqRMq3_eSzpiO3odzA2LVUXgWC_To,5057 xmantissa/static/images/last.png,sha256=u6aGeU8ZDVQ0iMEiYI4y_8k5IQ-WFAx2psbC-FiRElQ,639 xmantissa/static/images/last_disabled.png,sha256=bMzgDpF7YkDU4aD6aS8487lgmjl_TQsfMe2kuNYVdPo,593 xmantissa/static/images/li_current.gif,sha256=RpR64Bmo6bKXSV78tn91gdHDT8DEzu5SqaQ3j6Cej2I,143 xmantissa/static/images/li_over.gif,sha256=PAA3BMSd-Cdto78ZFpjrGLc1KR1XFHJ0z_rtnmgy--0,143 xmantissa/static/images/login-table-header-left-block.png,sha256=Ki2ARQQ5BM4jopm-2p64-CKAyFWOjyIy4MiZ0QXxUfI,815 xmantissa/static/images/me-icon.png,sha256=ANFgMst-OsU6VgghGe1LpYsETwg4IVeo6gfnAq2Tuzw,296 xmantissa/static/images/menu-separator.png,sha256=FruYDKCPb-0tYix43Q8mF4zoX202sA8VSMNAZq9gh9Q,119 xmantissa/static/images/menubar-bg.png,sha256=L3s1wmcZZ_0rwpgeRuuRZ90H190nvdMvSKsQB4ElUgY,136 xmantissa/static/images/mugshot-placeholder-smaller.png,sha256=kkwp5pZzoYW8ytVF4I0pagoetGcoJ6ck_8qTL2-Aejg,2138 xmantissa/static/images/mugshot-placeholder.png,sha256=hzbn_CpYMg6h73bO4PqddY5urd8YpwGAD3sXNTkDGJI,4514 xmantissa/static/images/nav_bg.gif,sha256=E1pQu7331p4U1B0D5H3m8yn4zbEpW9NFynqYHIK9UXE,167 xmantissa/static/images/neutral-small.png,sha256=cVZD38oTNuVlae5gbFUdhqdspDbPezmPIuvEhGNC7FQ,403 xmantissa/static/images/next.png,sha256=tkteZQd6N98hSgq-xCBoOOFDR7DvMXBT8GYPApwJDQc,625 xmantissa/static/images/next_disabled.png,sha256=x8jZ7xdlXrWm64EL3t5BFdjFxMvPxFygzdJpJUKMnJ0,579 xmantissa/static/images/ok-small.png,sha256=SzsZtWeUDd2bAgVLpSk2hC_ehynN5qYCB5g2zNAIO_U,574 xmantissa/static/images/password-reset-table-header-left-block.png,sha256=GenifFRx7oRVPr2zkmUKsrGn8NlVGBvb9C97gZPOAJ8,2060 xmantissa/static/images/people-filter-column-bg.png,sha256=enqs5oKuOU4ljGkAQsVznAJ-pjQh25UHGds_i4MlRiU,646 xmantissa/static/images/people-table-arrow.png,sha256=Qlk6KWGNBnsS3pC80roY4cvhOHrQV31P93nIFfhJQy8,295 xmantissa/static/images/people-table-column-header-bg.png,sha256=qluSeRbmaTkpsGzPzJlBCq_A9gIZ0H7FMTN4JB0nHT4,939 xmantissa/static/images/person-fragment-bl-corner.png,sha256=VTNtvOrZjXGS2rNTv6XbPeViG5e5hy1qO-uv9RkM4NA,138 xmantissa/static/images/person-fragment-br-corner.png,sha256=-BptjhORvNqrvg8ON_UUaKW1FTDADv5vtJVKPBKeolk,143 xmantissa/static/images/person-fragment-tl-corner.png,sha256=OuL9SB8HBw-vP0PAI8utW8rFVc3Qhdk2Eu8UL2CwgrM,131 xmantissa/static/images/person-fragment-tr-corner.png,sha256=GU201nR9Lc95nXNGTzqRlkcgXcBuCaDp56fgneqp-_c,133 xmantissa/static/images/person-gem.png,sha256=rmhWnWsDhyrVxuuU0sZnpNrOVyHrxLQ5sOMKwDX-QkE,613 xmantissa/static/images/person-summary-bl-corner.png,sha256=8_O6ca105Ih7rShw8QItMut-5EaGJqecV1VItL4ns7E,159 xmantissa/static/images/person-summary-br-corner.png,sha256=ecFQ39XuwUl5NKhSgmcapHsdmO84eIqzYb2Nw5PYB2o,154 xmantissa/static/images/person-summary-decoration-tl.png,sha256=2mfP_uwyYQdVDrbqwi6zc4zmG1a8c5pmkgLu5Hian3w,160 xmantissa/static/images/person-summary-decoration-tr.png,sha256=5edZbSav_Itq-LanvhuMTgeaMSqu90axJK4IDDh7hW0,152 xmantissa/static/images/person-widget-br-corner.png,sha256=C741FYjZgnozFMPAzfXadw1MZsZ5egaKWzSC2B6IKT8,120 xmantissa/static/images/person-widget-tr-corner.png,sha256=yhMbYnYqsXMKgOP8Wk2ngRQy3WMuFUXpOMpNcNhYVB0,119 xmantissa/static/images/person.png,sha256=cNJzc15VerxQANkfxSD6O8IpcFl8foKvvfCbSDqwXUg,796 xmantissa/static/images/prev.png,sha256=xWJr1CCRSgFykrkZIELezRTdFzIGaHsPuRzp183OnK0,628 xmantissa/static/images/prev_disabled.png,sha256=JuIwOIGXiqqSTcQ4jCS6i5FthmC4Zwxy-dp-o4qcVkQ,581 xmantissa/static/images/scrolltable-header-bg.png,sha256=wMNTNgmN1KgY6-8q9mbOFcPOVaBnZ_IzAkQzFeC-JKY,125 xmantissa/static/images/search-button-selected.png,sha256=Jd9rTdlkn7TkqtbtfkcvdAFSz2y7jW_qgj9fXy3iQqw,807 xmantissa/static/images/search-button-small.png,sha256=7IDyVijzv5k1FSacjC8IK4mZYkUi6I5p6kcTYYeyr34,803 xmantissa/static/images/search-button-unselected.png,sha256=QHcTvbZeArHLNu36HUQZ-NLphsnpGGnK6ZS4ASurqO0,912 xmantissa/static/images/signup-table-header-left-block.png,sha256=DTb0vgCrIhDtBIwP203kISZngivNVPA3X5wvAUbQKX4,1169 xmantissa/static/images/spinner.gif,sha256=5aQKZlPaQy6_9DXDy_MUDYA-1qlCXYWIrMDfpx-JmGE,9427 xmantissa/static/images/submenu_selected.gif,sha256=NrnAZw5aor43hzYWVPcdFd7M7mvkzDWrVw-tqv8dU60,42 xmantissa/static/images/tab-bg.png,sha256=D86f5CkNiRBJ_19Zsoz6U9d0qEPAYy0PcT2U6UeXPDA,162 xmantissa/static/images/tab-left-edge.png,sha256=F2I3kQoCcv36rHfxBX8VUnjeUb2dnjFMBW3Afy1GW4M,240 xmantissa/static/images/tab-right-edge.png,sha256=Dlkc6iNh26ddXTH071LqT5KcHI02Rz1H5qbAadpp7OE,233 xmantissa/static/images/tab-tl-corner.png,sha256=L1IDxh0kVmlu2t8BdU82z72BQFn7atTJy8YWW3Y4ai8,139 xmantissa/static/images/tab-top-edge-bg.png,sha256=d1o2CFGf3mG2z_a4Lw41EOGLKUpVYQfRcp7e6IaBrgs,117 xmantissa/static/images/tab-tr-corner.png,sha256=7pXtvukegNrqMT2I5_xJD8yaBYYKjKD81fOPAwhY16A,148 xmantissa/static/images/vip-flag.png,sha256=daEMiKIQ0TJH3Xty86hNXekZ9o7ph7GnZKWVFoDpizI,300 xmantissa/static/js/initialize.js,sha256=lE0Ri6eLNFZ8yyXQPZpp3TPkd3FVzxlrzQqeMyaIQoM,288 xmantissa/static/js/search.js,sha256=WO4_AlC3WfvYX-j-ASwfgUXvs1C8gPJ9J18jJz0nhl8,592 xmantissa/static/js/shell.js,sha256=1rydW_HMiFblO-ExZmhfe2_2oJreg57_Xl5Ly_j8mcY,4760 xmantissa/static/js/signup.js,sha256=Vvrm0w9Cy_9heYJtBq5lLUVbvuwDCrOmLJF7wXZQ1AU,940 xmantissa/static/js/PlotKit/dummy.svg,sha256=XaiiIHUUU40KuLz1pqS1bELzFjkDRSPV8n5oQmZsbC8,342 xmantissa/static/js/PlotKit/iecanvas.htc,sha256=KutpqCiNWgS_JJVyMf38W9XzLC4Wff7_jx7yioH7eAM,13097 xmantissa/themes/base/add-person.html,sha256=kB9stJfvlBOaQ0KCKyY9wBn0lNu4A5ZLjYuE64Km6Gg,155 xmantissa/themes/base/admin-endowment.html,sha256=iIf1qKv-vmhM8dMDgQakkpwbrw2u-suaPqLZVL9MAUU,693 xmantissa/themes/base/admin-python-repl.html,sha256=oWe5k1Q_oNJ1fkJbyczhNK5x4BqoQF03nQy5ChKFe34,305 xmantissa/themes/base/admin-stats.html,sha256=CUx6giysJUeHdhsQcpFBZnS1r9bZq6_ySOvICWwMkos,672 xmantissa/themes/base/admin-user-interaction.html,sha256=yHfgMrAvcqjW2Cw8J7_3xm1O4xUmMz2k7emeDr-acmw,225 xmantissa/themes/base/athena-unsupported.html,sha256=rdc0ShiUt5m1A3Xhxbk4txiL5HbnmNjbj7TK2rm80BQ,69 xmantissa/themes/base/authentication-configuration.html,sha256=e-_ELjfzPnhabAZhQcVzCnJPmPNoi9VtMQHxEN6m2mE,1989 xmantissa/themes/base/edit-person.html,sha256=VKQDC_KTIKa2DGMNv1DRI_xWGou9IAC0KGw3nHu5lik,335 xmantissa/themes/base/element-no-fragment-name-specified.html,sha256=98dbB26TO9HuR6V0ncN3LAQGOeNvClwX3IFHvBUoZyA,376 xmantissa/themes/base/fragment-no-fragment-name-specified.html,sha256=19Cor-nNjvrsk7Jg6V6coGRghxDNeowlAkP8cmpBPLw,377 xmantissa/themes/base/front-page.html,sha256=86RvOdCNUJdxbiX01ctUMdpaRgwW8XRIxNFzUP0-NUA,560 xmantissa/themes/base/import-people.html,sha256=RtS9sCA9zr_OsGq368UStiqD_f9NZPvz0ZUOA58UiY4,191 xmantissa/themes/base/inequality-scroller.html,sha256=v94g18FnnICixwFV21IbQJNBwxCcoWd4riC2LXe3ZkI,118 xmantissa/themes/base/initialize.html,sha256=1gRkzq8WsWM0tTET7IMJYZh6khHE9rqZIq846NfFBdE,506 xmantissa/themes/base/installed-offering.html,sha256=vKCE87ZCKr6_ARHxOkg-EgEiALBxxxkY-0lNtfNMGJk,218 xmantissa/themes/base/liveform-compact.html,sha256=ZXjSeyLjNEUPRadntZ_c-8no8pHNZ2xhh8ZuTV92wqc,4374 xmantissa/themes/base/liveform.html,sha256=iJZQPNIbPhOsV0Guf-pyeLApFVU5Rl5_wwmKQN3wqyU,4813 xmantissa/themes/base/login.html,sha256=POJd_aOFQIdxpfcexLP2tBHIlZtdGy-mRkp-jiuX-XM,3068 xmantissa/themes/base/mugshot-upload-form.html,sha256=IlkTHXAjjcGDVZEWyYboBkAoTKkIip8CeX-BjzE6FVQ,830 xmantissa/themes/base/offering-configuration.html,sha256=NfXR2zzT8-LbXTi5krSLOj4hMr0z1s5VcnhTbsZFTGM,659 xmantissa/themes/base/people-organizer.html,sha256=4PAc3vEm1FmvM9f_yCTxcsqQgsiliWYUkCWlJD-xTdg,2912 xmantissa/themes/base/person-contact-read-only-phone-number-view.html,sha256=mZ6hdAA4hTuFPSnba4yMNcJbKbO1aaK5bhG1LPSDNKg,294 xmantissa/themes/base/person-contact-read-only-view.html,sha256=HX35ZQIQ6h39YwfmlPyPFIC6UFNoVNYHU06N8ky67Qs,279 xmantissa/themes/base/person-detail.html,sha256=sDb8YOyLD2NzG86-VtCSlWqf4bPaEIyQL94rCxNwzVg,1476 xmantissa/themes/base/person-extracts.html,sha256=YhGpZct5mx7kY9RrIwvp_wQ3ojfB9LC7QNRpEc3IT5k,1602 xmantissa/themes/base/person-fragment.html,sha256=1UMcPGAOBC9-Dm3tiNBwjQ_mQoJFv0d4uROVSMdaPH0,1189 xmantissa/themes/base/person-plugins.html,sha256=HHfL-3gi5lrFxFq2SqzXAQucG7Qtu8sGkQ-hLnnKctw,324 xmantissa/themes/base/person-read-only-contact-info.html,sha256=qSLJpm1f3HBfAPa0Ru1o-UWHhBIMviS156yfKl2dwgM,359 xmantissa/themes/base/person-summary.html,sha256=3eRawev_LUzBdq3zDAvtU_cDa7ApmmlsAYgdlNlgky8,343 xmantissa/themes/base/port-configuration.html,sha256=-NijNIYxFHMYqj6Q706Q8gSwXyqr21xFczN3Rq4c9VI,334 xmantissa/themes/base/preference-editor.html,sha256=2a7lP4gEghal6S86XVXHIsW8wXqwm4SmEi8h1HlTXHM,161 xmantissa/themes/base/product-configuration.html,sha256=9D3Z64lZr9rllh6bh94M8QP8ylexQAZ7ggEzEoq5QbU,212 xmantissa/themes/base/repeated-liveform.html,sha256=KqrkeCy4dNeej92w5E6Wu2EpQ0652u3zZ0Sih_8C-M8,404 xmantissa/themes/base/reset-check-email.html,sha256=MqwCo3SPVIOWAvqDvHBl807BLKBDck_h2PSzHDtWX3g,132 xmantissa/themes/base/reset-done.html,sha256=XQJCHHXK_rw_IPGfaiswnCqnjl5T1b2Fl0boqudQDZE,126 xmantissa/themes/base/reset-step-two.html,sha256=K_EJiTFeyp25GCRCvMJEjRg2-Jm5nvwHn8rKLpTlV30,2735 xmantissa/themes/base/reset.html,sha256=GAdAcayo1gvOvgtnD2pxbnEbzhfdllskY2d-Mryu2yQ,2734 xmantissa/themes/base/scroller.html,sha256=CeK43yocLVv6M9O2xe7HcUhiuvdHzyUcgIQfMn4MWFk,520 xmantissa/themes/base/search.html,sha256=_z72ASGlZpVtfLwHRTMH75OeBzqwptZ8DZqIIYN1tAY,134 xmantissa/themes/base/settings.html,sha256=3aPp6spEDCCgNe74TS7uV45jUJSxoVATZxYd2ZbMFKo,146 xmantissa/themes/base/shell.html,sha256=g12k48RgUv76SkfM3lLCPLQi6DDi4nJDJAsgaT4pJ_g,7720 xmantissa/themes/base/signup-configuration.html,sha256=IjsyEWrkaPVsDiAajmW9172YAhzccGYRbuL4hrFezTA,466 xmantissa/themes/base/signup.html,sha256=us49y2asce33-_JXysc4FFgS7iCnzb8LrZCldgpYXXk,376 xmantissa/themes/base/single-endowment.html,sha256=4O98CxAy2XXgU8jP6N0qLoN3cQfOPv5QdstpzRItaY0,317 xmantissa/themes/base/suspend.html,sha256=1_7KmZAv38SPFW4eYYmf1TzLrhXg6zKFoM7TYcGhZ0s,107 xmantissa/themes/base/tdb.html,sha256=RJ02x4Tw8by_xEqKlC9ysq7qn-w97VxsPoe4N8gyLPg,3995 xmantissa/themes/base/traceback-viewer.html,sha256=8EHgkW9KeFIiq-myTLKgAVDrNhni_f8Ex3P1b98ovN8,119 xmantissa/themes/base/uninstalled-offering.html,sha256=pLncprOg5nymwbvt57IK6q1TVYbcfkISkfZBX67Lkhg,425 xmantissa/themes/base/user-detail.html,sha256=EsSnGQLZ60OPu4lY0wcBJPUdC_t67MxIbZRzHVEvJlg,190 xmantissa/themes/base/user-info-signup.html,sha256=Tm-Beu623t6FLmzdt5BNv2ekxb0djen05F3mzq3cZno,6775 axiom/plugins/mantissacmd.py,sha256=llxMxskI14doVuUu_3LzHkmY_Ar10NMMyl8N136eHAA,8713 axiom/plugins/offeringcmd.py,sha256=AdreGWoHgu4PWzFC5DnYtd4ImzwovtExWkb0YPRHXW8,2181 axiom/plugins/webcmd.py,sha256=YsSsvikmFc5gtbNsIaiw0DWrOeiB6vP7b5vNZPvY4_M,5974 nevow/plugins/mantissa_package.py,sha256=Q9jfKHucX2RMSxb5Mvn6Hn5e5kUgMqR82kbll7aAVis,153 Mantissa-0.8.2.dist-info/top_level.txt,sha256=mv2C_W4hrgDkqVXfGwPLXSyjT_eSina7DGKpzf6rbxM,22 Mantissa-0.8.2.dist-info/pbr.json,sha256=itsE6nv5Po-_e_Pdzabkp2ik-PX48o3ZOzcV1eCIUbc,47 Mantissa-0.8.2.dist-info/METADATA,sha256=vXeSX7uhj2aVllq5ICiFH2FU8GoDQcVUvnDauwiikfc,1049 Mantissa-0.8.2.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 Mantissa-0.8.2.dist-info/metadata.json,sha256=3pUG3NvJ9TZri-Gl3OGwZwxHJl-wtzAhs9UjKDCgBzY,1106 Mantissa-0.8.2.dist-info/WHEEL,sha256=54bVun1KfEBTJ68SHUmbxNPj80VxlQ0sHi4gZdGZXEY,92 Mantissa-0.8.2.dist-info/RECORD,, PK�����9Ftn[���������������������xmantissa/_recordattr.pyPK�����9FT �� �����������������xmantissa/_webidgen.pyPK�����9F֟N!��!���������������%"��xmantissa/_webutil.pyPK�����9F?\$��$���������������TD��xmantissa/ampserver.pyPK�����9F䊯0��0���������������:i��xmantissa/cachejs.pyPK�����9F9r�����������������x��xmantissa/endpoint.pyPK�����9F\�������������������xmantissa/error.pyPK�����9F1��1�����������������xmantissa/fragmentutils.pyPK�����9FxEb4f��4f���������������Y��xmantissa/fulltext.pyPK�����9FO�8��8�����������������xmantissa/interstore.pyPK�����9F:!��!���������������-�xmantissa/ixmantissa.pyPK�����9FB#]��]���������������:�xmantissa/liveform.pyPK�����9FR"������������������xmantissa/myaccount.pyPK�����9F$j*��j*����������������xmantissa/offering.pyPK�����9FKN�N���������������xmantissa/people.pyPK�����9F)c�����������������]�xmantissa/prefs.pyPK�����9Fa0�����������������{�xmantissa/product.pyPK�����9FAK��������������������xmantissa/publicresource.pyPK�����9FvO������������������xmantissa/publicweb.pyPK�����9FGh�����������������*�xmantissa/scrolltable.pyPK�����9FOfW������������������xmantissa/search.pyPK�����9FSv�����������������̽�xmantissa/settings.pyPK�����9FVMx��x����������������xmantissa/sharing.pyPK�����9F.y!��!���������������j8�xmantissa/signup.pyPK�����9FŊ}P��P����������������xmantissa/smtp.pyPK�����9FZKQ#��#���������������; �xmantissa/stats.pyPK�����9Fɻ �� ���������������T.�xmantissa/suspension.pyPK�����9F-��-���������������L8�xmantissa/tdb.pyPK�����9Fۭ!,��,���������������e�xmantissa/tdbview.pyPK�����9FS2؄+��+���������������G�xmantissa/terminal.pyPK�����9F%YI:��I:����������������xmantissa/web.pyPK�����9Fˌs��s���������������u�xmantissa/webadmin.pyPK�����9F{Sg��g���������������4l�xmantissa/webapp.pyPK�����9FiE}�����������������j�xmantissa/webgestalt.pyPK�����9F3>'��>'���������������y�xmantissa/webnav.pyPK�����9Fb+Z8+��+��������������� �xmantissa/websession.pyPK�����9F:v +��+���������������9 �xmantissa/websharing.pyPK�����9F*9w4��4���������������e �xmantissa/webtheme.pyPK�����dFXl70��0���������������ؚ �xmantissa/__init__.pyPK�����KdF5ֻzs��zs���������������; �xmantissa/website.pyPK�����FHXC��C��������������� �xmantissa/port.pyPK�����N5$GFr�������������������S �xmantissa/_version.pyPK�����9F8.��.���������������$T �xmantissa/reset.rfc2822PK�����9F/. �����������������U �xmantissa/signup.rfc2822PK�����9F@��@���������������V �xmantissa/statcollector.tacPK�����9FBA�������������������UX �xmantissa/plugins/__init__.pyPK�����9Fs�����������������EY �xmantissa/plugins/adminoff.pyPK�����9Ft�����������������9_ �xmantissa/plugins/baseoff.pyPK�����9F,d#��#�� �������������h �xmantissa/plugins/free_signup.pyPK�����9F';l��l���������������cl �xmantissa/plugins/offerings.pyPK�����rQF)>����������������� n �xmantissa/plugins/dropin.cachePK�����9F1` h)���)����������������t �xmantissa/test/__init__.pyPK�����9FN|w$��$���������������xt �xmantissa/test/fakes.pyPK�����9Fh���� �������������7 �xmantissa/test/livetest_forms.pyPK�����9F߄2��2��!�������������- �xmantissa/test/livetest_people.pyPK�����9F(������ ������������� �xmantissa/test/livetest_prefs.pyPK�����9F;O����"������������� �xmantissa/test/livetest_regions.pyPK�����9FK֡X �� ��&������������� �xmantissa/test/livetest_scrolltable.pyPK�����9FCz��z��!������������� �xmantissa/test/livetest_signup.pyPK�����9F� ���� ������������� �xmantissa/test/livetest_stats.pyPK�����9F!!̳:��:��������������� �xmantissa/test/peopleutil.pyPK�����9F�����������������+ �xmantissa/test/rendertools.pyPK�����9Fܪ �� ���������������3 �xmantissa/test/test_admin.pyPK�����9F"a9��9�� �������������%N �xmantissa/test/test_ampserver.pyPK�����9F[Sf" ��" ���������������< �xmantissa/test/test_cachejs.pyPK�����9F&>w��w��������������� �xmantissa/test/test_fulltext.pyPK�����9F����!������������� �xmantissa/test/test_interstore.pyPK�����9F3v��v��!������������� �xmantissa/test/test_javascript.pyPK�����9Fu5����������������� �xmantissa/test/test_liveform.pyPK�����9FNL��L��"������������� �xmantissa/test/test_mantissacmd.pyPK�����9F"ˌs1��1���������������R �xmantissa/test/test_offering.pyPK�����9F�c-��c-��%��������������xmantissa/test/test_password_reset.pyPK�����9F1ܫ �� ���������������H<�xmantissa/test/test_prefs.pyPK�����9FAνs�����������������F�xmantissa/test/test_product.pyPK�����9F{4]w��w���������������U�xmantissa/test/test_q2q.pyPK�����9F!����!�������������>W�xmantissa/test/test_recordattr.pyPK�����9F'k����"�������������-i�xmantissa/test/test_rendertools.pyPK�����9FD�����������������)n�xmantissa/test/test_search.pyPK�����9F 3}��3}����������������xmantissa/test/test_sharing.pyPK�����9FA�"��"���������������f��xmantissa/test/test_signup.pyPK�����9FQ �� ���������������#�xmantissa/test/test_siteroot.pyPK�����9F)�����������������*�xmantissa/test/test_smtp.pyPK�����9F528��28���������������9�xmantissa/test/test_tdb.pyPK�����9Fdp+@H��@H���������������^r�xmantissa/test/test_terminal.pyPK�����9F@S<��<���������������ۺ�xmantissa/test/test_theme.pyPK�����9FS$a=��a=����������������xmantissa/test/test_webapp.pyPK�����9F]�����������������B5�xmantissa/test/test_webcmd.pyPK�����9F},��},���������������G�xmantissa/test/test_webnav.pyPK�����9FA��A��!�������������Et�xmantissa/test/test_websession.pyPK�����9F}N��N��!�������������ŋ�xmantissa/test/test_websharing.pyPK�����9FNb|-��-����������������xmantissa/test/test_webshell.pyPK�����9F_ڨӽ��ӽ����������������xmantissa/test/test_website.pyPK�����9F+[^������������������xmantissa/test/validation.pyPK�����$eF݃ك��ك��"�������������F�xmantissa/test/test_scrolltable.pyPK�����,eF>���������������_O�xmantissa/test/test_people.pyPK�����,eFoA���� �������������7�xmantissa/test/test_publicweb.pyPK�����CmF Nh������������������xmantissa/test/test_stats.pyPK�����F %��%���������������t�xmantissa/test/test_port.pyPK�����;5$Gc��c��$�������������m�xmantissa/test/test_howtolistings.pyPK�����9FSX $�� $��#�������������w�xmantissa/test/test_autocomplete.jsPK�����9F������%�������������©�xmantissa/test/acceptance/__init__.pyPK�����9F@= �� ��%��������������xmantissa/test/acceptance/liveform.pyPK�����9Fb1_��_��#��������������xmantissa/test/acceptance/people.pyPK�����9Fk4��4��(��������������xmantissa/test/acceptance/scrolltable.pyPK�����9FR~Y2���2���#��������������xmantissa/test/historic/__init__.pyPK�����9F������-�������������q�xmantissa/test/historic/stub_addPerson1to2.pyPK�����9FNy������9��������������xmantissa/test/historic/stub_adminstatsapplication1to2.pyPK�����9FfP[��[��4��������������xmantissa/test/historic/stub_ampConfiguration1to2.pyPK�����9FǬI��I��,�������������n�xmantissa/test/historic/stub_anonsite1to2.pyPK�����9FxT ������?��������������xmantissa/test/historic/stub_defaultPreferenceCollection1to2.pyPK�����9Fds;������8�������������0�xmantissa/test/historic/stub_developerapplication1to2.pyPK�����9Fz��z��0�������������h�xmantissa/test/historic/stub_emailAddress1to2.pyPK�����9FJr��r��4�������������0�xmantissa/test/historic/stub_freeTicketSignup3to4.pyPK�����9Fɑ~4��4��4��������������xmantissa/test/historic/stub_freeTicketSignup4to5.pyPK�����9F0UŒ5��5��4�������������z�xmantissa/test/historic/stub_freeTicketSignup5to6.pyPK�����9F E����-��������������xmantissa/test/historic/stub_frontpage1to2.pyPK�����9FX����0��������������xmantissa/test/historic/stub_messagequeue1to2.pyPK�����9F+��+��+��������������xmantissa/test/historic/stub_mugshot1to2.pyPK�����9F96��6��+�������������5�xmantissa/test/historic/stub_mugshot2to3.pyPK�����9F ����-��������������xmantissa/test/historic/stub_organizer2to3.pyPK�����9F'������1��������������xmantissa/test/historic/stub_passwordReset1to2.pyPK�����9Fl������*�������������0�xmantissa/test/historic/stub_people1to2.pyPK�����9F*b��b��*�������������B�xmantissa/test/historic/stub_person1to2.pyPK�����9Fc��c��*��������������xmantissa/test/historic/stub_person2to3.pyPK�����9F&.m��m��/��������������xmantissa/test/historic/stub_phoneNumber1to2.pyPK�����9FT����(�������������Q�xmantissa/test/historic/stub_port1to2.pyPK�����9F(B������6��������������xmantissa/test/historic/stub_privateApplication2to3.pyPK�����9FnPh'��'��6��������������xmantissa/test/historic/stub_privateApplication3to4.pyPK�����9F3 �� ��6�������������V�xmantissa/test/historic/stub_privateApplication4to5.pyPK�����9Fr[����3��������������xmantissa/test/historic/stub_pyLuceneIndexer3to4.pyPK�����9Fq����3��������������xmantissa/test/historic/stub_pyLuceneIndexer4to5.pyPK�����9FE����,��������������xmantissa/test/historic/stub_realname1to2.pyPK�����9Fuצ����1��������������xmantissa/test/historic/stub_remoteIndexer1to2.pyPK�����9F=-z��z��1�������������} �xmantissa/test/historic/stub_remoteIndexer2to3.pyPK�����9F 6��6��0�������������F�xmantissa/test/historic/stub_searchresult1to2.pyPK�����9F.������,��������������xmantissa/test/historic/stub_settings1to2.pyPK�����9F8��8��.��������������xmantissa/test/historic/stub_statBucket1to2.pyPK�����9F:c��c��*�������������p�xmantissa/test/historic/stub_ticket1to2.pyPK�����9F)&'��'��2��������������xmantissa/test/historic/stub_userInfoSignup1to2.pyPK�����9F,]T����,��������������xmantissa/test/historic/stub_userinfo1to2.pyPK�����9F uP ��P ��+��������������xmantissa/test/historic/stub_website3to4.pyPK�����9FNٸ* ��* ��+�������������z(�xmantissa/test/historic/stub_website4to5.pyPK�����9F. �� ��+�������������4�xmantissa/test/historic/stub_website5to6.pyPK�����9F�A��A��-�������������A�xmantissa/test/historic/test_addPerson1to2.pyPK�����9Fي.����9�������������sC�xmantissa/test/historic/test_adminstatsapplication1to2.pyPK�����9F#Rt,����4�������������E�xmantissa/test/historic/test_ampConfiguration1to2.pyPK�����9Fׂ6����,�������������I�xmantissa/test/historic/test_anonsite1to2.pyPK�����9FN7��7��?�������������M�xmantissa/test/historic/test_defaultPreferenceCollection1to2.pyPK�����9F����8�������������O�xmantissa/test/historic/test_developerapplication1to2.pyPK�����9F6/u��u��0�������������Q�xmantissa/test/historic/test_emailAddress1to2.pyPK�����9FXxw`����4�������������dS�xmantissa/test/historic/test_freeTicketSignup3to4.pyPK�����9F߮`������4�������������IU�xmantissa/test/historic/test_freeTicketSignup4to5.pyPK�����9Fel5����4�������������V�xmantissa/test/historic/test_freeTicketSignup5to6.pyPK�����9Fo#l0N��N��-�������������W�xmantissa/test/historic/test_frontpage1to2.pyPK�����9Fc����0�������������Z�xmantissa/test/historic/test_messagequeue1to2.pyPK�����9F:i��i��+�������������^�xmantissa/test/historic/test_mugshot1to2.pyPK�����9FK����+�������������9a�xmantissa/test/historic/test_mugshot2to3.pyPK�����9F0v!����-�������������h�xmantissa/test/historic/test_organizer2to3.pyPK�����9F0]������1�������������l�xmantissa/test/historic/test_passwordReset1to2.pyPK�����9F=B��B��*�������������m�xmantissa/test/historic/test_people1to2.pyPK�����9Fd��d��*�������������Mo�xmantissa/test/historic/test_person1to2.pyPK�����9FS\*����*�������������r�xmantissa/test/historic/test_person2to3.pyPK�����9F <Kt��t��/�������������v�xmantissa/test/historic/test_phoneNumber1to2.pyPK�����9F����(�������������x�xmantissa/test/historic/test_port1to2.pyPK�����9F=z����6�������������d}�xmantissa/test/historic/test_privateApplication2to3.pyPK�����9FaM ��M ��6��������������xmantissa/test/historic/test_privateApplication3to4.pyPK�����9FnS����6�������������[�xmantissa/test/historic/test_privateApplication4to5.pyPK�����9FCn����3�������������F�xmantissa/test/historic/test_pyLuceneIndexer3to4.pyPK�����9Fyt"?��?��3�������������Z�xmantissa/test/historic/test_pyLuceneIndexer4to5.pyPK�����9Fo[ԥ����,��������������xmantissa/test/historic/test_realname1to2.pyPK�����9F2����1�������������ٜ�xmantissa/test/historic/test_remoteIndexer1to2.pyPK�����9F����1�������������;�xmantissa/test/historic/test_remoteIndexer2to3.pyPK�����9FYI����0��������������xmantissa/test/historic/test_searchresult1to2.pyPK�����9Fb������,��������������xmantissa/test/historic/test_settings1to2.pyPK�����9FmI����.��������������xmantissa/test/historic/test_statBucket1to2.pyPK�����9FzOĺO��O��*�������������z�xmantissa/test/historic/test_ticket1to2.pyPK�����9F&)����2��������������xmantissa/test/historic/test_userInfoSignup1to2.pyPK�����9FFww����,�������������t�xmantissa/test/historic/test_userinfo1to2.pyPK�����9FA3��3��+��������������xmantissa/test/historic/test_website3to4.pyPK�����9F;gB����+��������������xmantissa/test/historic/test_website4to5.pyPK�����9F+}����+�������������V�xmantissa/test/historic/test_website5to6.pyPK�����9FPez��z��0�������������j�xmantissa/test/historic/addPerson1to2.axiom.tbz2PK�����9FZʨ����<�������������2�xmantissa/test/historic/adminstatsapplication1to2.axiom.tbz2PK�����9FhԳ �� ��7�������������4�xmantissa/test/historic/ampConfiguration1to2.axiom.tbz2PK�����9FI �� ��/�������������<�xmantissa/test/historic/anonsite1to2.axiom.tbz2PK�����9F2ԯ����B�������������P�xmantissa/test/historic/defaultPreferenceCollection1to2.axiom.tbz2PK�����9F.����;�������������_�xmantissa/test/historic/developerapplication1to2.axiom.tbz2PK�����9F K����3��������������xmantissa/test/historic/emailAddress1to2.axiom.tbz2PK�����9FN����7��������������xmantissa/test/historic/freeTicketSignup3to4.axiom.tbz2PK�����9F����7�������������'�xmantissa/test/historic/freeTicketSignup4to5.axiom.tbz2PK�����9FB����7�������������r�xmantissa/test/historic/freeTicketSignup5to6.axiom.tbz2PK�����9FK0����0��������������xmantissa/test/historic/frontpage1to2.axiom.tbz2PK�����9F1��1��3��������������xmantissa/test/historic/messagequeue1to2.axiom.tbz2PK�����9FhO ߡ��ߡ��.������������� �xmantissa/test/historic/mugshot1to2.axiom.tbz2PK�����9FWi����.�������������į�xmantissa/test/historic/mugshot2to3.axiom.tbz2PK�����9F ͪ����0�������������4�xmantissa/test/historic/organizer2to3.axiom.tbz2PK�����9FMJ����4�������������C�xmantissa/test/historic/passwordReset1to2.axiom.tbz2PK�����9F#I��I��-�������������G�xmantissa/test/historic/people1to2.axiom.tbz2PK�����9FǨd��d��-�������������GK�xmantissa/test/historic/person1to2.axiom.tbz2PK�����9F @K����-�������������O�xmantissa/test/historic/person2to3.axiom.tbz2PK�����9F$f����2�������������SV�xmantissa/test/historic/phoneNumber1to2.axiom.tbz2PK�����9Fy$O����+�������������[�xmantissa/test/historic/port1to2.axiom.tbz2PK�����9FV:��:��9�������������{j�xmantissa/test/historic/privateApplication2to3.axiom.tbz2PK�����9FcA&?����9������������� o�xmantissa/test/historic/privateApplication3to4.axiom.tbz2PK�����9Fy �� ��9�������������8�xmantissa/test/historic/privateApplication4to5.axiom.tbz2PK�����9FvU����6��������������xmantissa/test/historic/pyLuceneIndexer3to4.axiom.tbz2PK�����9Foh-����6��������������xmantissa/test/historic/pyLuceneIndexer4to5.axiom.tbz2PK�����9F;$1��1��/��������������xmantissa/test/historic/realname1to2.axiom.tbz2PK�����9F4 �� ��4��������������xmantissa/test/historic/remoteIndexer1to2.axiom.tbz2PK�����9F!0��0��4�������������ֳ�xmantissa/test/historic/remoteIndexer2to3.axiom.tbz2PK�����9F����3�������������X�xmantissa/test/historic/searchresult1to2.axiom.tbz2PK�����9F&E��E��/�������������ȿ�xmantissa/test/historic/settings1to2.axiom.tbz2PK�����9FE����1�������������Z�xmantissa/test/historic/statBucket1to2.axiom.tbz2PK�����9F"FPv ��v ��-��������������xmantissa/test/historic/ticket1to2.axiom.tbz2PK�����9F����5�������������P�xmantissa/test/historic/userInfoSignup1to2.axiom.tbz2PK�����9FчD����/��������������xmantissa/test/historic/userinfo1to2.axiom.tbz2PK�����9F\t����.�������������s�xmantissa/test/historic/website3to4.axiom.tbz2PK�����9Fq;X����.��������������xmantissa/test/historic/website4to5.axiom.tbz2PK�����9F|wN����.������������� �xmantissa/test/historic/website5to6.axiom.tbz2PK�����9FJg���g���&�������������'�xmantissa/test/integration/__init__.pyPK�����9FTŕZd��Zd��&�������������i(�xmantissa/test/integration/test_web.pyPK�����9FOu{��{��#��������������xmantissa/test/resources/square.pngPK�����9F F����*������������� �xmantissa/benchmarks/benchmark_fulltext.pyPK�����9F4P&��&��'�������������^ �xmantissa/benchmarks/benchmark_stats.pyPK�����9F&C*��C*��(�������������t4�xmantissa/benchmarks/benchmark_stats2.pyPK�����9F 5+��+���������������^�xmantissa/js/Fadomatic.jsPK�����9Fn%�����������������_p�xmantissa/js/Mantissa/Admin.jsPK�����9F?b��b��'�������������e�xmantissa/js/Mantissa/Authentication.jsPK�����9FȎ0��0��%������������� �xmantissa/js/Mantissa/AutoComplete.jsPK�����9F!ml����#�������������ݴ�xmantissa/js/Mantissa/DOMReplace.jsPK�����9FA[[��[��!��������������xmantissa/js/Mantissa/LiveForm.jsPK�����9Fb^_+��+��!�������������)�xmantissa/js/Mantissa/Offering.jsPK�����9Fo��o���������������-�xmantissa/js/Mantissa/People.jsPK�����9F hI��I��$��������������xmantissa/js/Mantissa/Preferences.jsPK�����9Fq2��$�������������>�xmantissa/js/Mantissa/ScrollTable.jsPK�����9F=ȪL-��L-��"�������������q�xmantissa/js/Mantissa/StatGraph.jsPK�����9F\Yjo��o����������������xmantissa/js/Mantissa/TDB.jsPK�����9F|$��$��!��������������xmantissa/js/Mantissa/Validate.jsPK�����9F=N��N��!��������������xmantissa/js/Mantissa/__init__.jsPK�����9F=&u������#�������������F%�xmantissa/js/Mantissa/Test/Dummy.jsPK�����9F,A[t&��t&��.�������������V&�xmantissa/js/Mantissa/Test/TestAutoComplete.jsPK�����9Fڒ����,�������������M�xmantissa/js/Mantissa/Test/TestDOMReplace.jsPK�����9FpKRSL��SL��*�������������Tm�xmantissa/js/Mantissa/Test/TestLiveForm.jsPK�����9FfK����*��������������xmantissa/js/Mantissa/Test/TestOffering.jsPK�����9FX>��>��(�������������,�xmantissa/js/Mantissa/Test/TestPeople.jsPK�����9FPws.��s.��-��������������xmantissa/js/Mantissa/Test/TestPlaceholder.jsPK�����9F-6��6��,�������������n�xmantissa/js/Mantissa/Test/TestRegionLive.jsPK�����9FFI��-�������������X �xmantissa/js/Mantissa/Test/TestRegionModel.jsPK�����9F 97b*��b*��-�������������!�xmantissa/js/Mantissa/Test/TestScrollModel.jsPK�����9Ff N��N��*�������������/"�xmantissa/js/Mantissa/Test/TestValidate.jsPK�����9FT����&�������������("�xmantissa/js/Mantissa/Test/__init__.jsPK�����9F�M���M���������������"�xmantissa/js/MochiKit/Async.jsPK�����9Fc�����������������A#�xmantissa/js/MochiKit/Base.jsPK�����9FkW��kW���������������#�xmantissa/js/MochiKit/Color.jsPK�����9Fbzpp��p���������������7$�xmantissa/js/MochiKit/DOM.jsPK�����9F|r[����!�������������ʨ$�xmantissa/js/MochiKit/DateTime.jsPK�����9F@"��"���������������$�xmantissa/js/MochiKit/Format.jsPK�����9F j��j���������������$�xmantissa/js/MochiKit/Iter.jsPK�����9FG'��'�� �������������R%�xmantissa/js/MochiKit/Logging.jsPK�����9F V]*��*��$�������������z%�xmantissa/js/MochiKit/LoggingPane.jsPK�����9F :��:��!�������������%�xmantissa/js/MochiKit/MochiKit.jsPK�����9FkF���� �������������d%�xmantissa/js/MochiKit/MockDOM.jsPK�����9FJ�����������������Y%�xmantissa/js/MochiKit/Test.jsPK�����9Fi�H-��-���������������D%�xmantissa/js/MochiKit/Visual.jsPK�����9FTVދ����$�������������%�xmantissa/js/MochiKit/__package__.jsPK�����9FU&��&���������������%�xmantissa/js/PlotKit/Base.jsPK�����9F#E_��_���������������4#&�xmantissa/js/PlotKit/Canvas.jsPK�����9FFJ��J���������������S&�xmantissa/js/PlotKit/Layout.jsPK�����9Fl|rb��rb��#�������������s&�xmantissa/js/PlotKit/SVGRenderer.jsPK�����9F;"$��$��#�������������&1'�xmantissa/js/PlotKit/SweetCanvas.jsPK�����9FqD%������ �������������4V'�xmantissa/js/PlotKit/SweetSVG.jsPK�����9Fj��j���������������rr'�xmantissa/static/mantissa.cssPK�����9Fߣ �� ��-�������������-'�xmantissa/static/images/EmailAddress-icon.pngPK�����9FeS����&�������������'�xmantissa/static/images/Notes-icon.pngPK�����9Ffp��p��,�������������\'�xmantissa/static/images/PhoneNumber-icon.pngPK�����9Fߣ �� ��.�������������'�xmantissa/static/images/PostalAddress-icon.pngPK�����9FG4����+�������������o'�xmantissa/static/images/circle-throbber.gifPK�����9F\ȭ����"�������������'�xmantissa/static/images/delete.pngPK�����9FlK ������7�������������y'�xmantissa/static/images/divmod-table-body-left-edge.pngPK�����9F |���|���8�������������M'�xmantissa/static/images/divmod-table-body-right-edge.pngPK�����9F������;�������������'�xmantissa/static/images/divmod-table-footer-bottom-edge.pngPK�����9Fu������;�������������'�xmantissa/static/images/divmod-table-footer-left-corner.pngPK�����9F T������<�������������2'�xmantissa/static/images/divmod-table-footer-right-corner.pngPK�����9FW������9�������������o'�xmantissa/static/images/divmod-table-header-left-edge.pngPK�����9FC|! ��! ��;�������������'�xmantissa/static/images/divmod-table-header-right-block.pngPK�����9F6RZ������B�������������'�xmantissa/static/images/divmod-table-header-tileable-stretcher.pngPK�����9F8Ϳ����2�������������'�xmantissa/static/images/divorg-divmod-table-bg.pngPK�����9F5~��~��(�������������(�xmantissa/static/images/divorg-large.pngPK�����9FGY2% �� ��'�������������j(�xmantissa/static/images/divorg-menu.pngPK�����9F/56 ��6 ��$�������������(�xmantissa/static/images/dropdown.pngPK�����9FoȀj��j��$�������������1(�xmantissa/static/images/ellipsis.pngPK�����9FP_��_��'�������������ݠ(�xmantissa/static/images/error-small.pngPK�����9F;����!�������������(�xmantissa/static/images/first.pngPK�����9FKO33a��a��*�������������P(�xmantissa/static/images/first_disabled.pngPK�����9FI(]��]��%�������������(�xmantissa/static/images/header_bg.pngPK�����9F7O����+�������������(�xmantissa/static/images/header_bg_hover.pngPK�����9FMn���� �������������(�xmantissa/static/images/last.pngPK�����9F5Q��Q��)�������������`(�xmantissa/static/images/last_disabled.pngPK�����9FYW������&�������������(�xmantissa/static/images/li_current.gifPK�����9F������#�������������(�xmantissa/static/images/li_over.gifPK�����9F"t"/��/��9�������������(�xmantissa/static/images/login-table-header-left-block.pngPK�����9FFuyi(��(��#�������������!(�xmantissa/static/images/me-icon.pngPK�����9FZMw���w���*�������������(�xmantissa/static/images/menu-separator.pngPK�����9FV\������&�������������I(�xmantissa/static/images/menubar-bg.pngPK�����9FBZ��Z��7�������������(�xmantissa/static/images/mugshot-placeholder-smaller.pngPK�����9FH����/�������������(�xmantissa/static/images/mugshot-placeholder.pngPK�����9F{������"�������������(�xmantissa/static/images/nav_bg.gifPK�����9FeS����)�������������(�xmantissa/static/images/neutral-small.pngPK�����9FkWq��q�� �������������t(�xmantissa/static/images/next.pngPK�����9FPmC��C��)�������������#(�xmantissa/static/images/next_disabled.pngPK�����9FyR>��>��$�������������(�xmantissa/static/images/ok-small.pngPK�����9F'k �� ��B�������������-)�xmantissa/static/images/password-reset-table-header-left-block.pngPK�����9Fu����3������������� )�xmantissa/static/images/people-filter-column-bg.pngPK�����9FV'��'��.�������������p )�xmantissa/static/images/people-table-arrow.pngPK�����9FQ֐s����9�������������)�xmantissa/static/images/people-table-column-header-bg.pngPK�����9F������5�������������)�xmantissa/static/images/person-fragment-bl-corner.pngPK�����9FA`������5�������������)�xmantissa/static/images/person-fragment-br-corner.pngPK�����9Ft������5�������������)�xmantissa/static/images/person-fragment-tl-corner.pngPK�����9Fo������5�������������z)�xmantissa/static/images/person-fragment-tr-corner.pngPK�����9F~He��e��&�������������R)�xmantissa/static/images/person-gem.pngPK�����9F.������4�������������)�xmantissa/static/images/person-summary-bl-corner.pngPK�����9F5������4�������������)�xmantissa/static/images/person-summary-br-corner.pngPK�����9Fo^0������8�������������)�xmantissa/static/images/person-summary-decoration-tl.pngPK�����9F5Ĵ������8�������������)�xmantissa/static/images/person-summary-decoration-tr.pngPK�����9FHx���x���3�������������)�xmantissa/static/images/person-widget-br-corner.pngPK�����9F[w���w���3�������������)�xmantissa/static/images/person-widget-tr-corner.pngPK�����9F4����"�������������M)�xmantissa/static/images/person.pngPK�����9FKPbt��t�� �������������!)�xmantissa/static/images/prev.pngPK�����9FTIE��E��)�������������[$)�xmantissa/static/images/prev_disabled.pngPK�����9F=/}���}���1�������������&)�xmantissa/static/images/scrolltable-header-bg.pngPK�����9F;-T'��'��2�������������')�xmantissa/static/images/search-button-selected.pngPK�����9F}'#��#��/�������������*+)�xmantissa/static/images/search-button-small.pngPK�����9F�}����4�������������.)�xmantissa/static/images/search-button-unselected.pngPK�����9F2����:�������������|2)�xmantissa/static/images/signup-table-header-left-block.pngPK�����9F$��$��#�������������e7)�xmantissa/static/images/spinner.gifPK�����9F=[*���*���,�������������y\)�xmantissa/static/images/submenu_selected.gifPK�����9FA0������"�������������\)�xmantissa/static/images/tab-bg.pngPK�����9F`z������)�������������])�xmantissa/static/images/tab-left-edge.pngPK�����9F.c������*�������������_)�xmantissa/static/images/tab-right-edge.pngPK�����9F?F9������)�������������7`)�xmantissa/static/images/tab-tl-corner.pngPK�����9F|ru���u���+������������� a)�xmantissa/static/images/tab-top-edge-bg.pngPK�����9FC������)�������������a)�xmantissa/static/images/tab-tr-corner.pngPK�����9FJ4,��,��$�������������b)�xmantissa/static/images/vip-flag.pngPK�����9F3ֹ �� ��!�������������d)�xmantissa/static/js/initialize.jsPK�����9F[_P��P���������������oe)�xmantissa/static/js/search.jsPK�����9FQv=�����������������g)�xmantissa/static/js/shell.jsPK�����9Fq�����������������z)�xmantissa/static/js/signup.jsPK�����9FbAV��V��%�������������~)�xmantissa/static/js/PlotKit/dummy.svgPK�����9FgO)3��)3��(�������������L)�xmantissa/static/js/PlotKit/iecanvas.htcPK�����9Fe������%�������������)�xmantissa/themes/base/add-person.htmlPK�����9F&����*�������������)�xmantissa/themes/base/admin-endowment.htmlPK�����9FĎJO1��1��,�������������)�xmantissa/themes/base/admin-python-repl.htmlPK�����9Fq����&�������������)�xmantissa/themes/base/admin-stats.htmlPK�����9F` vW������1�������������)�xmantissa/themes/base/admin-user-interaction.htmlPK�����9FګE���E���-�������������%)�xmantissa/themes/base/athena-unsupported.htmlPK�����9F����7�������������)�xmantissa/themes/base/authentication-configuration.htmlPK�����9F0FO��O��&�������������)�xmantissa/themes/base/edit-person.htmlPK�����9FηOx��x��=�������������b)�xmantissa/themes/base/element-no-fragment-name-specified.htmlPK�����9F+1y��y��>�������������5)�xmantissa/themes/base/fragment-no-fragment-name-specified.htmlPK�����9FzR0��0��%������������� )�xmantissa/themes/base/front-page.htmlPK�����9F(_Wп������(�������������})�xmantissa/themes/base/import-people.htmlPK�����9Fl]v���v���.�������������)�xmantissa/themes/base/inequality-scroller.htmlPK�����9FQ����%�������������D)�xmantissa/themes/base/initialize.htmlPK�����9Ft^������-�������������)�xmantissa/themes/base/installed-offering.htmlPK�����9F6NJ����+�������������)�xmantissa/themes/base/liveform-compact.htmlPK�����9F,����#�������������)�xmantissa/themes/base/liveform.htmlPK�����9FRP �� �� �������������)�xmantissa/themes/base/login.htmlPK�����9F;>��>��.�������������M*�xmantissa/themes/base/mugshot-upload-form.htmlPK�����9Ff(8����1�������������*�xmantissa/themes/base/offering-configuration.htmlPK�����9F+e` ��` ��+������������� *�xmantissa/themes/base/people-organizer.htmlPK�����9F#&��&��E�������������b*�xmantissa/themes/base/person-contact-read-only-phone-number-view.htmlPK�����9F����8�������������*�xmantissa/themes/base/person-contact-read-only-view.htmlPK�����9Fk6����(�������������X*�xmantissa/themes/base/person-detail.htmlPK�����9F#B��B��*�������������b*�xmantissa/themes/base/person-extracts.htmlPK�����9F.,����*�������������$*�xmantissa/themes/base/person-fragment.htmlPK�����9FvBD��D��)�������������)*�xmantissa/themes/base/person-plugins.htmlPK�����9F{g��g��8�������������d+*�xmantissa/themes/base/person-read-only-contact-info.htmlPK�����9F5 W��W��)�������������!-*�xmantissa/themes/base/person-summary.htmlPK�����9F*EiN��N��-�������������.*�xmantissa/themes/base/port-configuration.htmlPK�����9F=I������,�������������X0*�xmantissa/themes/base/preference-editor.htmlPK�����9F)������0�������������C1*�xmantissa/themes/base/product-configuration.htmlPK�����9Fev����,�������������e2*�xmantissa/themes/base/repeated-liveform.htmlPK�����9F9V������,�������������C4*�xmantissa/themes/base/reset-check-email.htmlPK�����9F9/!~���~���%�������������5*�xmantissa/themes/base/reset-done.htmlPK�����9F=r �� ��)�������������5*�xmantissa/themes/base/reset-step-two.htmlPK�����9FB �� �� �������������@*�xmantissa/themes/base/reset.htmlPK�����9F*O����#�������������K*�xmantissa/themes/base/scroller.htmlPK�����9Fay������!�������������M*�xmantissa/themes/base/search.htmlPK�����9F!������#�������������N*�xmantissa/themes/base/settings.htmlPK�����9Fؠ(��(�� �������������O*�xmantissa/themes/base/shell.htmlPK�����9FJ?X����/�������������m*�xmantissa/themes/base/signup-configuration.htmlPK�����9FB;Ux��x��!�������������p*�xmantissa/themes/base/signup.htmlPK�����9F9=��=��+�������������q*�xmantissa/themes/base/single-endowment.htmlPK�����9Fm k���k���"�������������Ws*�xmantissa/themes/base/suspend.htmlPK�����9F�����������������t*�xmantissa/themes/base/tdb.htmlPK�����9Fk`w���w���+�������������ك*�xmantissa/themes/base/traceback-viewer.htmlPK�����9F xƩ����/�������������*�xmantissa/themes/base/uninstalled-offering.htmlPK�����9FQ۾������&�������������*�xmantissa/themes/base/user-detail.htmlPK�����9Fj w��w��+�������������*�xmantissa/themes/base/user-info-signup.htmlPK�����9F& "�� "���������������Q*�axiom/plugins/mantissacmd.pyPK�����9Fi΅�����������������*�axiom/plugins/offeringcmd.pyPK�����9FNG7V��V���������������S*�axiom/plugins/webcmd.pyPK�����9F������!�������������*�nevow/plugins/mantissa_package.pyPK�����[7$G^- ��� ���(�������������*�Mantissa-0.8.2.dist-info/DESCRIPTION.rstPK�����[7$G}%R��R��&�������������*�Mantissa-0.8.2.dist-info/metadata.jsonPK�����Z7$Gcw&/���/���!�������������*�Mantissa-0.8.2.dist-info/pbr.jsonPK�����Z7$G'������&������������� *�Mantissa-0.8.2.dist-info/top_level.txtPK�����[7$G4\���\����������������d*�Mantissa-0.8.2.dist-info/WHEELPK�����[7$G2����!�������������*�Mantissa-0.8.2.dist-info/METADATAPK�����[7$GDI��I���������������T*�Mantissa-0.8.2.dist-info/RECORDPK����Lj��ڋ+���