29. Tipos Dexterity - III: Python

Sin patrocinadores una conferencia sería difícil de financiar además de que es una buena oportunidad para las empresas Plone para hacer publicidad de sus servicios.

En esta parte vamos a tratar:

  • Crear el tipo de contenido sponsor que tiene un esquema Python

  • Crear un viewlet que muestra los patrocinadores ordenados por el nivel

  • Discutir las escalas de imagen.

Primero creamos el esquema para el nuevo tipo de contenido. En lugar del código XML ahora usamos código Python. Cree una nueva carpeta content con un archivo vacío __init__.py en esta. Ahora agregue un nuevo archivo content/sponsor.py, con el siguiente código Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from plone.app.textfield import RichText
from plone.autoform import directives
from plone.namedfile import field as namedfile
from plone.supermodel.directives import fieldset
from plone.supermodel import model
from z3c.form.browser.radio import RadioFieldWidget
from zope import schema
from zope.schema.vocabulary import SimpleVocabulary
from zope.schema.vocabulary import SimpleTerm

from ploneconf.site import MessageFactory as _


LevelVocabulary = SimpleVocabulary(
    [SimpleTerm(value=u'platinum', title=_(u'Platinum Sponsor')),
     SimpleTerm(value=u'gold', title=_(u'Gold Sponsor')),
     SimpleTerm(value=u'silver', title=_(u'Silver Sponsor')),
     SimpleTerm(value=u'bronze', title=_(u'Bronze Sponsor'))]
    )


class ISponsor(model.Schema):
    """Dexterity-Schema for Sponsors
    """

    directives.widget(level=RadioFieldWidget)
    level = schema.Choice(
        title=_(u"Sponsoring Level"),
        vocabulary=LevelVocabulary,
        required=True
    )

    text = RichText(
        title=_(u"Text"),
        required=False
    )

    url = schema.URI(
        title=_(u"Link"),
        required=False
    )

    fieldset('Images', fields=['logo', 'advertisment'])
    logo = namedfile.NamedBlobImage(
        title=_(u"Logo"),
        required=False,
    )

    advertisment = namedfile.NamedBlobImage(
        title=_(u"Advertisment (Gold-sponsors and above)"),
        required=False,
    )

    directives.read_permission(notes="cmf.ManagePortal")
    directives.write_permission(notes="cmf.ManagePortal")
    notes = RichText(
        title=_(u"Secret Notes (only for site-admins)"),
        required=False
    )

Algunas cosas son notables aquí:

  • Los campos en el esquema son en su mayoría basado en el paquete zope.schema. Una referencia de campos está disponible en la siguiente dirección URL: http://docs.plone.org/4/en/external/plone.app.dexterity/docs/reference/fields.html

  • En directives.widget(level=RadioFieldWidget) cambiamos el widget predeterminado para un campo de elección de un menú desplegable para casilla de selección simple. Una referencia incompleta de widgets está disponible en la siguiente dirección URL: http://docs.plone.org/4/en/external/plone.app.dexterity/docs/reference/widgets.html

  • LevelVocabulary se utiliza para crear las opciones que se utilizan en el campo level. De esta manera se podría traducir fácilmente el valor mostrado.

  • fieldset('Images', fields=['logo', 'advertisment']) mueve las dos campos de imágenes a otra pestaña.

  • directives.read_permission(...) establece el permiso de lectura y escritura, para el campo note a los usuarios que pueden agregar nuevos usuarios de tipoMiembro. Por lo general, este permiso sólo se concede a Administradores de Sitio y Administradores. Lo utilizamos para almacenar información que no debe ser visible públicamente. Tenga en cuenta que obj.note sigue siendo accesible en plantillas y código Python. Sólo utilizando el widget (como lo hacemos en la vista posterior) comprueba el permiso.

  • Nosotros no usaremos Grok aquí

Segundo nosotros creamos el Factory Type Information - FTI para el nuevo tipo de contenido en el archivo profiles/default/types/sponsor.xml, con el siguiente código XML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0"?>
<object name="sponsor" meta_type="Dexterity FTI" i18n:domain="plone"
   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
 <property name="title" i18n:translate="">Sponsor</property>
 <property name="description" i18n:translate="">None</property>
 <property name="icon_expr">string:${portal_url}/document_icon.png</property>
 <property name="factory">sponsor</property>
 <property name="add_view_expr">string:${folder_url}/++add++sponsor</property>
 <property name="link_target"></property>
 <property name="immediate_view">view</property>
 <property name="global_allow">True</property>
 <property name="filter_content_types">True</property>
 <property name="allowed_content_types"/>
 <property name="allow_discussion">False</property>
 <property name="default_view">view</property>
 <property name="view_methods">
  <element value="view"/>
 </property>
 <property name="default_view_fallback">False</property>
 <property name="add_permission">cmf.AddPortalContent</property>
 <property name="klass">plone.dexterity.content.Container</property>
 <property name="behaviors">
  <element value="plone.app.dexterity.behaviors.metadata.IDublinCore"/>
  <element value="plone.app.content.interfaces.INameFromTitle"/>
 </property>
 <property name="schema">ploneconf.site.content.sponsor.ISponsor</property>
 <property name="model_source"></property>
 <property name="model_file"></property>
 <property name="schema_policy">dexterity</property>
 <alias from="(Default)" to="(dynamic view)"/>
 <alias from="edit" to="@@edit"/>
 <alias from="sharing" to="@@sharing"/>
 <alias from="view" to="(selected layout)"/>
 <action title="View" action_id="view" category="object" condition_expr=""
    description="" icon_expr="" link_target="" url_expr="string:${object_url}"
    visible="True">
  <permission value="View"/>
 </action>
 <action title="Edit" action_id="edit" category="object" condition_expr=""
    description="" icon_expr="" link_target=""
    url_expr="string:${object_url}/edit" visible="True">
  <permission value="Modify portal content"/>
 </action>
</object>

Entonces nosotros registramos el FTI en el archivo profiles/default/types.xml, con el siguiente código XML:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<object name="portal_types" meta_type="Plone Types Tool">
 <property name="title">Controls the available content types in your portal</property>
 <object name="talk" meta_type="Dexterity FTI"/>
 <object name="sponsor" meta_type="Dexterity FTI"/>
 <!-- -*- more types can be added here -*- -->
</object>

Después volvemos a instalar nuestro paquete y entonces nosotros podemos crear un tipos de contenido Sponsor. Utilizamos la vista predeterminada proporcionada por Dexterity ya que mostremos a los patrocinadores en un viewlet.

En cambio, nosotros nos ajustamos la vista predeterminada con algunas sentencias CSS. Agregue lo siguiente al archivo resources/ploneconf.css, con el siguiente código CSS:

.template-view.portaltype-sponsor .named-image-widget img {
    width: 100%;
    height: auto;
}

.template-view.portaltype-sponsor fieldset#folder-listing {
    display: none;
}

Si quisiéramos una vista personalizada para los tipos de contenidos Sponsor podría tenerlo con esta apariencia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
      metal:use-macro="context/main_template/macros/master"
      i18n:domain="ploneconf.site">
<body>
  <metal:content-core fill-slot="content-core">
    <h3 tal:content="structure view/w/level/render">
      Level
    </h3>

    <div tal:content="structure view/w/text/render">
      Text
    </div>

    <div class="newsImageContainer">
      <a tal:attributes="href context/url">
        <img tal:condition="python:getattr(context, 'logo', None)"
             tal:attributes="src string:${context/absolute_url}/@@images/logo/preview" />
      </a>
    </div>

    <div>
      <a tal:attributes="href context/url">
        Website
      </a>

      <img tal:condition="python:getattr(context, 'advertisment', None)"
           tal:attributes="src string:${context/absolute_url}/@@images/advertisment/preview" />

      <div tal:condition="python: 'notes' in view.w"
           tal:content="structure view/w/notes/render">
        Notes
      </div>

    </div>
  </metal:content-core>
</body>
</html>

Nota

Tenga en cuenta que tenemos que manejar el campo con permisos especiales: tal:condition="python: 'notes' in view.w" comprueba si la conveniencia diccionario w proporcionada por la clase base DefaultView sostiene el widget para el campo note. Si el usuario actual no tiene permiso cmf.ManagePortal será omitido del diccionario y sale un error ya que notes no sería una clave en w. A primera comprobación si falta trabajamos alrededor de eso.

Nosotros mostramos los tipos de contenidos Sponsor en la parte inferior de la página Web en un viewlet.

Registrar el viewlet en el archivo browser/configure.zcml, agregando el siguiente código ZCML:

1
2
3
4
5
6
7
8
9
<browser:viewlet
  name="sponsorsviewlet"
  manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
  for="*"
  layer="..interfaces.IPloneconfSiteLayer"
  class=".viewlets.SponsorsViewlet"
  template="templates/sponsors_viewlet.pt"
  permission="zope2.View"
  />

Agregue en la clase viewlet en el archivo browser/viewlets.py, con el siguiente código Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from collections import OrderedDict
from plone import api
from plone.app.layout.viewlets.common import ViewletBase
from plone.memoize import ram
from ploneconf.site.behaviors.social import ISocial
from ploneconf.site.content.sponsor import LevelVocabulary
from random import shuffle
from time import time


class SocialViewlet(ViewletBase):

    def lanyrd_link(self):
        adapted = ISocial(self.context)
        return adapted.lanyrd


class SponsorsViewlet(ViewletBase):

    @ram.cache(lambda *args: time() // (60 * 60))
    def _sponsors(self):
        catalog = api.portal.get_tool('portal_catalog')
        brains = catalog(portal_type='sponsor')
        results = []
        for brain in brains:
            obj = brain.getObject()
            scales = api.content.get_view(
                name='images',
                context=obj,
                request=self.request)
            scale = scales.scale(
                'logo',
                width=200,
                height=80,
                direction='down')
            tag = scale.tag() if scale else ''
            if not tag:
                # only display sponsors with a logo
                continue
            results.append(dict(
                title=brain.Title,
                description=brain.Description,
                tag=tag,
                url=obj.url or obj.absolute_url(),
                level=obj.level
            ))
        return results

    def sponsors(self):
        sponsors = self._sponsors()
        if not sponsors:
            return
        results = OrderedDict()
        levels = [i.value for i in LevelVocabulary]
        for level in levels:
            level_sponsors = []
            for sponsor in sponsors:
                if level == sponsor['level']:
                    level_sponsors.append(sponsor)
            if not level_sponsors:
                continue
            shuffle(level_sponsors)
            results[level] = level_sponsors
        return results
  • _sponsors regresa una lista de diccionarios conteniendo todo la información necesaria sobre los tipos de contenidos Sponsor.

  • _sponsors se almacena en caché durante una hora usando plone.memoize. De esta manera no es necesario mantener todos los objetos tipos de contenidos Sponsor en la memoria todo el tiempo. También podríamos almacenar en caché hasta que uno de los tipos de contenidos Sponsor sea modificado:

    ...
    def _sponsors_cachekey(method, self):
        catalog = api.portal.get_tool('portal_catalog')
        brains = catalog(portal_type='sponsor')
        cachekey = sum([int(i.modified) for i in brains])
        return cachekey
    
    @ram.cache(_sponsors_cachekey)
    def _sponsors(self):
        catalog = api.portal.get_tool('portal_catalog')
    ...
    
  • Creamos la etiqueta HTML IMG completa utilizando una escala personalizada (200x80) utilizando la vista images del paquete plone.namedfile. Este escalas en realidad los logotipos y las guarda como nuevos registros blobs.

  • En sponsors regresamos un diccionario ordenado de una listas aleatorias de diccionarios (que contiene la información sobre los tipos de contenidos Sponsor).

Agregar la plantilla en el archivo browser/templates/sponsors_viewlet.pt, con el siguiente código ZPT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div metal:define-macro="portal_sponsorbox"
     i18n:domain="ploneconf.site">
    <div id="portal-sponsorbox"
         tal:define="sponsors view/sponsors;">
        <div tal:repeat="level sponsors"
             tal:attributes="id python:'level-' + level"
             tal:condition="sponsors">
            <h3 tal:content="python: level.capitalize()">
                Level
            </h3>
            <tal:images tal:define="items python:sponsors[level];"
                        tal:repeat="item items">
                <div class="sponsor">
                    <a href=""
                       tal:attributes="href python:item['url'];
                                       title python:item['title'];">
                        <img tal:replace="structure python:item['tag']" />
                    </a>
                </div>
            </tal:images>
            <div class="visualClear"><!-- --></div>
        </div>
    </div>
</div>

Ahora agregue algunas sentencias CSS para estilizar la apariencia. Edite el archivo resources/ploneconf.css, agregando el siguiente código CSS:

.sponsor {
    float: left;
    margin: 0 1em 1em 0;
}

.sponsor:hover {
    box-shadow: 0 0 8px #000000;
    -moz-box-shadow: 0 0 8px #000000;
    -webkit-box-shadow: 0 0 8px #000000;
}