Zope 3 General Tips and Tricks ============================== If you use Zope 3, sometimes you want async jobs that have local sites and security set up. ``zc.async.z3.Job`` is a subclass of the main ``zc.async.job.Job`` implementation that leverages the ``setUp`` and ``tearDown`` hooks to accomplish this. It takes the site and the ids of the principals in the security context at its instantiation. The values can be mutated. Then when the job runs it sets the context up for the job's code, and then tears it down after the work has been committed (or aborted, if there was a failure). This can be very convenient for jobs that care about site-based component registries, or that care about the participants in zope.security interactions. This is different than a ``try: finally:`` wrapper around your main code that does the work, both because it is handled for you transparently, and because the context is cleaned up *after* the job's main transaction is committed. This means that code that expects a security or site context during a pre-transaction hook will be satisfied. For instance, let's imagine we have a database, and we establish a local site and an interaction with a request. [#zope3job_database_setup]_ [#empty_setup]_ Unfortunately, this is a lot of set up. [#zope3job_setup]_ >>> import zope.app.component.hooks >>> zope.app.component.hooks.setSite(site) >>> import zope.security.management >>> import zc.async.z3 >>> zope.security.management.newInteraction( ... zc.async.z3.Participation(mickey)) # usually would be a request Now we create a new job. >>> def reportOnContext(): ... print (zope.app.component.hooks.getSite().__class__.__name__, ... tuple(p.principal.id for p in ... zope.security.management.getInteraction().participations)) >>> j = root['j'] = zc.async.z3.Job(reportOnContext) The ids of the principals in the participations in the current interaction are in a ``participants`` tuple. The site is on the job's ``site`` attribute. >>> j.participants ('mickey',) >>> j.site is site True If we end the interaction, clear the local site, and run the job, the job we used (``reportOnContext`` above) shows that the context was correctly in place. >>> zope.security.management.endInteraction() >>> zope.app.component.hooks.setSite(None) >>> transaction.commit() >>> j() ('StubSite', ('mickey',)) However, now the site and interaction are empty. >>> print zope.security.management.queryInteraction() None >>> print zope.app.component.hooks.getSite() None As mentioned, the context will be maintained through the transaction's commit. Let's illustrate. >>> import zc.async >>> import transaction.interfaces >>> def setTransactionHook(): ... t = transaction.interfaces.ITransactionManager(j).get() ... t.addBeforeCommitHook(reportOnContext) ... >>> zope.app.component.hooks.setSite(site) >>> zope.security.management.newInteraction( ... zc.async.z3.Participation(mickey), zc.async.z3.Participation(jack), ... zc.async.z3.Participation(foo)) # >1 == rare but possible scenario >>> j = root['j'] = zc.async.z3.Job(setTransactionHook) >>> j.participants ('mickey', 'jack', 'foo') >>> j.site is site True >>> zope.security.management.endInteraction() >>> zope.app.component.hooks.setSite(None) >>> transaction.commit() >>> j() ('StubSite', ('mickey', 'jack', 'foo')) >>> print zope.security.management.queryInteraction() None >>> print zope.app.component.hooks.getSite() None .. rubric:: Footnotes .. [#zope3job_database_setup] >>> from ZODB.tests.util import DB >>> db = DB() >>> conn = db.open() >>> root = conn.root() >>> import zc.async.configure >>> zc.async.configure.base() >>> import zc.async.testing >>> zc.async.testing.setUpDatetime() # pins datetimes .. [#empty_setup] Without a site or an interaction, you can still instantiate and run the job normally. >>> import zc.async.z3 >>> import operator >>> j = root['j'] = zc.async.z3.Job(operator.mul, 6, 7) >>> j.participants () >>> print j.site None >>> import transaction >>> transaction.commit() >>> j() 42 .. [#zope3job_setup] To do this, we need to set up the zope.app.component hooks, create a site, set up an authentication utility, and create some principals that the authentication utility can return. >>> import zope.app.component.hooks >>> zope.app.component.hooks.setHooks() >>> import zope.app.component.site >>> import persistent >>> class StubSite(persistent.Persistent, ... zope.app.component.site.SiteManagerContainer): ... pass >>> site = root['site'] = StubSite() >>> sm = zope.app.component.site.LocalSiteManager(site) >>> site.setSiteManager(sm) >>> import zope.security.interfaces >>> import zope.app.security.interfaces >>> import zope.interface >>> import zope.location >>> class StubPrincipal(object): ... zope.interface.implements(zope.security.interfaces.IPrincipal) ... def __init__(self, identifier, title, description=''): ... self.id = identifier ... self.title = title ... self.description = description ... >>> class StubPersistentAuth(persistent.Persistent, ... zope.location.Location): ... zope.interface.implements( ... zope.app.security.interfaces.IAuthentication) ... _mapping = {'foo': 'Foo Fighter', ... 'jack': 'Jack, Giant Killer', ... 'mickey': 'Mickey Mouse'} ... def getPrincipal(self, principal_id): ... return StubPrincipal(principal_id, self._mapping[principal_id]) ... >>> auth = StubPersistentAuth() >>> sm.registerUtility(auth, zope.app.security.interfaces.IAuthentication) >>> transaction.commit() >>> mickey = auth.getPrincipal('mickey') >>> jack = auth.getPrincipal('jack') >>> foo = auth.getPrincipal('foo')