[Products.Silva][Sylvain Viollon] Refactor icon support to make ...

[email protected] Tue, 27 Aug 2013 18:23:33 +0200
Newsgroups gmane.comp.web.zope.silva.cvs
Message-ID <[email protected]>
author:    Sylvain Viollon
date:      Tue Aug 27 18:23:30 2013 +0200
revision:  11852:7c450b1e9276 in Products.Silva
branch:    2.4
details:   https://hg.infrae.com/Products.Silva?cmd=changeset;node=7c450b1e9276
modified:  Products/Silva/SimpleMembership.py Products/Silva/icon.py Products/Silva/tests/test_icon.py
added:     
removed:   
log:       Refactor icon support to make them more customizable.


diffstat:

 Products/Silva/SimpleMembership.py |   23 ++++
 Products/Silva/icon.py             |  199 +++++++++++++++++++++++-------------
 Products/Silva/tests/test_icon.py  |  134 +++++++++++-------------
 3 files changed, 208 insertions(+), 148 deletions(-)

diffs (525 lines):

diff -r 30d0bbee6340 -r 7c450b1e9276 Products/Silva/SimpleMembership.py
--- a/Products/Silva/SimpleMembership.py	Tue Aug 27 11:53:08 2013 +0200
+++ b/Products/Silva/SimpleMembership.py	Tue Aug 27 18:23:30 2013 +0200
@@ -2,6 +2,9 @@
 # Copyright (c) 2003-2013 Infrae. All rights reserved.
 # See also LICENSE.txt
 
+import urllib
+import hashlib
+
 from five import grok
 
 # zope
@@ -13,6 +16,7 @@
 # silva
 from Products.Silva import SilvaPermissions
 from Products.Silva import roleinfo
+from Products.Silva.icon import Icon
 from Products.Silva.Membership import cloneMember, Member
 from Products.Silva.Security import Security
 from Products.Silva.helpers import add_and_edit
@@ -23,6 +27,25 @@
 from silva.core import conf as silvaconf
 from silva.translations import translate as _
 
+# This is not yet in use.
+GRAVATAR_URL = "https://secure.gravatar.com/avatar/"
+
+class GravatarIcon(Icon):
+
+    def __init__(self, icon):
+        self.icon = icon
+
+    def url(self, resolver, content):
+        if content is not None:
+            if interfaces.IAuthorization.providedBy(content):
+                content = content.source
+            email = content.avatar()
+            if email:
+                return (GRAVATAR_URL +
+                        hashlib.md5(email.lower()).hexdigest() +
+                        urllib.urlencode({'default': 'default', 'size': 16}))
+        return super(GravatarIcon, self).url(resolver, content)
+
 
 class SimpleMember(Member, Security, ZMIObject):
     """Silva Simple Member"""
diff -r 30d0bbee6340 -r 7c450b1e9276 Products/Silva/icon.py
--- a/Products/Silva/icon.py	Tue Aug 27 11:53:08 2013 +0200
+++ b/Products/Silva/icon.py	Tue Aug 27 18:23:30 2013 +0200
@@ -2,6 +2,8 @@
 # Copyright (c) 2003-2013 Infrae. All rights reserved.
 # See also LICENSE.txt
 
+import types
+
 from five import grok
 from zope.cachedescriptors.property import Lazy
 from zope.publisher.interfaces.browser import IBrowserRequest
@@ -11,88 +13,93 @@
 from silva.core.views.interfaces import IVirtualSite
 from silva.core.interfaces.adapters import IIconResolver
 
+_marker = object()
 
-class SilvaIcons(grok.DirectoryResource):
-    # This export the globals directory using Zope 3 technology.
+class IconDirectory(grok.DirectoryResource):
+    # This export the globals directory containing all the default
+    # icon using Grok.
     grok.path('icons')
     grok.name('silva.icons')
 
 
+class Icon(object):
+    """Define a simple icon, its value, how to compute its URL and
+    render a tag.
+    """
+    grok.implements(interfaces.IIcon)
+    template = """<img height="16" width="16" src="{url}" alt="{alt}" />"""
 
-# GRAVATAR_URL = "https://secure.gravatar.com/avatar.php?"
-# GRAVATAR_TEMPLATE = """
-# <img src="%(image)s" alt="%(userid)s's avatar" title="%(userid)s's avatar"
-#      style="height: %(size)spx; width: %(size)spx" />
-# """
+    def __init__(self, icon, template=None, url=None):
+        self.icon = icon
+        if url:
+            self.get_url = types.MethodType(url, self)
+        if template:
+            self.template = template
 
-# security.declareProtected(SilvaPermissions.AccessContentsInformation,
-#                           'avatar_tag')
-# def avatar_tag(self, size=32):
-#     """HTML <img /> tag for the avatar icon
-#     """
-#     #See http://en.gravatar.com/site/implement/python
-#     email = self.avatar()
-#     default = self.get_root_url() + "/globals/avatar.png"
+    def get_url(self, resolver, content):
+        return '/'.join((resolver.root_url, self.icon))
 
-#     if email:
-#         url = GRAVATAR_URL + urllib.urlencode(
-#             {'gravatar_id':hashlib.md5(email.lower()).hexdigest(),
-#              'default':default, 'size':str(size)})
-#     else:
-#         url = default
-#     info = {'userid': self.userid(),
-#             'size': size,
-#             'image': url}
-#     return GRAVATAR_TEMPLATE % info
+    def __str__(self):
+        return str(self.icon)
+
+
+class IconSprite(object):
+    """Define an icon sprite that let you override the default icon
+    set registered in Silva. You have the possibility to nest then in
+    order to reuse customizations defined in an another sprites.
+    """
+
+    def __init__(self, sprite, template=None, url=None, parent=_marker):
+        """
+        ``sprite`` is a dictionnary containing the customized icon
+               sub-url.
+        ``template`` is a Python string used to create a tag if
+               required.
+        ``url`` is a method called if the URL of the icon is
+               required. ``parent`` is the srpite that is extended.
+        """
+        if parent is _marker:
+            parent = registry
+        self._icons = {}
+        self._parent = parent
+        for key, icon in sprite.iteritems():
+            if not isinstance(key, tuple):
+                key = ('meta_type', key)
+            if not interfaces.IIcon.providedBy(icon):
+                icon = Icon(icon, template, url)
+            self._icons[key] = icon
+
+    def get(self, identifier, default=_marker):
+        icon = self._icons.get(identifier)
+        if icon is None:
+            return self._parent.get(identifier, default)
+        return icon
 
 
 class IconRegistry(object):
+    """The icon registry is the default icon sprite of Silva.
+    """
     grok.implements(interfaces.IIconRegistry)
 
     def __init__(self):
         self._icons = {}
 
-    def get_icon(self, content):
-        if interfaces.IGhost.providedBy(content):
-            identifier = ('ghost', 'link_ok')
-        elif interfaces.IGhostFolder.providedBy(content):
-            if content.get_link_status() is None:
-                if interfaces.IPublication.providedBy(content.get_haunted()):
-                    kind = 'publication'
-                else:
-                    kind = 'folder'
-            else:
-                kind = 'link_broken'
-            identifier = ('ghostfolder', kind)
-        elif interfaces.IFile.providedBy(content):
-            identifier = ('mime_type', content.get_mime_type())
-        elif interfaces.ISilvaObject.providedBy(content):
-            identifier = ('meta_type', content.meta_type)
-        else:
-            if content is None:
-                return '++static++/silva.icons/missing.png'
-            if interfaces.IAuthorization.providedBy(content):
-                content = content.source
-            meta_type = getattr(content, 'meta_type', None)
-            if meta_type is None:
-                raise ValueError(u"No icon for unknown object %r" % content)
-            identifier = ('meta_type', meta_type)
-        return self.get_icon_by_identifier(identifier)
-
-    def get_icon_by_identifier(self, identifier):
+    def get(self, identifier, default=_marker):
         icon = self._icons.get(identifier, None)
         if icon is None:
-            raise ValueError(u"No icon for %r" % repr(identifier))
+            if default is _marker:
+                raise ValueError(u"No icon for %r" % repr(identifier))
+            return default
         return icon
 
-    def register(self, identifier, icon_name):
+    def register(self, identifier, icon):
         """Register an icon.
-
-        NOTE: this will overwrite previous icon declarations
         """
         assert isinstance(identifier, tuple) and len(identifier) == 2, \
             'Invalid icon identifier'
-        self._icons[identifier] = icon_name
+        if not interfaces.IIcon.providedBy(icon):
+            icon = Icon(icon)
+        self._icons[identifier] = icon
 
 
 @apply
@@ -152,12 +159,13 @@
             '++static++/silva.icons/%s' % icon_name)
 
     misc_icons = [
+        ('default', None, 'generic.gif'),
         ('meta_type', None, 'missing.png'),
-        ('ghostfolder', 'folder', 'ghost_folder.gif'),
-        ('ghostfolder', 'publication', 'ghost_publication.gif'),
-        ('ghostfolder', 'link_broken', 'ghost_broken.png'),
-        ('ghost', 'link_ok', 'ghost.gif'),
-        ('ghost', 'link_broken', 'ghost_broken.png'),
+        ('meta_type', 'Silva Ghost Folder', 'ghost_folder.gif'),
+        ('meta_type', 'Silva Ghost Publication', 'ghost_publication.gif'),
+        ('meta_type', 'Broken Silva Ghost Folder', 'ghost_broken.png'),
+        ('meta_type', 'Silva Ghost', 'ghost.gif'),
+        ('meta_type', 'Broken Silva Ghost', 'ghost_broken.png'),
     ]
     for cls, kind, icon_name in misc_icons:
         registry.register(
@@ -168,47 +176,88 @@
 
 
 class IconResolver(grok.Adapter):
+    """Resolve and return an icon.
+    """
     grok.context(IBrowserRequest)
     grok.implements(IIconResolver)
 
-    default = '++static++/silva.icons/generic.gif'
+    sprite = registry
 
     def __init__(self, request):
         self.request = request
 
     @Lazy
-    def _base_url(self):
+    def root_url(self):
         site = IVirtualSite(self.request)
         return site.get_root_url()
 
     def get_tag(self, content=None, identifier=None):
         if content is not None:
-            url = self.get_content_url(content)
+            icon = self.get_content(content)
             alt = getattr(content, 'meta_type', 'Missing')
         else:
-            url = self.get_identifier_url(identifier)
+            icon = self.get_identifier(identifier)
             alt = identifier or 'Missing'
-        return """<img height="16" width="16" src="%s" alt="%s" />""" % (
-            url, alt)
+        if icon is not None:
+            return icon.template.format(
+                url=icon.get_url(self, content), alt=alt)
+        return u''
 
     def get_identifier(self, identifier):
         try:
-            return registry.get_icon_by_identifier(('meta_type', identifier))
+            return self.sprite.get(('meta_type', identifier))
         except ValueError:
-            return self.default
+            return self.sprite.get(('default', None), default=None)
 
     def get_content(self, content):
+        identifier = ('meta_type', None)
+        default = ('default', None)
         try:
-            return registry.get_icon(content)
+            if interfaces.IGhost.providedBy(content):
+                viewable = content.get_viewable()
+                if (viewable is not None and
+                    viewable.get_link_status() is not None):
+                    identifier = ('meta_type', 'Broken Silva Ghost')
+                else:
+                    identifier = ('meta_type', 'Silva Ghost')
+            elif interfaces.IGhostFolder.providedBy(content):
+                if content.get_link_status() is None:
+                    if interfaces.IPublication.providedBy(content.get_haunted()):
+                        identifier = ('meta_type', 'Silva Ghost Publication')
+                    else:
+                        identifier = ('meta_type', 'Silva Ghost Folder')
+                else:
+                    identifier = ('meta_type', 'Broken Silva Ghost Folder')
+            elif interfaces.IFile.providedBy(content):
+                identifier = ('mime_type', content.get_mime_type())
+                default = ('meta_type', 'Silva File')
+            elif interfaces.ISilvaObject.providedBy(content):
+                identifier = ('meta_type', content.meta_type)
+            elif content is None:
+                default = ('meta_type', None)
+            else:
+                if interfaces.IAuthorization.providedBy(content):
+                    content = content.source
+                meta_type = getattr(content, 'meta_type', None)
+                if meta_type is None:
+                    raise ValueError(u"No icon for unknown object %r" % content)
+                identifier = ('meta_type', meta_type)
+            return self.sprite.get(identifier)
         except ValueError:
-            return self.default
+            return self.sprite.get(default, default=None)
 
     def get_content_url(self, content):
         """Return a content icon URL.
         """
-        return "/".join((self._base_url, self.get_content(content),))
+        icon = self.get_content(content)
+        if icon is not None:
+            return icon.get_url(self, content)
+        return None
 
     def get_identifier_url(self, identifier):
         """Return a URL out of a identifier.
         """
-        return "/".join((self._base_url, self.get_identifier(identifier),))
+        icon = self.get_identifier(identifier)
+        if icon is not None:
+            return icon.get_url(self, None)
+        return None
diff -r 30d0bbee6340 -r 7c450b1e9276 Products/Silva/tests/test_icon.py
--- a/Products/Silva/tests/test_icon.py	Tue Aug 27 11:53:08 2013 +0200
+++ b/Products/Silva/tests/test_icon.py	Tue Aug 27 18:23:30 2013 +0200
@@ -5,13 +5,11 @@
 # Python
 import unittest
 
-# Silva
-from silva.core import interfaces
-from silva.core.interfaces.adapters import IIconResolver
+from silva.core.interfaces import IIconRegistry, IIconResolver, IIcon
 from zope.component import queryAdapter
 from zope.interface.verify import verifyObject
 
-from Products.Silva.icon import IconRegistry, registry
+from Products.Silva.icon import registry
 from Products.Silva.testing import FunctionalLayer, TestRequest
 
 
@@ -27,35 +25,64 @@
         self.root.pdf.set_content_type('application/pdf')
         self.root.text.set_content_type('text/plain')
 
-    def test_icon_url(self):
+    def test_icon_resolver(self):
         """Test adapter to retrieve icon urls'.
         """
         resolver = queryAdapter(TestRequest(), IIconResolver)
         self.assertTrue(verifyObject(IIconResolver, resolver))
 
+        icon = resolver.get_content(self.root)
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            resolver.get_content(self.root),
+            str(icon),
             '++resource++icon-Silva-Root.png')
+        icon = resolver.get_identifier('Silva Root')
+        self.assertTrue(verifyObject(IIcon, icon))
+        self.assertEqual(
+            str(icon),
+            '++resource++icon-Silva-Root.png')
+        icon = resolver.get_identifier(None) # Missing
+        self.assertTrue(verifyObject(IIcon, icon))
+        self.assertEqual(
+            str(icon),
+            '++static++/silva.icons/missing.png')
+        icon = resolver.get_identifier('Best content in the world') # Generic
+        self.assertTrue(verifyObject(IIcon, icon))
+        self.assertEqual(
+            str(icon),
+            '++static++/silva.icons/generic.gif')
+
         self.assertEqual(
             resolver.get_content_url(self.root),
             'http://localhost/root/++resource++icon-Silva-Root.png')
         self.assertEqual(
-            resolver.get_identifier('Silva Root'),
-            '++resource++icon-Silva-Root.png')
+            resolver.get_content_url(None),
+            'http://localhost/root/++static++/silva.icons/missing.png')
         self.assertEqual(
             resolver.get_identifier_url('Silva Root'),
             'http://localhost/root/++resource++icon-Silva-Root.png')
+        self.assertEqual(
+            resolver.get_identifier_url(None),
+            'http://localhost/root/++static++/silva.icons/missing.png')
+        self.assertEqual(
+            resolver.get_identifier_url('best content in the world'),
+            'http://localhost/root/++static++/silva.icons/generic.gif')
 
+        icon = resolver.get_content(self.root.pdf)
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            resolver.get_content(self.root.pdf),
+            str(icon),
             '++static++/silva.icons/file_pdf.png')
+        icon = resolver.get_content(self.root.text)
+        self.assertTrue(verifyObject(IIcon, icon))
+        self.assertEqual(
+            str(icon),
+            '++static++/silva.icons/file_txt.png')
+
         self.assertEqual(
             resolver.get_content_url(self.root.pdf),
             'http://localhost/root/++static++/silva.icons/file_pdf.png')
         self.assertEqual(
-            resolver.get_content(self.root.text),
-            '++static++/silva.icons/file_txt.png')
-        self.assertEqual(
             resolver.get_content_url(self.root.text),
             'http://localhost/root/++static++/silva.icons/file_txt.png')
 
@@ -85,79 +112,40 @@
     def test_default_icons(self):
         """Test default registered icons.
         """
-        # Silva content types
+        self.assertTrue(verifyObject(IIconRegistry, registry))
+
+        icon = registry.get(('meta_type', 'Silva Link'))
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            registry.get_icon(self.root),
-            '++resource++icon-Silva-Root.png')
-        self.assertEqual(
-            registry.get_icon_by_identifier(
-                ('meta_type', 'Silva Link')),
+            str(icon),
             '++resource++icon-Silva-Link.png' )
 
-        # Simple member icons
-        member = self.root.service_members.get_member('author')
+        icon = registry.get(('meta_type', 'Silva Simple Member'))
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            registry.get_icon(member),
+            str(icon),
             '++resource++icon-Silva-Simple-Member.png')
 
-        # File icons
+        icon = registry.get(('mime_type', 'application/pdf'))
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            registry.get_icon(self.root.pdf),
+            str(icon),
             '++static++/silva.icons/file_pdf.png')
+
+        icon = registry.get(('mime_type', 'text/plain'))
+        self.assertTrue(verifyObject(IIcon, icon))
         self.assertEqual(
-            registry.get_icon(self.root.text),
+            str(icon),
             '++static++/silva.icons/file_txt.png')
 
-    def test_member_gravatar(self):
-        """Test that if you ask the icon for a member you can a
-        gravatar icon.
-        """
-        # XXX This will will be implemented in Silva 3.1
-        # self.assertXMLEqual(
-        #     user.avatar_tag(),
-        #     '<img src="' + self.root.get_root_url() + '/globals/avatar.png" alt="manager\'s avatar" title="manager\'s avatar" style="height: 32px; width: 32px" />')
-        # user.set_email('[email protected]')
-        # self.assertEqual('[email protected]', user.avatar())
-        # self.assertXMLEqual(
-        #     user.avatar_tag(),
-        #     '<img src="https://secure.gravatar.com/avatar.php?default=' + urllib.quote(self.root.get_root_url(),'') + '%2Fglobals%2Favatar.png&size=32&gravatar_id=b58996c504c5638798eb6b511e6f49af" alt="manager\'s avatar" title="manager\'s avatar" style="height: 32px; width: 32px" />')
+        with self.assertRaises(ValueError):
+            registry.get(('meta_type', 'Best content in the world'))
 
-    def test_registry(self):
-        """Test registry
-        """
-        self.assertEqual(
-            self.root.pdf.get_mime_type(), 'application/pdf')
-        self.assertEqual(
-            self.root.text.get_mime_type(), 'text/plain')
-
-        registry = IconRegistry()
-        self.assertTrue(verifyObject(interfaces.IIconRegistry, registry))
-
-        registry.register(
-            ('meta_type', 'Silva Root'), 'root.png')
-        registry.register(
-            ('mime_type', 'text/plain'), 'file_text.png')
-        registry.register(
-            ('mime_type', 'application/octet-stream'), 'file.png')
-        registry.register(
-            ('mime_type', 'application/pdf'), 'file_pdf.png')
-
-        self.assertEqual(
-            registry.get_icon_by_identifier(
-                ('meta_type', 'Silva Root')),
-            'root.png')
-        self.assertEqual(
-            registry.get_icon_by_identifier(
-                ('mime_type', 'application/octet-stream')),
-            'file.png')
-        with self.assertRaises(ValueError):
-            registry.get_icon_by_identifier(('meta_type', 'Foo Bar'),)
-
-        self.assertEqual(registry.get_icon(self.root), 'root.png')
-        self.assertEqual(registry.get_icon(self.root.pdf), 'file_pdf.png')
-        self.assertEqual(registry.get_icon(self.root.text), 'file_text.png')
-        with self.assertRaises(ValueError):
-            registry.get_icon(TestRequest())
+        marker = object()
+        icon = registry.get(
+            ('meta_type', 'Best content in the world'),
+            default=marker)
+        self.assertIs(icon, marker)
 
 
 def test_suite():