KEMBAR78
Dexterity in the Wild | PDF
Dexterity in the Wild
Technical case study of a complex Dexterity-based integration
David Glick
 • web developer at Groundwire Consulting

 • Plone core developer

 • Dexterity maintainer
• Strategy and technology consulting for mission-driven organizations and
    businesses

  • Building relationships to create change that helps build thriving communities
    and a healthy planet.

Services:

  • engagement strategy

  • websites (Plone)

  • CRM databases (Salesforce.com)
• Net Impact's mission is to mobilize a new generation to use their careers to
  drive transformational change in their workplaces and the world.

• 501(c)3 based in San Francisco

• over 280 chapters worldwide
Process
1. Strategy

2. Technical discovery

3. Implementation (CRM and web)
Goals
 • Build on top of proven, extensible platforms

 • Reorganize and simplify their extensive content

 • Provide an enhanced and streamlined experience for members
Key features
 • Browsable member directory & editable member profiles

 • Member data managed in Salesforce but presented on the website

 • Conference registration

 • Chapter directory

 • Webinar archive

Coming:

 • Chapter leader portal

 • Member Mail

 • Job board
Implementation notes
Member database
Requirement: Members are searchable and get their own profile page (and can
be easily synced with Salesforce without using
collective.salesforce.authplugin).

Solution: Members as content.
Membrane
  • Allows Plone users to be represented as content items

  • Provides PluggableAuthService plugins which look up the user item in a
    special catalog (the membrane_tool), then adapt to IMembraneObject to get
    an implementation suitable for accomplishing a particular task.

Plugins for:

  • Authentication

  • User properties

  • etc.
dexterity.membrane
dexterity.membrane
 • Behavior to turn a content type into a member.

 • Takes care of:

     ◦ Name content item based on person's first/last name.

     ◦ Authentication

     ◦ Provide fullname and bio properties to Plone

     ◦ Allow the user to edit their profile

     ◦ Password resets

 • Only requirement is your content type must have these fields:

     ◦ first_name, last_name, homepage, bio, password
Membrane: the ugly
 • extra catalog with unneeded indexes
The member profile workflow
Requirement: Users can choose whether or not their profiles are public.




Solution: A boolean in the member schema, plus an auto-triggering workflow.
Auto-triggering workflow
Two states:

  • membersonly

  • private

Plus an initial state, "autotrigger".

Plus two automatic transitions out of the autotrigger state.
Automatic workflow transitions
 • Fires after any manual workflow transition.

 • Doesn't show up in the workflow menu.

Example from the workflow definition:

<transition transition_id="auto_to_private" new_state="private"
            title="Members only"
            trigger="AUTOMATIC"
            before_script="" after_script="">
                                            >
  <guard>
    <guard-expression>
    <guard-expression>not:object/@@netimpact-utils/is_contact_publishable</guard-expres
                                                                         </guard-expres
  </guard>
</transition>
The workflow transition trigger
We need a manual transition to make the automatic magic happen!

@grok.subscribe(IContact, IObjectModifiedEvent)
def trigger_contact_workflow(contact, event):
    wtool = getToolByName(contact, 'portal_workflow')
    wtool.doActionFor(contact, 'autotrigger')
The result




Overkill? Maybe.
Multi-level workflow
Requirement: Any content can be designated as public, private, or visible to two
levels of member (free & paid).

Specific instance: The member directory is only accessible to members.

Solution: custom default workflow.
The two_level_member_workflow
Most content can be assigned one of these states:


  • Private - visible to Net Impact staff only

  • Premium - visible to paid members only

  • Members-only - visible to members and supporting (paid)
    members

  • Public - visible to anyone
Roles
These levels of access are modeled using 3 built-in roles:

  • Site Administrator (for staff)

  • Member (for free members)

  • Anonymous (for the public)

And one custom role:

  • Paid Member
Granting the correct roles based on member status
Membrane lets us assign custom roles using an IMembraneUserRoles adapter:

class ContactRoleProvider
      ContactRoleProvider(grok.Adapter, MembraneUser):
    grok.context(IContact)
    grok.implements(IMembraneUserRoles)

    def __init__(self, context):
        self.context = context

    def getRolesForPrincipal(self, principal, request=None):
        roles = []
        if self.context.is_staff:
            roles.append('Site Administrator')
        roles.append('Member')
        if self.context.member_status in ('Premium', 'Lifetime'):
            roles.append('Paid Member')
        return roles
Registration and profile editing
Requirement: Multi-part profile editing form with overlays.

Solution: Lots of z3c.form forms based on the content model.
XML model
In part:

<model xmlns="http://namespaces.plone.org/supermodel/schema"
       xmlns:form="http://namespaces.plone.org/supermodel/form">>
  <schema>
    <fieldset name="links" label="Links">>
      <field name="homepage" type="zope.schema.ASCIILine"
              form:validator="netimpact.content.validators.URLValidator">
                                                                        >
         <title>
         <title>Personal Website</title>
                                </title>
         <description>
         <description>Include http://</description>
                                     </description>
         <required>
         <required>False</required>
                        </required>
      </field>
      <field name="twitter" type="zope.schema.TextLine"
              form:omitted="true">
                                 >
         <title>
         <title>Twitter</title>
                       </title>
         <description>
         <description>Enter your twitter id (e.g. netimpact)</description>
                                                            </description>
         <required>
         <required>False</required>
                        </required>
      </field>
    </fieldset>
  </schema>
</model>
Connecting the model to a concrete schema
We want to use a schema called IContact, not whatever Dexterity generates for
us.

In interfaces.py:

from zope.interface import alsoProvides
from plone.directives import form
from zope.app.content.interfaces import IContentType

class IContact
      IContact(form.Schema):
    form.model('models/contact.xml')
alsoProvides(IContact, IContentType)

In profiles/default/types/netimpact.contact.xml:

<property name="schema">netimpact.content.interfaces.IContact</property>
                       >                                     </property>
Using that schema to build a form
Unusual requirements:

 • We have multiple forms with different fields, so can't use autoform.

 • Late binding of the model means we have to defer form field setup.

from plone.directives import dexterity
from netimpact.content.interfaces import IContact

class EditProfileNetworking
      EditProfileNetworking(dexterity.EditForm):
    grok.name('edit-networking')
    label = u'Networking'

    # avoid autoform functionality
    def updateFields(self):
        pass

    @property
    def fields(self):
        return field.Fields(IContact).select('homepage', 'company_homepage',
                                             'twitter', 'linkedin')
Data grid (collective.z3cform.datagridfield)
Autocomplete
Chapter selection
Searching the member directory
Requirement: Members get access to a member directory searchable by keyword,
chapter, location, job function, issue, industry, or sector.

Solution: eea.facetednavigation
Custom listings for members
Requirement: Members show in listings with custom info (school or company and
location).




Solution:

  • Override folder_listing

  • Make search results use folder_listing
Synchronizing content with Salesforce.com
Requirement: Manage and report on members in Salesforce, present the
directory on the web.

Solution: Nightly data sync.
collective.salesforce.content




http://github.com/Groundwire/collective.salesforce.content
Contact schema with Salesforce metadata
<model xmlns="http://namespaces.plone.org/supermodel/schema"
       xmlns:form="http://namespaces.plone.org/supermodel/form"
       xmlns:sf="http://namespaces.plone.org/salesforce/schema">>
  <schema sf:object="Contact"
          sf:container="/member-directory"
          sf:criteria="Member_Status__c != null">
                                                >

    <field name="email" type="zope.schema.ASCIILine"
      form:validator="netimpact.content.validators.EmailValidator"
      security:read-permission="cmf.ModifyPortalContent"
      sf:field="Email">
                      >
      <title>
      <title>E-mail Address</title>
                           </title>

    </field>
  </schema>
</model>

Performs a query like:

SELECT Id, Email FROM Contact WHERE Member_Status__c != null
Extending Dexterity schemas
Parameterized behavior.

 • Storage: Schema tagged values

 • In Python schemas: new grok directives

 • In XML model: new XML directives in custom namespace

 • TTW: Custom views to edit the tagged values
Field with custom value converter
We wanted to convert Salesforce Ids of Chapters into the Plone UUID of
corresponding Chapter items:

<field name="chapter" type="zope.schema.Choice"
       form:widget="netimpact.content.browser.widgets.ChapterFieldWidget"
       sf:field="Chapter__c" sf:converter="uuid">
                                                >
  <title>
  <title>Chapter</title>
                 </title>
  <description></description>
  <vocabulary>
  <vocabulary>netimpact.content.Chapters</vocabulary>
                                        </vocabulary>
  <required>
  <required>True</required>
                 </required>
  <default>
  <default>n/a</default>
               </default>
</field>
Custom value converters
The converter:

from collective.salesforce.behavior.converters import DefaultValueConverter

class UUIDConverter
      UUIDConverter(DefaultValueConverter, grok.Adapter):
    grok.provides(ISalesforceValueConverter)
    grok.context(IField)
    grok.name('uuid')

    def toSchemaValue(self, value):
        if value:
            res = get_catalog().searchResults(sf_object_id=value)
            if res:
                return res[0].UID
Handling collections of related info
Education list of dicts in main Contact schema:

<field name="education" type="zope.schema.List"
       form:widget="collective.z3cform.datagridfield.DataGridFieldFactory"
       sf:relationship="Schools_Attended__r">
                                            >
  <title>
  <title>Most Recent School</title>
                            </title>
  <required>
  <required>True</required>
                </required>
  <min_length> </min_length>
  <min_length>1</min_length>
  <value_type type="collective.z3cform.datagridfield.DictRow">
                                                             >
    <schema>
    <schema>netimpact.content.interfaces.IEducationInfo</schema>
                                                       </schema>
  </value_type>
</field>
The subschema
IEducationInfo is another model-based schema:

from plone.directives import form

class IEducationInfo
      IEducationInfo(form.Schema):
    form.model('models/education_info.xml')

<model xmlns="http://namespaces.plone.org/supermodel/schema"
       xmlns:form="http://namespaces.plone.org/supermodel/form"
       xmlns:sf="http://namespaces.plone.org/salesforce/schema">>
  <schema sf:object="School_Attended__c"
          sf:criteria="Organization__c != ''
                       ORDER BY Graduation_Date__c asc NULLS LAST">
                                                                  >
    <field name="school_id" type="zope.schema.TextLine" sf:field="Organization__c">
                                                                                  >
      <title>
      <title>School ID</title>
                      </title>
      <required>
      <required>False</required>
                     </required>
    </field>
  </schema>
</model>

SELECT Id, (SELECT Organization__c FROM School_Attended__c) FROM Contact
            SELECT
Writing back to Salesforce
Handled less automatically, in response to an ObjectModifiedEvent:

@grok.subscribe(IContact, IObjectModifiedEvent)
def save_contact_to_salesforce(contact, event):
    if not IModifiedViaSalesforceSync.providedBy(event):
        upsertMember(contact)
Handling payments
Requirement: Accept payments for:

 • Several types of membership

 • Conference registration

 • Conference expo exhibitors

 • Chapter dues

Solution: groundwire.checkout
groundwire.checkout
Pieces of GetPaid groundwire.checkout reuses
 • Core objects (cart and order storage)

 • Payment processing code (Authorize.net)

 • Compatible with getpaid.formgen and pfg.donationform
What groundwire.checkout provides

 • Single-page z3c.form-based checkout form with:

     ◦ cart listing,

     ◦ credit card info fieldset

     ◦ billing address fieldset

     ◦ much, much easier to customize than PloneGetPaid's

 • Order confirmation view with summary of the completed transaction

 • Agnostic as to how items get added to the cart; only handles checkout

 • API for performing actions after an item is purchased
Basic example
Add an item to the cart and redirect to checkout:

from getpaid.core.item import PayableLineItem
from groundwire.checkout.utils import get_cart
from groundwire.checkout.utils import redirect_to_checkout

item = PayableLineItem()
item.item_id = 'item'
item.name = 'My Item'
item.cost = float(5)
item.quantity = 1

cart = get_cart()
if 'item' in cart:
    del cart['item']
cart['item'] = item
redirect_to_checkout()
Performing actions after purchase
Custom item classes can perform their own actions:

from getpaid.core.item import PayableLineItem

class MyLineItem
      MyLineItem(PayableLineItem):

    def after_charged(self):
        print 'charged!'
Pricing
Products are managed in Salesforce.




But we need to determine the constituency (and thus the price) in Plone.
Product content type
Discounts




 • Auto-apply vs. coded discounts
Mixed theming approach
 • Diazo without a theme

<theme if-content="false()" href="theme.html" />

<!-- Add the site slogan after the logo (example rule with XSLT) -->
<replace css:content="#portal-logo">
                                   >
    <xsl:copy-of css:select="#portal-logo" />
    <p id="portal-slogan">Where good works.</p>
                         >                 </p>
</replace>

 • z3c.jbot to make changes to templates
Edit bar at top




<replace css:content="#visual-portal-wrapper">
                                             >
    <xsl:copy-of css:select="#edit-bar" />
    <div id="visual-portal-wrapper">>
        <xsl:apply-templates />
    </div>
</replace>
<replace css:content="#edit-bar" />
Tile-based layout




<div class="tile-placeholder"
     tal:attributes="data-tile-href string:${portal_url}/
         @@groundwire.tiles.richtext/login-newmember-features" />
Conclusion
What Plone could do
 • Rewrite the password reset tool

 • Better support for multiple levels of membership

 • Easier way to customize a type's listing view

 • Asynchronous processing infrastructure

 • Built-in support for tiles
What Dexterity could do
 • Make it possible to parameterize widgets and validators in the model

 • Better way to make multiple forms based on the same schema

 • Expand the through-the-web editor
What Plone gives for free (or cheap)
Plone was absolutely the right tool for the job.

  • Basic content management

  • Custom form creation using PloneFormGen

  • Fine-grained access control

  • Collections

  • Basic content types
Visit the site
http://netimpact.org
Contact me
David Glick
     dglick@groundwireconsulting.com

Groundwire Consulting
     http://groundwireconsulting.com
Questions?

Dexterity in the Wild

  • 1.
    Dexterity in theWild Technical case study of a complex Dexterity-based integration
  • 2.
    David Glick •web developer at Groundwire Consulting • Plone core developer • Dexterity maintainer
  • 3.
    • Strategy andtechnology consulting for mission-driven organizations and businesses • Building relationships to create change that helps build thriving communities and a healthy planet. Services: • engagement strategy • websites (Plone) • CRM databases (Salesforce.com)
  • 4.
    • Net Impact'smission is to mobilize a new generation to use their careers to drive transformational change in their workplaces and the world. • 501(c)3 based in San Francisco • over 280 chapters worldwide
  • 5.
    Process 1. Strategy 2. Technicaldiscovery 3. Implementation (CRM and web)
  • 6.
    Goals • Buildon top of proven, extensible platforms • Reorganize and simplify their extensive content • Provide an enhanced and streamlined experience for members
  • 7.
    Key features •Browsable member directory & editable member profiles • Member data managed in Salesforce but presented on the website • Conference registration • Chapter directory • Webinar archive Coming: • Chapter leader portal • Member Mail • Job board
  • 8.
  • 9.
    Member database Requirement: Membersare searchable and get their own profile page (and can be easily synced with Salesforce without using collective.salesforce.authplugin). Solution: Members as content.
  • 10.
    Membrane •Allows Plone users to be represented as content items • Provides PluggableAuthService plugins which look up the user item in a special catalog (the membrane_tool), then adapt to IMembraneObject to get an implementation suitable for accomplishing a particular task. Plugins for: • Authentication • User properties • etc.
  • 11.
  • 12.
    dexterity.membrane • Behaviorto turn a content type into a member. • Takes care of: ◦ Name content item based on person's first/last name. ◦ Authentication ◦ Provide fullname and bio properties to Plone ◦ Allow the user to edit their profile ◦ Password resets • Only requirement is your content type must have these fields: ◦ first_name, last_name, homepage, bio, password
  • 13.
    Membrane: the ugly • extra catalog with unneeded indexes
  • 14.
    The member profileworkflow Requirement: Users can choose whether or not their profiles are public. Solution: A boolean in the member schema, plus an auto-triggering workflow.
  • 15.
    Auto-triggering workflow Two states: • membersonly • private Plus an initial state, "autotrigger". Plus two automatic transitions out of the autotrigger state.
  • 16.
    Automatic workflow transitions • Fires after any manual workflow transition. • Doesn't show up in the workflow menu. Example from the workflow definition: <transition transition_id="auto_to_private" new_state="private" title="Members only" trigger="AUTOMATIC" before_script="" after_script=""> > <guard> <guard-expression> <guard-expression>not:object/@@netimpact-utils/is_contact_publishable</guard-expres </guard-expres </guard> </transition>
  • 17.
    The workflow transitiontrigger We need a manual transition to make the automatic magic happen! @grok.subscribe(IContact, IObjectModifiedEvent) def trigger_contact_workflow(contact, event): wtool = getToolByName(contact, 'portal_workflow') wtool.doActionFor(contact, 'autotrigger')
  • 18.
  • 19.
    Multi-level workflow Requirement: Anycontent can be designated as public, private, or visible to two levels of member (free & paid). Specific instance: The member directory is only accessible to members. Solution: custom default workflow.
  • 20.
    The two_level_member_workflow Most contentcan be assigned one of these states: • Private - visible to Net Impact staff only • Premium - visible to paid members only • Members-only - visible to members and supporting (paid) members • Public - visible to anyone
  • 21.
    Roles These levels ofaccess are modeled using 3 built-in roles: • Site Administrator (for staff) • Member (for free members) • Anonymous (for the public) And one custom role: • Paid Member
  • 22.
    Granting the correctroles based on member status Membrane lets us assign custom roles using an IMembraneUserRoles adapter: class ContactRoleProvider ContactRoleProvider(grok.Adapter, MembraneUser): grok.context(IContact) grok.implements(IMembraneUserRoles) def __init__(self, context): self.context = context def getRolesForPrincipal(self, principal, request=None): roles = [] if self.context.is_staff: roles.append('Site Administrator') roles.append('Member') if self.context.member_status in ('Premium', 'Lifetime'): roles.append('Paid Member') return roles
  • 23.
    Registration and profileediting Requirement: Multi-part profile editing form with overlays. Solution: Lots of z3c.form forms based on the content model.
  • 26.
    XML model In part: <modelxmlns="http://namespaces.plone.org/supermodel/schema" xmlns:form="http://namespaces.plone.org/supermodel/form">> <schema> <fieldset name="links" label="Links">> <field name="homepage" type="zope.schema.ASCIILine" form:validator="netimpact.content.validators.URLValidator"> > <title> <title>Personal Website</title> </title> <description> <description>Include http://</description> </description> <required> <required>False</required> </required> </field> <field name="twitter" type="zope.schema.TextLine" form:omitted="true"> > <title> <title>Twitter</title> </title> <description> <description>Enter your twitter id (e.g. netimpact)</description> </description> <required> <required>False</required> </required> </field> </fieldset> </schema> </model>
  • 27.
    Connecting the modelto a concrete schema We want to use a schema called IContact, not whatever Dexterity generates for us. In interfaces.py: from zope.interface import alsoProvides from plone.directives import form from zope.app.content.interfaces import IContentType class IContact IContact(form.Schema): form.model('models/contact.xml') alsoProvides(IContact, IContentType) In profiles/default/types/netimpact.contact.xml: <property name="schema">netimpact.content.interfaces.IContact</property> > </property>
  • 28.
    Using that schemato build a form Unusual requirements: • We have multiple forms with different fields, so can't use autoform. • Late binding of the model means we have to defer form field setup. from plone.directives import dexterity from netimpact.content.interfaces import IContact class EditProfileNetworking EditProfileNetworking(dexterity.EditForm): grok.name('edit-networking') label = u'Networking' # avoid autoform functionality def updateFields(self): pass @property def fields(self): return field.Fields(IContact).select('homepage', 'company_homepage', 'twitter', 'linkedin')
  • 29.
  • 30.
  • 31.
  • 32.
    Searching the memberdirectory Requirement: Members get access to a member directory searchable by keyword, chapter, location, job function, issue, industry, or sector. Solution: eea.facetednavigation
  • 34.
    Custom listings formembers Requirement: Members show in listings with custom info (school or company and location). Solution: • Override folder_listing • Make search results use folder_listing
  • 35.
    Synchronizing content withSalesforce.com Requirement: Manage and report on members in Salesforce, present the directory on the web. Solution: Nightly data sync.
  • 36.
  • 37.
    Contact schema withSalesforce metadata <model xmlns="http://namespaces.plone.org/supermodel/schema" xmlns:form="http://namespaces.plone.org/supermodel/form" xmlns:sf="http://namespaces.plone.org/salesforce/schema">> <schema sf:object="Contact" sf:container="/member-directory" sf:criteria="Member_Status__c != null"> > <field name="email" type="zope.schema.ASCIILine" form:validator="netimpact.content.validators.EmailValidator" security:read-permission="cmf.ModifyPortalContent" sf:field="Email"> > <title> <title>E-mail Address</title> </title> </field> </schema> </model> Performs a query like: SELECT Id, Email FROM Contact WHERE Member_Status__c != null
  • 38.
    Extending Dexterity schemas Parameterizedbehavior. • Storage: Schema tagged values • In Python schemas: new grok directives • In XML model: new XML directives in custom namespace • TTW: Custom views to edit the tagged values
  • 39.
    Field with customvalue converter We wanted to convert Salesforce Ids of Chapters into the Plone UUID of corresponding Chapter items: <field name="chapter" type="zope.schema.Choice" form:widget="netimpact.content.browser.widgets.ChapterFieldWidget" sf:field="Chapter__c" sf:converter="uuid"> > <title> <title>Chapter</title> </title> <description></description> <vocabulary> <vocabulary>netimpact.content.Chapters</vocabulary> </vocabulary> <required> <required>True</required> </required> <default> <default>n/a</default> </default> </field>
  • 40.
    Custom value converters Theconverter: from collective.salesforce.behavior.converters import DefaultValueConverter class UUIDConverter UUIDConverter(DefaultValueConverter, grok.Adapter): grok.provides(ISalesforceValueConverter) grok.context(IField) grok.name('uuid') def toSchemaValue(self, value): if value: res = get_catalog().searchResults(sf_object_id=value) if res: return res[0].UID
  • 41.
    Handling collections ofrelated info Education list of dicts in main Contact schema: <field name="education" type="zope.schema.List" form:widget="collective.z3cform.datagridfield.DataGridFieldFactory" sf:relationship="Schools_Attended__r"> > <title> <title>Most Recent School</title> </title> <required> <required>True</required> </required> <min_length> </min_length> <min_length>1</min_length> <value_type type="collective.z3cform.datagridfield.DictRow"> > <schema> <schema>netimpact.content.interfaces.IEducationInfo</schema> </schema> </value_type> </field>
  • 42.
    The subschema IEducationInfo isanother model-based schema: from plone.directives import form class IEducationInfo IEducationInfo(form.Schema): form.model('models/education_info.xml') <model xmlns="http://namespaces.plone.org/supermodel/schema" xmlns:form="http://namespaces.plone.org/supermodel/form" xmlns:sf="http://namespaces.plone.org/salesforce/schema">> <schema sf:object="School_Attended__c" sf:criteria="Organization__c != '' ORDER BY Graduation_Date__c asc NULLS LAST"> > <field name="school_id" type="zope.schema.TextLine" sf:field="Organization__c"> > <title> <title>School ID</title> </title> <required> <required>False</required> </required> </field> </schema> </model> SELECT Id, (SELECT Organization__c FROM School_Attended__c) FROM Contact SELECT
  • 43.
    Writing back toSalesforce Handled less automatically, in response to an ObjectModifiedEvent: @grok.subscribe(IContact, IObjectModifiedEvent) def save_contact_to_salesforce(contact, event): if not IModifiedViaSalesforceSync.providedBy(event): upsertMember(contact)
  • 44.
    Handling payments Requirement: Acceptpayments for: • Several types of membership • Conference registration • Conference expo exhibitors • Chapter dues Solution: groundwire.checkout
  • 45.
  • 46.
    Pieces of GetPaidgroundwire.checkout reuses • Core objects (cart and order storage) • Payment processing code (Authorize.net) • Compatible with getpaid.formgen and pfg.donationform
  • 47.
    What groundwire.checkout provides • Single-page z3c.form-based checkout form with: ◦ cart listing, ◦ credit card info fieldset ◦ billing address fieldset ◦ much, much easier to customize than PloneGetPaid's • Order confirmation view with summary of the completed transaction • Agnostic as to how items get added to the cart; only handles checkout • API for performing actions after an item is purchased
  • 48.
    Basic example Add anitem to the cart and redirect to checkout: from getpaid.core.item import PayableLineItem from groundwire.checkout.utils import get_cart from groundwire.checkout.utils import redirect_to_checkout item = PayableLineItem() item.item_id = 'item' item.name = 'My Item' item.cost = float(5) item.quantity = 1 cart = get_cart() if 'item' in cart: del cart['item'] cart['item'] = item redirect_to_checkout()
  • 49.
    Performing actions afterpurchase Custom item classes can perform their own actions: from getpaid.core.item import PayableLineItem class MyLineItem MyLineItem(PayableLineItem): def after_charged(self): print 'charged!'
  • 50.
    Pricing Products are managedin Salesforce. But we need to determine the constituency (and thus the price) in Plone.
  • 51.
  • 52.
    Discounts • Auto-applyvs. coded discounts
  • 53.
    Mixed theming approach • Diazo without a theme <theme if-content="false()" href="theme.html" /> <!-- Add the site slogan after the logo (example rule with XSLT) --> <replace css:content="#portal-logo"> > <xsl:copy-of css:select="#portal-logo" /> <p id="portal-slogan">Where good works.</p> > </p> </replace> • z3c.jbot to make changes to templates
  • 54.
    Edit bar attop <replace css:content="#visual-portal-wrapper"> > <xsl:copy-of css:select="#edit-bar" /> <div id="visual-portal-wrapper">> <xsl:apply-templates /> </div> </replace> <replace css:content="#edit-bar" />
  • 55.
    Tile-based layout <div class="tile-placeholder" tal:attributes="data-tile-href string:${portal_url}/ @@groundwire.tiles.richtext/login-newmember-features" />
  • 56.
  • 57.
    What Plone coulddo • Rewrite the password reset tool • Better support for multiple levels of membership • Easier way to customize a type's listing view • Asynchronous processing infrastructure • Built-in support for tiles
  • 58.
    What Dexterity coulddo • Make it possible to parameterize widgets and validators in the model • Better way to make multiple forms based on the same schema • Expand the through-the-web editor
  • 59.
    What Plone givesfor free (or cheap) Plone was absolutely the right tool for the job. • Basic content management • Custom form creation using PloneFormGen • Fine-grained access control • Collections • Basic content types
  • 60.
  • 61.
    Contact me David Glick dglick@groundwireconsulting.com Groundwire Consulting http://groundwireconsulting.com
  • 62.