[kupu-checkins] r33114 - in kupu/trunk/kupu: common doc plone plone/kupu_plone_layer plone/tests

duncan at codespeak.net duncan at codespeak.net
Tue Oct 10 16:56:32 CEST 2006


Author: duncan
Date: Tue Oct 10 16:56:24 2006
New Revision: 33114

Added:
   kupu/trunk/kupu/common/kupu_kjax.js   (contents, props changed)
   kupu/trunk/kupu/plone/kupu_plone_layer/kupu_kjax_support.xml.pt   (contents, props changed)
   kupu/trunk/kupu/plone/kupu_plone_layer/kupu_migration.xml.pt   (contents, props changed)
   kupu/trunk/kupu/plone/tests/test_links.py   (contents, props changed)
   kupu/trunk/kupu/plone/tests/test_urls.py   (contents, props changed)
   kupu/trunk/kupu/plone/zmi_links.pt   (contents, props changed)
Modified:
   kupu/trunk/kupu/common/kupubasetools.js
   kupu/trunk/kupu/common/kupudrawers.js
   kupu/trunk/kupu/common/kupuhelpers.js
   kupu/trunk/kupu/doc/CHANGES.txt
   kupu/trunk/kupu/plone/html2captioned.py
   kupu/trunk/kupu/plone/kupu_config.pt
   kupu/trunk/kupu/plone/kupu_plone_layer/kupuplone.css.dtml
   kupu/trunk/kupu/plone/plonedrawers.py
   kupu/trunk/kupu/plone/plonelibrarytool.py
   kupu/trunk/kupu/plone/tests/runme.cmd
   kupu/trunk/kupu/plone/tests/test_browserSupportsKupu.py
   kupu/trunk/kupu/plone/tests/test_html2captioned.py
   kupu/trunk/kupu/plone/tests/test_plonedrawer.py
Log:
Added a link maintenance page.

Added: kupu/trunk/kupu/common/kupu_kjax.js
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/common/kupu_kjax.js	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,94 @@
+/*****************************************************************************
+ *
+ * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
+ *
+ * This software is distributed under the terms of the Kupu
+ * License. See LICENSE.txt for license text. For a list of Kupu
+ * Contributors see CREDITS.txt.
+ * 
+ *****************************************************************************/
+
+/* Javascript to aid migration page. */
+
+function Migration() {};
+(function(p){
+    var fudge = new LibraryDrawer();
+    p._loadXML = fudge._loadXML;
+    p._xmlcallback = function(dom) {
+        this.xmldata = dom;
+        this.updateDisplay();
+    };
+    p.updateDisplay = function() {
+        var nodes = this.xmldata.selectNodes("//*[@kj:mode]");
+        for (var i = 0; i < nodes.length; i++) {
+            var n = nodes[i];
+            var mode = n.getAttribute('kj:mode');
+            n = document.importNode(n, true);
+            var id = n.getAttribute('id');
+            var target;
+            if (id) {
+                target = document.getElementById(id);
+            } else {
+                target = document.getElementById('kupu-default-target');
+                mode = 'append';
+            }
+            if (mode=='append') {
+                while(n.firstChild) {
+                    target.appendChild(n.firstChild);
+                };
+            } else if (mode=='replace') {
+                Sarissa.copyChildNodes(n, target);
+            } else if (mode=='prepend') {
+                var t = target.firstChild;
+                while (n.firstChild) {
+                    target.insertBefore(n.firstChild, t);
+                };
+            };
+        };
+        this.nextRequest();
+    };
+    p.nextRequest = function() {
+        var next = this.xmldata.selectSingleNode('//*[@kj:next]');
+        if (next) {
+            var xmluri = next.getAttribute('kj:next');
+            this._loadXML(xmluri, this._xmlcallback);
+        } else {
+            this.trace("complete");
+        };
+    };
+    p.newRequest = function(uri) {
+        this._loadXML(uri, this._xmlcallback);
+    };
+    p.clearLog = function() {
+        var el = document.getElementById("log");
+        while (el.firstChild) el.removeChild(el.firstChild);
+    };
+    p.submitForm = function(form) {
+        var fields = [];
+        function push(el, v) {
+            fields.push(el.name+"="+encodeURIComponent(v));
+        }
+        for(var i=0; i < form.elements.length; i++)
+        {
+            var el = form.elements[i];
+            var name = /input/i.test(el.tagName)?el.type:el.tagName;
+            if (/checkbox|radio/i.test(name) && !el.checked) continue;
+            if (/select/i.test(name)) {
+                push(el, options[el.selectedIndex].value);
+                continue;
+            }
+            if (/text|hidden|checkbox|radio|textarea/i.test(name)) {
+                push(el, el.value);
+            };
+        }
+        //alert(fields.join('\n'));
+        this._loadXML(form.getAttribute('action'), this._xmlcallback, fields.join('&'));
+        return false;
+    };
+    p.trace = function(s) {
+        var el = document.getElementById("log");
+        if (el) el.appendChild(newElement("div", [s]));
+    };
+})(Migration.prototype);
+
+var kj = new Migration();

Modified: kupu/trunk/kupu/common/kupubasetools.js
==============================================================================
--- kupu/trunk/kupu/common/kupubasetools.js	(original)
+++ kupu/trunk/kupu/common/kupubasetools.js	Tue Oct 10 16:56:24 2006
@@ -412,12 +412,14 @@
             for (var i = 0; i < paraoptions.length; i++) {
                 select.appendChild(option(paraoptions[i]));
             }
-            var grp = document.createElement('optgroup');
-            grp.label = 'Character styles';
-            for (var i = 0; i < styleoptions.length; i++) {
-                grp.appendChild(option(styleoptions[i]));
+            if (styleoptions.length) {
+                var grp = document.createElement('optgroup');
+                grp.label = 'Character styles';
+                for (var i = 0; i < styleoptions.length; i++) {
+                    grp.appendChild(option(styleoptions[i]));
+                }
+                select.appendChild(grp);
             }
-            select.appendChild(grp);
         }
         if (inTable) {
             var grp = tablegrp = document.createElement('optgroup');

Modified: kupu/trunk/kupu/common/kupudrawers.js
==============================================================================
--- kupu/trunk/kupu/common/kupudrawers.js	(original)
+++ kupu/trunk/kupu/common/kupudrawers.js	Tue Oct 10 16:56:24 2006
@@ -1199,7 +1199,7 @@
                     throw "Error loading XML";
                 };
                 var dom = xmlhttp.responseXML;
-                if (!dom.documentElement) { /* IE bug! */
+                if (!dom || !dom.documentElement) { /* IE bug! */
                     dom = Sarissa.getDomDocument();
                     dom.loadXML(xmlhttp.responseText);
                 }

Modified: kupu/trunk/kupu/common/kupuhelpers.js
==============================================================================
--- kupu/trunk/kupu/common/kupuhelpers.js	(original)
+++ kupu/trunk/kupu/common/kupuhelpers.js	Tue Oct 10 16:56:24 2006
@@ -258,7 +258,7 @@
             };
             var name = child.nodeName.toLowerCase();
             if (child.attributes[0] && /^_/.test(child.attributes[0])) {
-                name += child.attributes[0].toLowerCase(); // Fix for Opera
+                name += child.attributes[0].name.toLowerCase(); // Fix for Opera
             }
             if (dict[name] != undefined) {
                 if (!dict[name].push) {

Modified: kupu/trunk/kupu/doc/CHANGES.txt
==============================================================================
--- kupu/trunk/kupu/doc/CHANGES.txt	(original)
+++ kupu/trunk/kupu/doc/CHANGES.txt	Tue Oct 10 16:56:24 2006
@@ -14,6 +14,11 @@
     closest style round the selection (i.e. a span or block tag, or 
     removes the className if it hits a table tag with a class).
 
+  - Added a link maintenance page. Checks for bad links (i.e. those 
+    kupu doesn't understand), also converts links from relative paths
+    to resolveuid and back again. Currently it doesn't actually change
+    anything though!
+
 - 1.4 Beta 1
 
   - Fixed some problems with handling of multi-valued form fields in the

Modified: kupu/trunk/kupu/plone/html2captioned.py
==============================================================================
--- kupu/trunk/kupu/plone/html2captioned.py	(original)
+++ kupu/trunk/kupu/plone/html2captioned.py	Tue Oct 10 16:56:24 2006
@@ -13,6 +13,7 @@
 from DocumentTemplate.DT_Var import newline_to_br
 import re
 from cgi import escape
+from urlparse import urlsplit, urljoin, urlunsplit
 
 __revision__ = '$Id$'
 
@@ -151,3 +152,318 @@
 def initialize():
     engine = getToolByName(portal, 'portal_transforms')
     engine.registerTransform(register())
+
+ATTR_HREF = ATTR_VALUE % 'href'
+LINK_PATTERN = re.compile(
+    r'(?P<prefix>\<(?:img\s[^>]*src|a\s[^>]*href)=(?:"?))(?P<href>(?<=")[^"]*|[^ \/>]*)',
+    re.IGNORECASE)
+
+class Migration:
+    FIELDS = ('portal_type', 'typename', 'fieldname',
+        'fieldlabel', 'position', 'action', 'dryrun',
+        'image_tails'
+    )
+
+    def __init__(self, tool):
+        self.tool = tool
+        self.url_tool = getToolByName(tool, 'portal_url')
+        self.portal = self.url_tool.getPortalObject()
+        self.portal_base = self.url_tool.getPortalPath()
+        self.portal_base_url = self.portal.absolute_url()
+        self.prefix_length = len(self.portal_base)+1
+        self.uid_catalog = getToolByName(tool, 'uid_catalog')
+        self.reference_tool = getToolByName(tool, 'reference_catalog')
+        self.portal_catalog = getToolByName(tool, 'portal_catalog')
+        self._continue = True
+
+    def initFromRequest(self):
+        self.image_tails = self.tool._getImageSizes()
+        request = self.tool.REQUEST
+        fields = [f for f in request.form.get('fields',()) if f.get('selected',0)]
+        if fields:
+            f = fields[0]
+            self.portal_type = f.portal_type
+            self.typename = f.type
+            self.fieldname = f.name
+            self.fieldlabel = f.label
+        else:
+            self.portal_type = None
+            self.fieldname = None
+            self.fieldlabel = None
+
+        self.position = 0
+        self.action = request.form.get('button', None)
+        self.dryrun = request.form.get('dryrun', '') != 'I agree'
+
+    def saveState(self):
+        SESSION = self.tool.REQUEST.SESSION
+        SESSION['kupu_migrator'] = dict([(f, getattr(self, f, None)) for f in self.FIELDS])
+
+    def restoreState(self):
+        SESSION = self.tool.REQUEST.SESSION
+        state = SESSION['kupu_migrator']
+        for f in self.FIELDS:
+            setattr(self, f, state[f])
+
+    def clearState(self):
+        SESSION = self.tool.REQUEST.SESSION
+        if SESSION.has_key('kupu_migrator'):
+            del SESSION['kupu_migrator']
+
+    def status(self):
+        s = [ '%s=%s' % (f,getattr(self, f, 'unset')) for f in
+            self.FIELDS ]
+        return '\n'.join(s)
+
+    def getInfo(self):
+        info = {}
+        if self._continue:
+            info['nexturi'] = self.tool.absolute_url_path()+'/kupu_migration.xml?button=continue'
+        if hasattr(self, '_total'):
+            info['total'] = self._total
+            info['position'] = self.position
+            if self._total==0:
+                info['percent'] = '100%'
+            else:
+                info['percent'] = '%d%%' % ((100.*self.position)/self._total)
+            info['objects'] = getattr(self, '_objects', [])
+        action = getattr(self, 'action', '')
+        if action:
+            headings = { 'check': 'Bad links',
+                'touid': 'Links converted to resolveuid form',
+                'topath': 'Links converted to relative path',
+                }
+            heading = headings[action]
+            if self.typename:
+                heading += ' for %s (%s)' % (self.typename, self.fieldlabel)
+            info['heading'] = heading
+
+        if not self.action=='check':
+            if self.dryrun:
+                dryrun = 'Dryrun only, no changes are being made to your data'
+            else:
+                dryrun = '''Content is being updated
+                (actually that's a lie: until the code is more tested I'm not updating anything)'''
+            info['dryrun'] = dryrun
+
+        return info
+
+    def docontinue(self):
+        """Scan selected documents looking for convertible links"""
+        brains = self.portal_catalog.searchResults(portal_type=self.portal_type)
+        pos = self.position
+        self._total = total = len(brains)
+        brains = brains[pos:pos+10]
+        self.position = pos + len(brains)
+        if not brains:
+            self._continue = False
+            return False # Done
+
+        self._objects = res = []
+        for b in brains:
+            braininfo = self.brain_check(b)
+            if braininfo:
+                res.append(braininfo)
+
+        self._continue = True
+        return True
+
+    def brain_check(self, brain):
+        """Check the relative links within this object."""
+        def checklink(match):
+            matched = match.group(0)
+            newlink = link = match.group('href')
+            classification, uid, relpath, tail = self.classifyLink(link, base)
+
+            if self.action=='check':
+                if classification=='bad':
+                    info.append(link)
+            elif self.action=='touid':
+                if classification=='internal':
+                    if uid and uid==objuid:
+                        newlink = tail
+                    elif uid:
+                        newlink = 'resolveuid/%s%s' % (uid, tail)
+                    else:
+                        newlink = relpath+tail
+
+            elif self.action=='topath':
+                if classification=='internal':
+                    newlink = relpath+tail
+
+            if newlink != link:
+                prefix = match.group('prefix')
+                changes.append((match.start()+len(prefix), match.end(), newlink))
+                return prefix + newlink
+            return matched
+
+        info = []
+        changes = []
+        object = brain.getObject()
+        objuid = getattr(object.aq_base, 'UID', None)
+        if objuid:
+            objuid = objuid
+
+        base = object.absolute_url()
+        if getattr(object.aq_explicit, 'isPrincipiaFolderish', 0):
+            base += '/'
+        field = object.getField(self.fieldname)
+        data = field.getRaw(object)
+        newdata = LINK_PATTERN.sub(checklink, data)
+
+        if info or changes:
+            title = brain.Title
+            if not title:
+                title = getattr(brain, 'getId')
+            if not title:
+                title = '<object>'
+            if data != newdata:
+                diffs = htmlchanges(data, changes)
+            else:
+                diffs = None
+            return dict(title=title, info=info, url=object.absolute_url_path(),
+                diffs=diffs)
+        return None
+
+    def UIDfromURL(self, url):
+        """Convert an absolute URL to a UID"""
+        if not url.startswith(self.portal_base_url):
+            return None
+        path = url[len(self.portal_base_url)+1:]
+        if not path:
+            return None
+        try:
+            metadata = self.uid_catalog.getMetadataForUID(path)
+        except KeyError:
+            return None
+        return metadata.get('UID', None)
+
+    def brainfromurl(self, url):
+        """convert a url to a catalog brain"""
+        if not url.startswith(self.portal_base_url):
+            return None
+        url = self.portal_base + url[len(self.portal_base_url):]
+        brains = self.portal_catalog.searchResults(path=url)
+        if len(brains) != 1:
+            # Happens on Plone 2.0 :(
+            for b in brains:
+                if b.getPath()==url:
+                    return b
+            return None
+        return brains[0]
+
+    def resolveToPath(self, absurl):
+        if 'resolveuid/' in absurl:
+            bits = absurl.split('resolveuid/', 1)
+            bits = bits[1].split('/',1)
+            uid = bits[0]
+            if len(bits)==1:
+                tail = ''
+            else:
+                tail = '/' + bits[1]
+
+            # TODO: should be able to convert uid to brain without
+            # touching the actual object.
+            obj = self.reference_tool.lookupObject(uid)
+            if obj is not None:
+                newurl = obj.absolute_url()
+                return uid, newurl, tail
+        return None, None, None
+
+    def classifyLink(self, url, base, first=True):
+        """Classify a link as:
+        internal, external, bad
+
+        Returns a tuple:
+        (classification, uid, relpath, tail)
+        giving potential urls: resolveuid/<uid><tail>
+        or: <relpath><table>
+        """
+        if url.startswith('portal_factory'):
+            url = url[14:]
+        absurl = urljoin(base, url)
+        if not absurl.startswith(self.portal_base_url):
+            return 'external', None, url, ''
+        absurl = absurl.strip('/')
+
+        scheme, netloc, path, query, fragment = urlsplit(absurl)
+        tail = urlunsplit(('','','',query,fragment))
+        absurl = urlunsplit((scheme,netloc,path,'',''))
+
+        if 'resolveuid/' in absurl:
+            UID, newurl, ntail = self.resolveToPath(absurl)
+            if UID is None:
+                return 'bad', None, url, ''
+            absurl = newurl
+            tail = ntail + tail
+        else:
+            UID = self.UIDfromURL(absurl)
+
+        brain = self.brainfromurl(absurl)
+        if not brain:
+            if first:
+                # Allow image size modifiers on the end of urls.
+                p = absurl.split('/')
+                absurl = '/'.join(p[:-1])
+                if p[-1] in self.image_tails:
+                    tail = '/'+p[-1]+tail
+                    c, uid, url, _ = self.classifyLink(absurl, base, first=False)
+                    return c, uid, url, tail
+            return 'bad', None, url, ''
+
+        relative, _ = makeUrlRelative(absurl, base)
+        # Don't convert page-internal links to uids.
+        # Also fix up spurious portal_factory references
+        if not relative:
+            return 'internal', None, '', tail
+        return 'internal', UID, relative, tail
+
+def makeUrlRelative(url, base):
+    """Make a link relative to base.
+    This method assumes we have already checked that url and base have a common prefix.
+    """
+    sheme, netloc, path, query, fragment = urlsplit(url)
+    _, _, basepath, _, _ = urlsplit(base)
+
+    baseparts = basepath.split('/')
+    pathparts = path.split('/')
+
+    basetail = baseparts.pop(-1)
+
+    # Remove common elements
+    while pathparts and baseparts and baseparts[0]==pathparts[0]:
+        baseparts.pop(0)
+        pathparts.pop(0)
+
+    for i in range(len(baseparts)):
+        pathparts.insert(0, '..')
+
+    if not pathparts:
+        pathparts.insert(0, '.')
+    elif pathparts==[basetail]:
+        pathparts.pop(0)
+    
+
+    return '/'.join(pathparts), urlunsplit(('','','',query,fragment))
+
+def htmlchanges(data, changes):
+    out = []
+    prev = 0
+    lastend = 0
+    for s,e,new in changes:
+        start = max(prev, s-10)
+        if start != prev:
+            if start-10 > prev:
+                out.append(html_quote(data[prev:prev+10]))
+                out.append('...')
+            else:
+                out.append(html_quote(data[prev:start]))
+        out.append(html_quote(data[start:s]))
+        out.append('<del>%s</del>' % html_quote(data[s:e]))
+        out.append('<ins>%s</ins>' % html_quote(new))
+        prev = e
+    if prev:
+        out.append(html_quote(data[prev:prev+10]))
+        if prev+10 < len(data):
+            out.append('...')
+    return ''.join(out)

Modified: kupu/trunk/kupu/plone/kupu_config.pt
==============================================================================
--- kupu/trunk/kupu/plone/kupu_config.pt	(original)
+++ kupu/trunk/kupu/plone/kupu_config.pt	Tue Oct 10 16:56:24 2006
@@ -33,7 +33,7 @@
     <!-- simulating views -->
     <ul class="contentViews"
        tal:define="tabs python:('Config','kupu_config'),('Libraries','zmi_libraries'),('Resource Types','zmi_resource_types'),
-        ('Documentation', 'zmi_docs'),;
+        ('Documentation', 'zmi_docs'),('Links', 'zmi_links'),;
                    tabs python:[ {'label':label, 'name':name} for (label,name) in tabs ];"
     >
         <li tal:repeat="tab tabs"

Added: kupu/trunk/kupu/plone/kupu_plone_layer/kupu_kjax_support.xml.pt
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/plone/kupu_plone_layer/kupu_kjax_support.xml.pt	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<tal:block
+          xmlns:tal="http://xml.zope.org/namespaces/tal"
+          xmlns:metal="http://xml.zope.org/namespaces/metal"
+          xmlns:html="http://www.w3.org/TR/REC-html40"
+    define="charset here/portal_properties/site_properties/default_charset|here/portal_properties/default_charset|string:utf-8;
+        content_type python:request.RESPONSE.setHeader('Content-Type', 'text/xml;;charset=%s' % charset);">
+<html
+     xmlns:tal="http://xml.zope.org/namespaces/tal"
+     xmlns:metal="http://xml.zope.org/namespaces/metal"
+     xmlns:html="http://www.w3.org/TR/REC-html40"
+     xmlns:kj="http://kupu.oscom.org/namespaces/kjax">
+  <body>
+    <div tal:condition="request/qlen|nothing"
+         tal:define="length python:len(context.portal_catalog.searchResults(**request.form));"
+         id="query_length"
+         kj:mode="replace"
+         tal:content="string: $length objects in catalog">
+    </div>
+  </body>
+</html>
+</tal:block>

Added: kupu/trunk/kupu/plone/kupu_plone_layer/kupu_migration.xml.pt
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/plone/kupu_plone_layer/kupu_migration.xml.pt	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<tal:block
+          xmlns:tal="http://xml.zope.org/namespaces/tal"
+          xmlns:metal="http://xml.zope.org/namespaces/metal"
+          xmlns:html="http://www.w3.org/TR/REC-html40"
+          define="charset here/portal_properties/site_properties/default_charset|here/portal_properties/default_charset|string:utf-8;
+          content_type python:request.RESPONSE.setHeader('Content-Type', 'text/xml;;charset=%s' % charset);">
+  <html
+       xmlns="http://www.w3.org/1999/xhtml"
+       xmlns:kj="http://kupu.oscom.org/namespaces/kjax"
+
+       tal:define="pss modules/Products/PythonScripts/standard;
+       wantform not:request/form.submitted|request/button|nothing;
+       uri string:${context/absolute_url_path}/${template/getId}">
+    <body>
+      <div id="target" kj:mode="replace" tal:condition="wantform">
+        <form action="kupu_migration.xml"
+              method="post"
+              name="options_form"
+              onsubmit="return kj.submitForm(this);">
+          <fieldset>
+            <legend>Type (Field name)</legend>
+          <div tal:repeat="f context/getKupuFields">
+            <tal:var define="id string:type_radio_${repeat/f/index};
+                     first repeat/f/start;
+                     checked python:test(first,'checked',None);
+                     pt python:pss.url_quote(f['portal_type']);
+                     infouri string:${context/absolute_url_path}/kupu_kjax_support.xml?qlen=1&amp;portal_type=$pt;">
+            <input type="hidden" tal:attributes="value f/portal_type" name="fields.portal_type:records" />
+            <input type="hidden" tal:attributes="value f/type" name="fields.type:records" />
+            <input type="hidden" tal:attributes="value f/name" name="fields.name:records" />
+            <input type="hidden" tal:attributes="value f/label" name="fields.label:records" />
+            <input type="radio" name="fields.selected:records" value="1"
+                   tal:attributes="onclick string:kj.newRequest('$infouri');
+                   id id;checked checked;" />
+            <label tal:attributes="for id;">
+              <span tal:omit-tag="" tal:content="string:${f/type} (${f/label})" />
+            </label><span tal:attributes="kj:next infouri" tal:condition="checked" />
+            </tal:var>
+          </div>
+          </fieldset>
+          <fieldset>
+            <legend>Info</legend>
+            <div id="query_length"></div>
+          </fieldset>
+          <fieldset>
+            <legend>Save changes</legend>
+            <div class="formHelp">
+              Unless you agree that you are willing to let it update
+              your content, this tool will normally only show you what it could do.
+              If you have backed up your data and want to convert your links, type
+              'I agree' in the box below. The 'check links' option never changes content
+              whatever you type.
+            </div>
+            <input type="text" name="dryrun" value="" />
+          </fieldset>
+          <input type="hidden" name="form.submitted" value="1" />
+          <input type="hidden" name="button" value="" />
+          <input type="submit" value="check links" name="checklinks"
+                onclick="this.form.button.value='check'" />
+          <input type="submit" value="relative -> uids" name="touids"
+                 onclick="this.form.button.value='touid'" />
+          <input type="submit" value="uids -> path" name="topath"
+                 onclick="this.form.button.value='topath'" />
+        </form>
+      </div>
+      <div id="kupu-output" kj:mode="replace" tal:condition="wantform" />
+
+      <div tal:condition="not:wantform">
+        <tal:var tal:define="action request/button|nothing;
+                 m python:context.link_migration(action);">
+          <div tal:condition="m/nexturi|nothing" tal:attributes="kj:next m/nexturi" />
+          <div id="target" kj:mode="replace">
+            <!-- <pre tal:content="context/link_migration">current status</pre> -->
+            <div tal:condition="m/position|nothing">
+              <!-- progress bar -->
+              <div class="kupu-progress">&#xa0;
+                 <div class="kupu-progressbar"
+                      tal:attributes="style string:width:${m/percent}">&#xa0;</div>
+                 <div class="kupu-progresstext"
+                      tal:content="string:${m/position} of ${m/total}" />
+              </div>
+            </div>
+
+            <input type="button"
+                   value="go again!"
+                   tal:condition="not:m/nexturi|nothing"
+                   tal:attributes="onclick string:kj.newRequest('${uri}')" />
+
+            <h3 tal:condition="m/heading|nothing" tal:content="m/heading|nothing" />
+            <div class="highlightedSearchTerm" style="text-align:center"
+                 tal:condition="m/dryrun|nothing"
+                 tal:content="m/dryrun|nothing" />
+          </div>
+          <div id="kupu-output" kj:mode="append"
+               tal:condition="m/objects|nothing">
+            <div tal:repeat="o m/objects">
+              <div style="margin-top:1em"><a tal:attributes="href o/url" tal:content="o/title" /></div>
+              <blockquote tal:condition="o/info|nothing">
+                <div tal:repeat="link o/info" tal:content="link" />
+              </blockquote>
+              <div style="padding-left:1em"
+                   tal:content="structure o/diffs"
+                   tal:condition="o/diffs|nothing" />
+            </div>
+          </div>
+        </tal:var>
+      </div>
+    </body>
+  </html>
+</tal:block>

Modified: kupu/trunk/kupu/plone/kupu_plone_layer/kupuplone.css.dtml
==============================================================================
--- kupu/trunk/kupu/plone/kupu_plone_layer/kupuplone.css.dtml	(original)
+++ kupu/trunk/kupu/plone/kupu_plone_layer/kupuplone.css.dtml	Tue Oct 10 16:56:24 2006
@@ -273,6 +273,17 @@
 .even .kupu-preview-row {
     background-color: &dtml-evenRowBackgroundColor;;
 }
-
+.kupu-progress {
+    background-color: white;;
+    border: &dtml-borderWidth; &dtml-borderStyle; &dtml-contentViewBorderColor;;
+    width: 90%; margin: 1em; position: relative;
+}
+.kupu-progressbar {
+    background-color: &dtml-contentViewBackgroundColor;;
+    width: 50%; position:absolute;top:0;
+}
+.kupu-progresstext {
+    text-align:center;position:absolute;top:0;width:100%;
+}
 /* </dtml-let></dtml-with> */
 

Modified: kupu/trunk/kupu/plone/plonedrawers.py
==============================================================================
--- kupu/trunk/kupu/plone/plonedrawers.py	(original)
+++ kupu/trunk/kupu/plone/plonedrawers.py	Tue Oct 10 16:56:24 2006
@@ -714,7 +714,15 @@
     def canCaption(self, field):
         return (getattr(field, 'default_output_type', None) in
             ('text/x-html-safe', 'text/x-html-captioned'))
-        
+
+    security.declareProtected("View", "getKupuFields")
+    def getKupuFields(self, filter=1):
+        """Returns a list of all kupu editable fields"""
+        inuse = getToolByName(self, 'portal_catalog').uniqueValuesFor('portal_type')
+        for t,f,pt in self._getKupuFields():
+            if pt in inuse or not filter:
+                yield { 'type': t, 'name': f.getName(), 'label': f.widget.label, 'portal_type':pt }
+
     def _getKupuFields(self):
         """Yield all fields which are editable using kupu"""
         archetype_tool = getToolByName(self, 'archetype_tool', None)
@@ -726,18 +734,18 @@
                 for f in schema.fields():
                     w = f.widget
                     if isinstance(w, (RichWidget, VisualWidget)):
-                        yield typename, f
+                        yield typename, f, t['portal_type']
 
     security.declareProtected("View", "supportedCaptioning")
     def supportedCaptioning(self):
         """Returns a list of document/fields which have support for captioning"""
-        supported = [t+'/'+f.widget.label for (t,f) in self._getKupuFields() if self.canCaption(f) ]
+        supported = [t+'/'+f.widget.label for (t,f,pt) in self._getKupuFields() if self.canCaption(f) ]
         return str.join(', ', supported)
 
     security.declareProtected("View", "unsupportedCaptioning")
     def unsupportedCaptioning(self):
         """Returns a list of document/fields which do not have support for captioning"""
-        unsupp = [t+'/'+f.widget.label for (t,f) in self._getKupuFields() if not self.canCaption(f) ]
+        unsupp = [t+'/'+f.widget.label for (t,f,pt) in self._getKupuFields() if not self.canCaption(f) ]
         return str.join(', ', unsupp)
 
     def transformIsEnabled(self):
@@ -789,4 +797,20 @@
                 base = base[:posfactory]
         return base
 
+    def _getImageSizes(self):
+        resource_type = self.getResourceType()
+        portal = getToolByName(self, 'portal_url').getPortalObject()
+        mediatypes = resource_type.get_portal_types()
+        catalog = getToolByName(self, 'portal_catalog')
+        adaptor = InfoAdaptor(self, resource_type, portal)
+
+        sizes = {}
+        for portal_type in mediatypes:
+            brains = catalog.searchResults(portal_type=portal_type, limit=1)[:1]
+            if brains:
+                info = adaptor.get_image_sizes(brains[0], portal_type, '')
+                for i in info:
+                    sizes[i['uri']] = 1
+        return sizes
+
 InitializeClass(PloneDrawers)

Modified: kupu/trunk/kupu/plone/plonelibrarytool.py
==============================================================================
--- kupu/trunk/kupu/plone/plonelibrarytool.py	(original)
+++ kupu/trunk/kupu/plone/plonelibrarytool.py	Tue Oct 10 16:56:24 2006
@@ -33,6 +33,7 @@
 from Products.kupu.config import TOOLNAME, TOOLTITLE
 from StringIO import StringIO
 from urllib import quote_plus, unquote_plus
+import html2captioned
 
 _default_libraries = (
     dict(id="root",
@@ -110,9 +111,10 @@
         # We load default values here, so __init__ can still be used
         # in unit tests. Plus, it only makes sense to load these if
         # we're being added to a Plone site anyway
-        for lib in _default_libraries:
-            self.addLibrary(**lib)
-        self._res_types.update(_default_resource_types)
+        if not len(self._libraries):
+            for lib in _default_libraries:
+                self.addLibrary(**lib)
+            self._res_types.update(_default_resource_types)
 
     security.declareProtected('View', "getLinkbyuid")
     def getLinkbyuid(self):
@@ -371,6 +373,36 @@
         _docs = f.read()
         return _docs
 
+    security.declareProtected(permissions.ManageLibraries, "link_migration")
+    def link_migration(self, button='status'):
+        """Do link checking or conversion, a little bit at a time"""
+        action = button
+        migrator = html2captioned.Migration(self)
+        if action=='continue':
+            migrator.restoreState()
+            res = migrator.docontinue()
+            info = migrator.getInfo()
+            if res:
+                migrator.saveState()
+            else:
+                migrator.clearState()
+            return info
+        elif action=='status':
+            try:
+                migrator.restoreState()
+            except KeyError:
+                return "state cleared"
+            return migrator.status()
+
+        else:
+            migrator.initFromRequest()
+            migrator.saveState()
+            return migrator.getInfo()
+
+    security.declareProtected(permissions.ManageLibraries, "zmi_links")
+    zmi_links = PageTemplateFile("zmi_links.pt", globals())
+    zmi_links.title = 'kupu link maintenance'
+
     security.declareProtected(permissions.ManageLibraries, "zmi_docs")
     zmi_docs = PageTemplateFile("zmi_docs.pt", globals())
     zmi_docs.title = 'kupu configuration documentation'

Modified: kupu/trunk/kupu/plone/tests/runme.cmd
==============================================================================
--- kupu/trunk/kupu/plone/tests/runme.cmd	(original)
+++ kupu/trunk/kupu/plone/tests/runme.cmd	Tue Oct 10 16:56:24 2006
@@ -7,9 +7,11 @@
 set PRODUCTS_PATH=;%~D0%~P0..\..\..;%PLONEHOME%\Zope\lib\python\Products;%PLONEHOME%\Data\Products
 set INSTANCE_HOME=%PLONEHOME%\Data
 set SOFTWARE_HOME=%PLONEHOME%\Zope\lib\python
+ at set PYTHON=C:\Plone20\Zope\bin\python.exe
 rem "%PLONEHOME%\Python\python.exe" %~D0%~P0test_browserSupportsKupu.py %2
 rem "%PLONEHOME%\Python\python.exe" %~D0%~P0test_librarymanager.py
 rem "%PLONEHOME%\Python\python.exe" %~D0%~P0test_html2captioned.py
 rem "%PLONEHOME%\Python\python.exe" %~D0%~P0test_resourcetypemapper.py
-"%PLONEHOME%\Python\python.exe" "%~D0%~P0runalltests.py" %2
+rem "%PYTHON%" %~D0%~P0test_urls.py
+"%PYTHON%" "%~D0%~P0runalltests.py" %2
 endlocal

Modified: kupu/trunk/kupu/plone/tests/test_browserSupportsKupu.py
==============================================================================
--- kupu/trunk/kupu/plone/tests/test_browserSupportsKupu.py	(original)
+++ kupu/trunk/kupu/plone/tests/test_browserSupportsKupu.py	Tue Oct 10 16:56:24 2006
@@ -21,7 +21,15 @@
 from Products.CMFPlone.tests import PloneTestCase
 from Products.CMFPlone.tests.PloneTestCase import portal_name, portal_owner
 from AccessControl.SecurityManagement import newSecurityManager, noSecurityManager
+try:
+    import transaction
+except ImportError:
+    class dummy:
+        def get(self): return get_transaction()
+        def commit(self): return self.get().commit()
+    transaction = dummy()
 
+    
 def installKupu(quiet=0):
     _start = time.time()
     if not quiet: ZopeTestCase._print('Adding Kupu ... ')
@@ -40,7 +48,7 @@
 
     # Log out
     noSecurityManager()
-    get_transaction().commit()
+    transaction.commit()
     if not quiet: ZopeTestCase._print('done (%.3fs)\n' \
                                       % (time.time()-_start,))
     ZopeTestCase.close(app)

Modified: kupu/trunk/kupu/plone/tests/test_html2captioned.py
==============================================================================
--- kupu/trunk/kupu/plone/tests/test_html2captioned.py	(original)
+++ kupu/trunk/kupu/plone/tests/test_html2captioned.py	Tue Oct 10 16:56:24 2006
@@ -6,15 +6,6 @@
 from Products.CMFPlone.tests import PloneTestCase
 from os.path import join, abspath, dirname
 
-#try:
-#    import Zope # Sigh, make product initialization happen
-#    HAS_ZOPE = 1
-#    Zope.startup()
-#except ImportError:
-#    HAS_ZOPE = 0
-#except AttributeError: # Zope > 2.6
-#    pass
-
 from Products.PortalTransforms.tests.test_transforms import *
 from Products.PortalTransforms.tests.utils import normalize_html
 
@@ -44,6 +35,8 @@
         return self.description
     def absolute_url(self):
         return '[url for %s]' % self.uid
+    def absolute_url_path(self):
+        return '[url for %s]' % self.uid
 
 class MockCatalogTool:
     def lookupObject(self, uid):

Added: kupu/trunk/kupu/plone/tests/test_links.py
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/plone/tests/test_links.py	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,253 @@
+##############################################################################
+#
+# Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
+#
+# This software is distributed under the terms of the Kupu
+# License. See LICENSE.txt for license text. For a list of Kupu
+# Contributors see CREDITS.txt.
+#
+##############################################################################
+"""Tests for the link checking and migration code"""
+
+import os, sys
+if __name__ == '__main__':
+    execfile(os.path.join(sys.path[0], 'framework.py'))
+
+import Acquisition
+from Testing.ZopeTestCase import ZopeTestCase, installProduct
+from Products.CMFPlone.tests.PloneTestCase import portal_name, portal_owner
+from Products.Archetypes.tests import ArchetypesTestCase
+from AccessControl.SecurityManagement import newSecurityManager
+try:
+    from Products.ATContentTypes.lib import constraintypes
+except:
+    constraintypes = None
+from Products.kupu.plone.tests import TestContent
+
+try:
+    installProduct('Five', 1)
+except:
+    pass
+installProduct('ATContentTypes', 1)
+installProduct('kupu', 1)
+
+from Products.kupu.plone.plonelibrarytool import PloneKupuLibraryTool
+from Products.kupu.plone.html2captioned import Migration
+
+RESOURCES = dict(
+    linkable = ('Document', 'Image', 'File', 'Folder'),
+    mediaobject = ('Image',),
+    collection = ('Folder',),
+    containsanchors = ('Document',),
+    )
+
+# Type names vary according to the version of Plone and/or
+# ATContentTypes. Map the new names to the old ones here, and
+# turn it into an identity mapping later if we can.
+TypeMapping = {
+    'Document': 'ATDocument',
+    'Image': 'ATImage',
+    'Link': 'ATLink',
+    'Folder': 'ATFolder',
+    'File': 'ATFile',
+    'News Item': 'ATNewsItem',
+    'Event': 'ATEvent',
+}
+def MapType(typename):
+    return TypeMapping[typename]
+
+class TestLinkCode(ArchetypesTestCase.ArcheSiteTestCase):
+    """Test the link checking code"""
+
+    def afterSetUp(self):
+        portal = self.portal
+        self.setRoles(['Manager',])
+        quickinstaller = portal.portal_quickinstaller
+        quickinstaller.installProduct('ATContentTypes')
+        quickinstaller.installProduct('kupu')
+        self.kupu = portal.kupu_library_tool
+        typestool = self.portal.portal_types
+        if not hasattr(typestool, 'ATDocument'):
+            # Use the type names without the AT prefix
+            for k in TypeMapping:
+                TypeMapping[k] = k
+
+    def loginPortalOwner(self):
+        '''Use if you need to manipulate the portal itself.'''
+        uf = self.app.acl_users
+        user = uf.getUserById(portal_owner).__of__(uf)
+        newSecurityManager(None, user)
+
+    def create(self, id, metatype='ATDocument', folder=None, **kwds):
+        '''Create an object in the cms portal'''
+        if folder is None:
+            folder = self.portal
+
+        folder.invokeFactory(MapType(metatype), id)
+        obj = getattr(folder, id)
+
+        if metatype=='Folder' and constraintypes:
+            obj.setConstrainTypesMode(constraintypes.DISABLED)
+
+        if metatype=='Document':
+            obj.setTitle('Simple document')
+            obj.setText('Sample document text')
+            for k, v in kwds.items():
+                field = obj.getField(k)
+                mutator = field.getMutator(obj)(v)
+
+            obj.reindexObject()
+        return obj
+
+    def setup_content(self):
+        self.setRoles(['Manager',])
+        self.loginPortalOwner()
+        f = self.create('folder', 'Folder')
+
+        for id in ('alpha', 'beta'):
+            self.create(id, 'Document', f, subject=['aspidistra'])
+        self.create('gamma', 'Image', f)
+
+        sub1 = self.create('sub1', 'Folder', f)
+        sub1.setSubject(['aspidistra'])
+        sub1.reindexObject()
+        sub2 = self.create('sub2', 'Folder', f)
+        self.create('delta', 'Folder', sub2)
+
+        portal = self.portal
+        tool = self.portal.kupu_library_tool
+        types = tool.zmi_get_resourcetypes()
+        #tool.deleteResource([ t.name for t in types])
+        for k,v in RESOURCES.items():
+            tool.addResourceType(k, [MapType(t) for t in v])
+
+    def test_relative(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.sub2.delta.absolute_url()
+        path = '../alpha'
+        expected = ('internal', portal.folder.alpha.UID(), path, '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_external(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.sub2.delta.absolute_url()
+        path = 'mailto:me at nowhere'
+        expected = ('external', None, path, '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_localexternal(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.sub2.delta.absolute_url()
+        path = 'http://nohost/plone/folder/alpha'
+        expected = ('internal', portal.folder.alpha.UID(), '../alpha', '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_abspath(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = '/plone/folder/beta'
+        expected = ('internal', portal.folder.beta.UID(), 'beta', '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_anchor(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = '#fred'
+        expected = ('internal', None, '', path)
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_redundant(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'alpha#fred'
+        expected = ('internal', None, '', '#fred')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_bad_portal_factory(self):
+        # Some version of kupu wrongly inserted jumplinks to
+        # portal_factory. Check these get cleaned.
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'portal_factory#fred'
+        expected = ('internal', None, '', '#fred')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_dot(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = '.'
+        expected = ('internal', portal.folder.UID(), path, '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_resolveuid(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'resolveuid/' + portal.folder.beta.UID()
+        expected = ('internal', portal.folder.beta.UID(), 'beta', '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_resolveuidEmbedded(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'wibble/resolveuid/' + portal.folder.beta.UID() + '#fragment'
+        expected = ('internal', portal.folder.beta.UID(), 'beta', '#fragment')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_badlink(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'wibble'
+        expected = ('bad', None, path, '')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_image(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'gamma/image_thumb'
+        expected = ('internal', portal.folder.gamma.UID(), 'gamma', '/image_thumb')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+    def test_image2(self):
+        self.setup_content()
+        migrator = Migration(self.kupu)
+        portal = self.portal
+        base = portal.folder.alpha.absolute_url()
+        path = 'resolveuid/'+portal.folder.gamma.UID()+'/image_icon'
+        expected = ('internal', portal.folder.gamma.UID(), 'gamma', '/image_icon')
+        self.assertEquals(expected, migrator.classifyLink(path, base))
+
+        
+if __name__ == '__main__':
+    framework()
+else:
+    # While framework.py provides its own test_suite()
+    # method the testrunner utility does not.
+    from unittest import TestSuite, makeSuite
+    def test_suite():
+        suite = TestSuite()
+        suite.addTest(makeSuite(TestLinkCode))
+        return suite

Modified: kupu/trunk/kupu/plone/tests/test_plonedrawer.py
==============================================================================
--- kupu/trunk/kupu/plone/tests/test_plonedrawer.py	(original)
+++ kupu/trunk/kupu/plone/tests/test_plonedrawer.py	Tue Oct 10 16:56:24 2006
@@ -27,6 +27,10 @@
     constraintypes = None
 from Products.kupu.plone.tests import TestContent
 
+try:
+    installProduct('Five', 1)
+except:
+    pass
 installProduct('ATContentTypes', 1)
 installProduct('kupu', 1)
 

Added: kupu/trunk/kupu/plone/tests/test_urls.py
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/plone/tests/test_urls.py	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,93 @@
+##############################################################################
+#
+# Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
+#
+# This software is distributed under the terms of the Kupu
+# License. See LICENSE.txt for license text. For a list of Kupu
+# Contributors see CREDITS.txt.
+#
+##############################################################################
+"""Tests for the library tool
+
+$Id: test_resourcetypemapper.py 21075 2005-12-12 12:59:59Z duncan $
+"""
+
+import os, sys
+if __name__ == '__main__':
+    execfile(os.path.join(sys.path[0], 'framework.py'))
+
+
+import unittest
+from urlparse import urlsplit, urljoin, urlunsplit
+from Products.kupu.plone.html2captioned import makeUrlRelative
+
+class test_urls(unittest.TestCase):
+    def test1(self):
+        data = 'http://host/site/a'
+        base = 'http://host/site/'
+        expected = 'a', ''
+        self.assertEquals(expected, makeUrlRelative(data, base))
+
+    def testRelativeLinks1(self):
+        data =  'http://localhost/cms/folder/jim#_ftnref1'
+        expected = "jim", "#_ftnref1"
+        base = 'http://localhost/cms/folder/fred'
+
+        self.assertEquals(expected, makeUrlRelative(data, base))
+
+    def testRelativeLinks2(self):
+        data =  "http://localhost/cms/folder/otherdoc#key"
+        expected = "otherdoc", "#key"
+        base = 'http://localhost/cms/folder/'
+
+        self.assertEquals(expected, makeUrlRelative(data, base))
+
+    def testRelativeLinks3(self):
+        data =  "http://localhost/cms/otherfolder/otherdoc"
+        expected = "../otherfolder/otherdoc", ""
+        base = 'http://localhost/cms/folder/'
+
+        self.assertEquals(expected, makeUrlRelative(data, base))
+
+    def testRelativeLinks4(self):
+        data =  "http://localhost:9080/plone/Members/admin/art1"
+        expected = "", ""
+        base = 'http://localhost:9080/plone/Members/admin/art1'
+
+        self.assertEquals(expected, makeUrlRelative(data, base))
+
+    def testRelativeLinks5(self):
+        data =  'http://localhost:9080/plone/Members/admin/art1/subitem'
+        expected = "art1/subitem", ""
+        base = 'http://localhost:9080/plone/Members/admin/art1'
+
+        actual = makeUrlRelative(data, base)
+        self.assertEquals(actual, expected)
+
+    def testRelativeLinks6(self):
+        data =  "http://localhost:9080/plone/Members/admin"
+        expected = ".", ""
+        base = 'http://localhost:9080/plone/Members/admin/art1'
+
+        actual = makeUrlRelative(data, base)
+        self.assertEquals(actual, expected)
+
+    def testRelativeQuery(self):
+        data =  "http://localhost:9080/plone/Members/admin/jim?x=5"
+        expected = "jim", "?x=5"
+        base = 'http://localhost:9080/plone/Members/admin/art1'
+
+        actual = makeUrlRelative(data, base)
+        self.assertEquals(actual, expected)
+
+if __name__ == '__main__':
+    framework()
+else:
+    # While framework.py provides its own test_suite()
+    # method the testrunner utility does not.
+    from unittest import TestSuite, makeSuite
+    def test_suite():
+        suite = TestSuite()
+        suite.addTest(makeSuite(test_urls))
+        return suite
+

Added: kupu/trunk/kupu/plone/zmi_links.pt
==============================================================================
--- (empty file)
+++ kupu/trunk/kupu/plone/zmi_links.pt	Tue Oct 10 16:56:24 2006
@@ -0,0 +1,32 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
+lang="en"
+metal:use-macro="here/kupu_config/macros/master"
+i18n:domain="plone">
+
+  <metal:cssslot fill-slot="css_slot">
+    <link href="kupustyles.css" rel="stylesheet" type="text/css" tal:attributes="href string:${portal_url}/kupustyles.css"/>
+    <link href="kupuplone.css" rel="stylesheet" type="text/css" tal:attributes="href string:${portal_url}/kupuplone.css"/>
+    <link href="kupudrawerstyles.css" rel="stylesheet" type="text/css" tal:attributes="href string:${portal_url}/kupudrawerstyles.css"/>
+  </metal:cssslot>
+  <body>
+
+    <div class="documentContent" metal:fill-slot="kupu_content">
+      <tal:if condition="not:exists:portal/portal_javascripts"
+        metal:use-macro="context/kupu_wysiwyg_support/macros/kupu_js_include">
+      </tal:if>
+      <script type="text/javascript"
+              tal:attributes="src context/kupu_kjax.js/absolute_url_path"
+              ></script>
+      <script type="text/javascript">
+registerPloneFunction(function() {kj.newRequest('kupu_migration.xml');});
+      </script>
+      <div id="target">
+        Loading kupu link maintenance...
+      </div>
+      <div id="kupu-output"></div>
+<!--       <div id="log" style="height:100px;overflow:scroll;"> -->
+<!--       </div> -->
+    </div>
+  </body>
+</html>
+


More information about the kupu-checkins mailing list