[Zope3-checkins] CVS: Zope3/src/zope/app/bundle/browser - __init__.py:1.1 bundle.pt:1.1 configure.zcml:1.1 tests.py:1.1

Stephan Richter srichter at cosmos.phy.tufts.edu
Wed Mar 10 08:11:14 EST 2004


Update of /cvs-repository/Zope3/src/zope/app/bundle/browser
In directory cvs.zope.org:/tmp/cvs-serv31386/src/zope/app/bundle/browser

Added Files:
	__init__.py bundle.pt configure.zcml tests.py 
Log Message:


Moved bundles to zope.app.bundle. Noone uses this code, so I did not provide
module aliases.




=== Added File Zope3/src/zope/app/bundle/browser/__init__.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bundle support.

(See also http://dev.zope.org/Zope3/ThroughTheWebSiteDevelopment .)

A (site-management) bundle is a (site-management) folder with special
status.

Eventually, bundles will be read-only, and the only thing you can do
with bundles is install and uninstall them.  At installation time, a
bundle's dependencies and are analized and satisfied, and the
registrations in the bundle are activated, unless they conflict with
existing registrations.  This is an interactive process.

XXX This interim code is much less ambitious: it just provides a view
on a (site-management) folder that displays all registrations in a
bundle and lets the user activate them.

$Id: __init__.py,v 1.1 2004/03/10 13:11:13 srichter Exp $
"""
import re
from transaction import get_transaction

from zope.app import zapi
from zope.app.i18n import ZopeMessageIDFactory as _
from zope.app.container.interfaces import IReadContainer
from zope.app.interfaces.services.registration import \
     IRegistration, RegisteredStatus, ActiveStatus, UnregisteredStatus
from zope.app.interfaces.services.service import IServiceRegistration
from zope.component import ComponentLookupError
from zope.publisher.browser import BrowserView

class BundleView(BrowserView):

    def __init__(self, context, request):
        BrowserView.__init__(self, context, request)
        self.mypath = zapi.getPath(self.context)
        self.myversion = self.parseVersion(self.mypath)
        # Compute sitepath as the parent of mypath
        sitepath = zapi.getPath(self.context)
        i = sitepath.rfind("/")
        if i > 0:
            sitepath = sitepath[:i]
        elif i == 0:
            sitepath = "/"
        else:
            sitepath = ""
        self.sitepath = sitepath
        self.registrations = self.findRegistrations(self.context, "")
        self.registrations.sort(self.compareRegistrations)
        self.services = self.findServices()

    # Methods called from the page template (bundle.pt)

    def update(self):
        if not self.request.form:
            return
        if zapi.getName(self.context) == "default":
            # XXX This is not right: we should be able to tell bundles
            # from non-bundles and only allow this command for
            # bundles.  The Bundle tab should only be present for
            # bundles.  But for now, we simply prevent the user from
            # making a big mistake and changing the default folder.
            return "ERROR: Won't change the default folder"
        if "allclear" in self.request:
            count = 0
            for path, obj in self.registrations:
                if obj.status != UnregisteredStatus:
                    obj.status = UnregisteredStatus
                    count += 1
            if count:
                get_transaction().note("deactivate bundle")
            status = _("unregistered ${count} registrations")
            status.mapping = {'count': str(count)}
            return status
        activated = []
        registered = []
        for key, value in self.request.form.items():
            if value not in (ActiveStatus, RegisteredStatus):
                continue
            for path, obj in self.registrations:
                if key == path:
                    break
            else:
                raise ComponentLookupError(key)
        for path, obj in self.registrations:
            value = self.request.form.get(path)
            if value not in (ActiveStatus, RegisteredStatus):
                continue
            if obj.status != value:
                if value == ActiveStatus:
                    activated.append(path)
                    obj.status = ActiveStatus
                else:
                    registered.append(path)
                    obj.status = RegisteredStatus
        s = ""
        mapping = {}
        if activated:
            s += _("Activated: ${activated}.\n")
            mapping['activated'] = ", ".join(activated)
        if registered:
            s += _("Registered: ${registered}.\n")
            mapping['registered'] = ", ".join(registered)
        if s:
            get_transaction().note("activate bundle")
        # We have to do that again, since the adding to a message id makes it
        # a unicode string again.
        s = _(s)
        s.mapping = mapping
        return s

    def listServices(self):
        infos = []
        for name in self.services:
            path, insite, inbundle = self.getServiceStatus(name)
            d = {"service": name,
                 "path": path,
                 "insite": insite,
                 "inbundle": inbundle}
            infos.append(d)
        return infos

    def listRegistrations(self):
        infos = []
        for path, obj in self.registrations:
            name, advice, conflict = self.getAdvice(obj)
            d = {"path": path,
                 "service": name,
                 "advice": advice,
                 "conflict": conflict,
                 "status": obj.status,
                 "usage": obj.usageSummary(),
                 "implementation": obj.implementationSummary()}
            infos.append(d)
        return infos

    # The rest are helper methods

    def getServiceStatus(self, name):
        try:
            svc = zapi.getService(self.context, name)
        except:
            svc = None
        path = ""
        insite = False
        if svc:
            try:
                path = zapi.getPath(svc)
            except:
                pass
            else:
                insite = (path == self.sitepath or
                          path.startswith(self.sitepath + "/"))
        inbundle = self.findServiceRegistration(name)
        return path, insite, inbundle

    def getAdvice(self, obj):
        name = self.getServiceName(obj)
        advice = ActiveStatus
        conflict = ""
        sm = zapi.getServiceManager(obj)
        service = sm.queryLocalService(name)
        if service:
            registry = service.queryRegistrationsFor(obj)
            if registry:
                active = registry.active()
                if active and active != obj:
                    conflict = zapi.getPath(active)
                    if not self.inOlderVersion(active):
                        advice = RegisteredStatus
        return name, advice, conflict

    def inOlderVersion(self, obj):
        # Return whether obj (an active component) belongs to an older
        # version of the same bundle we're proposing to activate here.
        # XXX This assumes sites are named with ++etc++site; there is
        # no support for the older ++etc++Services.
        path = zapi.getPath(obj)
        prefix = "/++etc++site/"
        i = path.rfind(prefix) # (can the prefix occur twice?)
        if i < 0:
            return False
        i += len(prefix) # points just after the second "/"
        i = path.find("/", i) # finds next slash after that
        if i >= 0:
            path = path[:i]
        # Now path is of the form ".../++etc++site/name-version"
        version = self.parseVersion(path)
        if not version:
            return False
        i = path.rfind("-") + 1
        return self.mypath[:i] == path[:i] and self.myversion > version

    nineDigits = re.compile(r"^\d{1,9}$")

    def parseVersion(self, path):
        # Return a list containing the version numbers, suitably
        # modified for sane version comparison.  If there is no
        # version number, return None.  A version number is any number
        # of dot-separated integers of at most 9 digits, optionally
        # followed by another dot and something like "a1" or "b1"
        # indicating an alpha or beta version.  If no alpha or beta
        # version is present, "f" is assumed (indicating "final").
        # ("f" is chosen to compare higher than "a1", "b1" or "c1" but
        # lower than "p1"; "p1" is sometimes used to indicate a patch
        # release.)  Examples:
        #
        # "/foo/bar-boo"        -> None
        # "/foo/bar-boo-1.0"    -> ["f000000001", "f000000000", "f"]
        # "/foo/bar-boo-1.0.f"  -> ["f000000001", "f000000000", "f"]
        # "/foo/bar-boo-1.0.a1" -> ["f000000001", "f000000000", "a1"]
        #
        # Note that we do a string compare on the alpha/beta version
        # number; "a10" will compare less than "a2".  OTOH, the
        # integers are padded with leading zeros, so "10" will compare
        # higher than "2".
        i = path.rfind("/") + 1
        base = path[i:]
        i = base.rfind("-") + 1
        if not i:
            return None # No version
        version = base[i:]
        parts = version.split(".")
        last = parts[-1]
        if self.nineDigits.match(last):
            last = "f"
        else:
            last = last.lower()
            del parts[-1]
            if not parts:
                return None
        for i in range(len(parts)):
            p = parts[i]
            if not self.nineDigits.match(p):
                return None
            parts[i] = "f" + "0"*(9-len(p)) + p
        parts.append(last)
        return parts

    def findServiceRegistration(self, name):
        for path, obj in self.registrations:
            if IServiceRegistration.providedBy(obj):
                if obj.name == name:
                    return path
        return None

    def findRegistrations(self, f, prefix):
        alist = []
        for name, obj in f.items():
            if IRegistration.providedBy(obj):
                alist.append((prefix+name, obj))
            elif IReadContainer.providedBy(obj):
                alist.extend(self.findRegistrations(obj, prefix+name+"/"))
        return alist

    def compareRegistrations(self, c1, c2):
        path1, obj1 = c1
        path2, obj2 = c2
        t1 = (self.getAdjustedServiceName(obj1),
              obj1.usageSummary(),
              obj1.implementationSummary())
        t2 = (self.getAdjustedServiceName(obj2),
              obj2.usageSummary(),
              obj2.implementationSummary())
        return cmp(t1, t2)

    def findServices(self):
        sd = {}
        for path, obj in self.registrations:
            sd[self.getServiceName(obj)] = 1
        services = sd.keys()
        services.sort(self.compareServiceNames)
        return services

    def compareServiceNames(self, n1, n2):
        return cmp(self.adjustServiceName(n1), self.adjustServiceName(n2))

    def getAdjustedServiceName(self, registration):
        name = self.getServiceName(registration)
        return self.adjustServiceName(name)

    def adjustServiceName(self, name):
        # XXX Strange...  There's no symbol for it in servicenames.py
        if name == "Services":
            return ""
        else:
            return name

    def getServiceName(self, registration):
        # Return the service associated with a registration.
        return registration.serviceType


=== Added File Zope3/src/zope/app/bundle/browser/bundle.pt ===
<html metal:use-macro="views/standard_macros/page">
<head>
  <title metal:fill-slot="title" i18n:translate="">Bundle Information</title>
</head>
<body>
<div metal:fill-slot="body" tal:define="message view/update">

  <h1 i18n:translate="">Bundle Information</h1>

  <tal:block define="serviceList view/listServices">
    <h4 i18n:translate="">Services needed by this bundle</h4>

    <ul tal:condition="serviceList">
      <li tal:repeat="svc serviceList">
        <span i18n:translate="">
          <i tal:content="svc/service" i18n:name="service_name">Foo</i> service
        </span>:
        <span tal:condition="svc/insite" i18n:translate="">
          present in site at
          <a tal:content="svc/path" tal:attributes="href svc/path"
             i18n:name="path">/path</a>
        </span>
        <span tal:condition="not:svc/insite">
          <span tal:condition="svc/inbundle" i18n:translate="">
            registered in bundle at
            <a tal:content="svc/inbundle"
               tal:attributes="href svc/inbundle" i18n:name="path">path</a>
          </span>
          <span tal:condition="not:svc/inbundle">
            <font size="+1" color="red">
              <b i18n:translate="">UNFULFILLED DEPENDENCY</b>
            </font>
            <br />
            <b i18n:translate="">
              (You must <a href="../default/AddService">add a
              <i tal:content="svc/service" i18n:name="service_name">Foo</i>
              service to this site</a> before you can activate this bundle)
            </b>
          </span>
        </span>
      </li>
    </ul>
    <p tal:condition="not:serviceList" i18n:translate="">
      No services are required by this bundle.
    </p>

    <h4 i18n:translate="">Registrations in this bundle</h4>

    <div class="message" tal:condition="message">
      <span tal:replace="message">view/update message here</span>
      <br /><br />
      <i>
        <a href="@@bundle.html" i18n:translate="">(click to clear message)</a>
      </i>
    </div>

    <form action="@@bundle.html" method="get"
          tal:define="registrationList view/listRegistrations">

      <tal:block tal:repeat="svc serviceList">

        <p i18n:translate="">For 
          <i tal:content="svc/service" i18n:name="service_name">Foo</i> service
        </p>

        <ul>
          <tal:block tal:repeat="cnf registrationList">
            <li tal:condition="python: cnf['service'] == svc['service']"
                tal:define="activate python:cnf['advice'] == 'Active'">
              <a tal:attributes="href cnf/path" i18n:translate="">
                <i tal:content="cnf/usage" i18n:name="usage_summary">Usage</i>
                implemented by
                <i tal:content="cnf/implementation" i18n:name="impl_summary">
                  Implementation summary</i>
              </a>
              <span tal:condition="cnf/conflict">
                <br />
                <font color="red" tal:condition="not:activate"
                      i18n:translate="">
                  Conflicts with
                </font>
                <font color="green" tal:condition="activate" i18n:translate="">
                  Overrides
                </font>
                <a tal:content="cnf/conflict"
                   tal:attributes="href cnf/conflict">path</a>
              </span>
              <br />
              <span tal:condition="python: cnf['advice'] == 'Active'">
                <input type="radio" tal:attributes="name cnf/path"
                       value="Registered" />
                <span i18n:translate="">Register only</span>
                <b>
                  <input type="radio" tal:attributes="name cnf/path"
                         value="Active" checked="checked" />
                  <span i18n:translate="">Register and activate</span>
                </b>
              </span>
              <span tal:condition="python: cnf['advice'] == 'Registered'">
                <b>
                  <input type="radio" tal:attributes="name cnf/path"
                         value="Registered" checked="checked" />
                  <span i18n:translate="">Register only</span>
                </b>
                <input type="radio" tal:attributes="name cnf/path"
                       value="Active" />
                <span i18n:translate="">Register and activate</span>
              </span>
              <span i18n:translate="">
                (is: <span tal:replace="cnf/status"
                           i18n:name="active_status">Active</span>)
              </span>
            </li>
          </tal:block>
        </ul>

      </tal:block>

      <p tal:condition="not:registrationList" i18n:translate="">
        No registrations are provided by this bundle.
      </p>

      <p i18n:translate="">
        Click "Activate bundle" to perform the above actions.
      </p>

      <p><input type="submit" value="Activate bundle" 
                i18n:attributes="value activate-bundle-button"/></p>

      <p><input type="reset" value="Reset form" 
                i18n:attributes="value reset-button"/></p>
    </form>
  </tal:block>

  <form action="@@bundle.html" method="get">

    <p i18n:translate="">Click "Deactivate bundle" to unregister all 
      registrations in this bundle.</p>

    <p><input type="submit" value="Deactivate bundle"
              i18n:attributes="value deactivate-bundle-button"/></p>
              
    <input type="hidden" name="allclear" value="1" />

  </form>

</div>
</body>
</html>


=== Added File Zope3/src/zope/app/bundle/browser/configure.zcml ===
<zope:configure 
   xmlns:zope="http://namespaces.zope.org/zope"
   xmlns="http://namespaces.zope.org/browser">

  <page
     for="zope.app.bundle.interfaces.IBundle"
    permission="zope.ManageServices" 
    class="zope.app.browser.container.contents.JustContents"
    name="index.html" attribute="index" />

  <page
     name="contents.html"
     for="zope.app.bundle.interfaces.IBundle"
     menu="zmi_views" title="Contents"
     permission="zope.ManageServices"
     class="zope.app.browser.container.contents.Contents"
     attribute="contents" />

  <page
      name="bundle.html"
      for="zope.app.bundle.interfaces.IBundle"
      menu="zmi_views" title="Bundle"
      permission="zope.ManageServices"
      class=".BundleView"
      template="bundle.pt" />

</zope:configure>


=== Added File Zope3/src/zope/app/bundle/browser/tests.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Unit tests for BundleView class.

XXX Incomplete.

$Id: tests.py,v 1.1 2004/03/10 13:11:13 srichter Exp $
"""
import unittest

from zope.interface import implements
from zope.app.interfaces.traversing import IPhysicallyLocatable
from zope.app.tests.placelesssetup import PlacelessSetup
from zope.app.bundle.browser import BundleView


class SampleClass(object):

    implements(IPhysicallyLocatable)

    def __init__(self, path="/foo"):
        self.path = path

    def getPath(self):
        return self.path

    def items(self):
        return []


class TestBundleView(PlacelessSetup, unittest.TestCase):

    def setUp(self):
        super(TestBundleView, self).setUp()

    def test_parseVersion(self):
        bv = BundleView(SampleClass(), None)
        pv = bv.parseVersion
        self.assertEquals(pv("/foo/bar-666"), ["f000000666", "f"])
        self.assertEquals(pv("/foo/bar-1.0"),
                          ["f000000001", "f000000000", "f"])
        self.assertEquals(pv("foo-bar-2.3.4"),
                          ["f000000002", "f000000003", "f000000004", "f"])
        self.assertEquals(pv("bar-5.6.a7"), ["f000000005", "f000000006", "a7"])
        self.assertEquals(pv("foo"), None)
        self.assertEquals(pv("foo-bar"), None)
        self.assertEquals(pv("foo.1.0"), None)
        self.assertEquals(pv("foo-1.a1.0"), None)

    def test_inOlderVersion(self):
        bv = BundleView(SampleClass("/++etc++site/foo-bar-1.0.0"), None)
        iov = bv.inOlderVersion
        self.failUnless(iov(SampleClass("/++etc++site/foo-bar-0.9/RM/1")))
        self.failIf(iov(SampleClass("/++etc++site/bar-foo-0.9/RM/2")))
        self.failIf(iov(SampleClass("/++etc++site/foo-bar-1.1/RM/3")))

    def test_listServices(self):
        bv = BundleView(SampleClass("/++etc++site/foo-bar-1.0.0"), None)
        infos = bv.listServices()
        self.assertEquals(infos, [])

    def test_listRegistrations(self):
        bv = BundleView(SampleClass("/++etc++site/foo-bar-1.0.0"), None)
        infos = bv.listRegistrations()
        self.assertEquals(infos, [])


def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(TestBundleView))
    return suite

if __name__ == '__main__':
    unittest.main()




More information about the Zope3-Checkins mailing list