Addon for PloneFormGen providing Silverpop integration
Project description
collective.pfg.silverpop
========================
Addon for PloneFormGen providing Integration
of Silverpop enterprise newslettering.
Adds a FormSilverpopAdapter which can be used
to create newsletter signup forms to add a recipient
to a Silverpop Newsletter.
Supports Simple Opt-In /Opt-Out,
meaning that a user can sign in to a newletter,
by checking a boolean field,
and opt-out by unchecking the boolean field.
Re Opt-In is not implemented yet.
- PloneFormGen: http://pypi.python.org/pypi/Products.PloneFormGen
- Silverpop: http://www.silverpop.com/
- Code repository: http://svn.plone.org/svn/collective/collective.pfg.silverpop/
Changelog
*********
0.5 (2009-05-11)
----------------
- add support for opt-in/opt-out functionality
- define new policy: if the form contains a field with id
'silverpop_opt_in' we use this as control for opt in.
If the field is True , the user is added to the newsletter.
If it is False, the user will be opted our from the
newsletter (e.g. usage boolean field checkbox checked=1, unchecked=0)
[hplocher]
- interpret xml response of silverpop [hplocher]
- refactor test to create pretty xml output [hplocher]
0.4 (2009-05-06)
----------------
- remove workflow for FormSilverpopAdapter [hplocher]
- add functionality to define a custom 'field_id' ->
'silverpop_column_name' mapping [hplocher]
- add a Mapping grid to the FormSilverPopAdapter:
(id(readonly), title(readonly), silverpop api key)
for configuring the mapping [hplocher]
- requires DataGridField [hplocher]
0.3 (2009-04-08)
----------------
- New policy: filter data fields by prefix. We're only using field names which
start with COLUMN_NAME_PREFIX ("silverpop_"). This saves us from having field
names which clash with plone IDs. Additionally, we've defined a mapping
table for column names which are required verbatim as of the SilverPop API --
COLUMN_MAPPING.
[seletz]
- Removed CONFIRMATION logic -- this can be handled better in PFG.
[seletz]
0.2 (2009-04-08)
----------------
- add unicode support,
fixes #1 [Hans-Peter Locher]
0.1 (2009-04-02)
----------------
- Initial release
[Hans-Peter Locher]
Introduction
************
This test shows how a PFG Form folder is added. We
also add our custom **FormSilverpopAdapter**, configure
it and show how the actual sent XML looks.
Setup
-----
First, we must perform some setup. We use the testbrowser that is shipped
with Five, as this provides proper Zope 2 integration. Most of the
documentation, though, is in the underlying zope.testbrower package.
>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> portal_url = self.portal.absolute_url()
The following is useful when writing and debugging testbrowser tests. It lets
us see all error messages in the error_log.
>>> self.portal.error_log._ignored_exceptions = ()
With that in place, we can go to the portal front page and log in. We will
do this using the default user from PloneTestCase:
>>> from Products.PloneTestCase.setup import portal_owner, default_password
>>> browser.open(portal_url)
We have the login portlet, so let's use that.
>>> browser.getControl(name='__ac_name').value = portal_owner
>>> browser.getControl(name='__ac_password').value = default_password
>>> browser.getControl(name='submit').click()
Here, we set the value of the fields on the login form and then simulate a
submit click.
We also set the roles we want to have::
>>> self.setRoles(['Member', 'Manager'])
We monkeypatch urllib to prohibit making requests to silverpop and get test output
(url, headers, data).
We create a Fake class to be returned by urlib2.urlopen::
>>> class Fake(object):
... def read(self): return "<success>true</success>"
In our test method, we print request's url, headers, data (we decode
the urlencoded data for the test) and
return a Fake object::
>>> import cgi
>>> def test_urlopen(req):
... print req.get_full_url()
... print req.headers
... xml = dict(cgi.parse_qsl(req.data))['xml']
... print xml
... return Fake()
>>> import urllib2
Finally we patch urllib2.urlopen::
>>> urllib2.urlopen = test_urlopen
We also define a FakeRequest class to define our request
containing just a form::
>>> class FakeRequest(dict):
... def __init__(self, **kwargs):
... self.form = kwargs
Adding content
--------------
Add a new Form Folder::
>>> browser.getLink('Form Folder').click()
>>> browser.getControl('Title').value = 'testform'
>>> browser.getControl('Save').click()
>>> 'testform' in browser.contents
True
Go to the new Form Folder::
>>> browser.getLink('testform').click()
We use the 'Add new' menu to add a new content item::
>>> browser.getLink('Add new').click()
Then we select the type of item we want to add. In this case we select
'FormSilverpopAdapter' and click the 'Add' button to get to the add form::
>>> browser.getControl('FormSilverpopAdapter').click()
>>> browser.getControl(name='form.button.Add').click()
>>> 'FormSilverpopAdapter' in browser.contents
True
Now we fill the form and submit it::
>>> browser.getControl(name='title').value = 'testadapter'
>>> browser.getControl('Silverpop API URL').value = 'http://url.com'
>>> browser.getControl('Silverpop List Id').value = '1'
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
True
We added a new 'FormSilverpopAdapter' content item to the testform.
Field Name Policy
-----------------
We enforce the following policies regarding the field names which we send to
SilverPop via their API:
- field names MUST start with a common perfix: "silverpop\_"
- there must be one field "silverpop_email"
- there can be an additional field "silverpop_opt_in"
to control opt-in/opt-out
We have a transformation function which does that::
>>> from collective.pfg.silverpop.utilities import transform_column_name
>>> transform_column_name("silverpop_foo")
'foo'
>>> transform_column_name("no_prefix") is None
True
onSuccess
---------
On submit of the form, the onSuccess method of
our FormSilverpopAdapter will be called.
We want to access our testform and testadapter directly::
>>> self.testform = self.portal.testform
>>> self.testadapter = self.portal.testform.testadapter
We create some fields inside our form.
First, we create fields which should be regarded by our 'FormSilverpopAdapter'.
The special 'sivlerpop_email' field (this field has a fixed mapping)::
>>> self.testform.invokeFactory('FormStringField', 'silverpop_email', title='Email')
'silverpop_email'
A field to insert a name::
>>> self.testform.invokeFactory('FormStringField', 'silverpop_name', title='Name')
'silverpop_name'
We also create a misc field, which shouldn't be regarded, although
it is in the same form::
>>> self.testform.invokeFactory('FormStringField', 'credits_to_admin', title='Give your credits to the admin of the site')
'credits_to_admin'
USER DEFINED MAPPING
++++++++++++++++++++
We offer the ability, to define a mapping from
field ids to Silverpop API Keys.
NO MAPPING
..........
First, we check what happens when we don't change the mapping.
We go to the 'FormSilverpopAdapter' s edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
The current mapping should only contain one record
(with columns id, title, silverpop api key),
so we check all columns.
id::
>>> browser.getControl(name='mapping.id:records').value
'silverpop_name'
title::
>>> browser.getControl(name='mapping.title:records').value
'Name'
silverpop api key::
>>> browser.getControl(name='mapping.silverpop api key:records', index=0).value
''
We set up the list of fields::
>>> fields = [self.testform.silverpop_email, self.testform.silverpop_name, self.testform.credits_to_admin,]
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', credits_to_admin='I like the high availability')
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>name</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
CUSTOM MAPPING
..............
Now, we check what happens when we change
the mapping.
We want to use 'FIRST NAME' as COLUMN
for silverpop, for the 'silverpop_name'
field.
We go to the 'FormSilverpopAdapter' s edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
We change the value inside the mapping::
>>> browser.getControl(name='mapping.silverpop api key:records', index=0).value = 'FIRST NAME'
>>> browser.getControl('Save').click()
The list of fields, is still the same.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', credits_to_admin='I like the high availability')
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>FIRST NAME</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
OPT-IN/OPT-OUT
++++++++++++++
We create a boolean field with the special id 'silverpop_opt_in'
in our form::
>>> self.testform.invokeFactory('FormBooleanField', 'silverpop_opt_in', title='Yes I want to get the newsletter')
'silverpop_opt_in'
This field mustn't occur in the testadapter's mapping grid.
We go to the 'FormSilverpopAdapter's edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
The current mapping should still only contain one record, so
we check the id column::
>>> browser.getControl(name='mapping.id:records').value
'silverpop_name'
OPT-IN
......
The user wants to get the newsletter. A checked boolean field
will lead to a 'True' in the request.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', silverpop_opt_in='True')
The list of fields now looks as follows::
>>> fields = [self.testform.silverpop_email, self.testform.silverpop_name, self.testform.silverpop_opt_in]
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>FIRST NAME</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
OPT-OUT
.......
The user doesn't want to get the newsletter, or
doesn't want it anymore. An unchecked boolean field
will lead to a 'False' in the request.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', silverpop_opt_in='False')
The list of fields is still the same.
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<OptOutRecipient>
<LIST_ID>1</LIST_ID>
<EMAIL>x@x.com</EMAIL>
</OptOutRecipient>
</Body>
</Envelope>
Contributors
************
Hans-Peter Locher, Author
Stefan Eletzhofer
Download
********
========================
Addon for PloneFormGen providing Integration
of Silverpop enterprise newslettering.
Adds a FormSilverpopAdapter which can be used
to create newsletter signup forms to add a recipient
to a Silverpop Newsletter.
Supports Simple Opt-In /Opt-Out,
meaning that a user can sign in to a newletter,
by checking a boolean field,
and opt-out by unchecking the boolean field.
Re Opt-In is not implemented yet.
- PloneFormGen: http://pypi.python.org/pypi/Products.PloneFormGen
- Silverpop: http://www.silverpop.com/
- Code repository: http://svn.plone.org/svn/collective/collective.pfg.silverpop/
Changelog
*********
0.5 (2009-05-11)
----------------
- add support for opt-in/opt-out functionality
- define new policy: if the form contains a field with id
'silverpop_opt_in' we use this as control for opt in.
If the field is True , the user is added to the newsletter.
If it is False, the user will be opted our from the
newsletter (e.g. usage boolean field checkbox checked=1, unchecked=0)
[hplocher]
- interpret xml response of silverpop [hplocher]
- refactor test to create pretty xml output [hplocher]
0.4 (2009-05-06)
----------------
- remove workflow for FormSilverpopAdapter [hplocher]
- add functionality to define a custom 'field_id' ->
'silverpop_column_name' mapping [hplocher]
- add a Mapping grid to the FormSilverPopAdapter:
(id(readonly), title(readonly), silverpop api key)
for configuring the mapping [hplocher]
- requires DataGridField [hplocher]
0.3 (2009-04-08)
----------------
- New policy: filter data fields by prefix. We're only using field names which
start with COLUMN_NAME_PREFIX ("silverpop_"). This saves us from having field
names which clash with plone IDs. Additionally, we've defined a mapping
table for column names which are required verbatim as of the SilverPop API --
COLUMN_MAPPING.
[seletz]
- Removed CONFIRMATION logic -- this can be handled better in PFG.
[seletz]
0.2 (2009-04-08)
----------------
- add unicode support,
fixes #1 [Hans-Peter Locher]
0.1 (2009-04-02)
----------------
- Initial release
[Hans-Peter Locher]
Introduction
************
This test shows how a PFG Form folder is added. We
also add our custom **FormSilverpopAdapter**, configure
it and show how the actual sent XML looks.
Setup
-----
First, we must perform some setup. We use the testbrowser that is shipped
with Five, as this provides proper Zope 2 integration. Most of the
documentation, though, is in the underlying zope.testbrower package.
>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> portal_url = self.portal.absolute_url()
The following is useful when writing and debugging testbrowser tests. It lets
us see all error messages in the error_log.
>>> self.portal.error_log._ignored_exceptions = ()
With that in place, we can go to the portal front page and log in. We will
do this using the default user from PloneTestCase:
>>> from Products.PloneTestCase.setup import portal_owner, default_password
>>> browser.open(portal_url)
We have the login portlet, so let's use that.
>>> browser.getControl(name='__ac_name').value = portal_owner
>>> browser.getControl(name='__ac_password').value = default_password
>>> browser.getControl(name='submit').click()
Here, we set the value of the fields on the login form and then simulate a
submit click.
We also set the roles we want to have::
>>> self.setRoles(['Member', 'Manager'])
We monkeypatch urllib to prohibit making requests to silverpop and get test output
(url, headers, data).
We create a Fake class to be returned by urlib2.urlopen::
>>> class Fake(object):
... def read(self): return "<success>true</success>"
In our test method, we print request's url, headers, data (we decode
the urlencoded data for the test) and
return a Fake object::
>>> import cgi
>>> def test_urlopen(req):
... print req.get_full_url()
... print req.headers
... xml = dict(cgi.parse_qsl(req.data))['xml']
... print xml
... return Fake()
>>> import urllib2
Finally we patch urllib2.urlopen::
>>> urllib2.urlopen = test_urlopen
We also define a FakeRequest class to define our request
containing just a form::
>>> class FakeRequest(dict):
... def __init__(self, **kwargs):
... self.form = kwargs
Adding content
--------------
Add a new Form Folder::
>>> browser.getLink('Form Folder').click()
>>> browser.getControl('Title').value = 'testform'
>>> browser.getControl('Save').click()
>>> 'testform' in browser.contents
True
Go to the new Form Folder::
>>> browser.getLink('testform').click()
We use the 'Add new' menu to add a new content item::
>>> browser.getLink('Add new').click()
Then we select the type of item we want to add. In this case we select
'FormSilverpopAdapter' and click the 'Add' button to get to the add form::
>>> browser.getControl('FormSilverpopAdapter').click()
>>> browser.getControl(name='form.button.Add').click()
>>> 'FormSilverpopAdapter' in browser.contents
True
Now we fill the form and submit it::
>>> browser.getControl(name='title').value = 'testadapter'
>>> browser.getControl('Silverpop API URL').value = 'http://url.com'
>>> browser.getControl('Silverpop List Id').value = '1'
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
True
We added a new 'FormSilverpopAdapter' content item to the testform.
Field Name Policy
-----------------
We enforce the following policies regarding the field names which we send to
SilverPop via their API:
- field names MUST start with a common perfix: "silverpop\_"
- there must be one field "silverpop_email"
- there can be an additional field "silverpop_opt_in"
to control opt-in/opt-out
We have a transformation function which does that::
>>> from collective.pfg.silverpop.utilities import transform_column_name
>>> transform_column_name("silverpop_foo")
'foo'
>>> transform_column_name("no_prefix") is None
True
onSuccess
---------
On submit of the form, the onSuccess method of
our FormSilverpopAdapter will be called.
We want to access our testform and testadapter directly::
>>> self.testform = self.portal.testform
>>> self.testadapter = self.portal.testform.testadapter
We create some fields inside our form.
First, we create fields which should be regarded by our 'FormSilverpopAdapter'.
The special 'sivlerpop_email' field (this field has a fixed mapping)::
>>> self.testform.invokeFactory('FormStringField', 'silverpop_email', title='Email')
'silverpop_email'
A field to insert a name::
>>> self.testform.invokeFactory('FormStringField', 'silverpop_name', title='Name')
'silverpop_name'
We also create a misc field, which shouldn't be regarded, although
it is in the same form::
>>> self.testform.invokeFactory('FormStringField', 'credits_to_admin', title='Give your credits to the admin of the site')
'credits_to_admin'
USER DEFINED MAPPING
++++++++++++++++++++
We offer the ability, to define a mapping from
field ids to Silverpop API Keys.
NO MAPPING
..........
First, we check what happens when we don't change the mapping.
We go to the 'FormSilverpopAdapter' s edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
The current mapping should only contain one record
(with columns id, title, silverpop api key),
so we check all columns.
id::
>>> browser.getControl(name='mapping.id:records').value
'silverpop_name'
title::
>>> browser.getControl(name='mapping.title:records').value
'Name'
silverpop api key::
>>> browser.getControl(name='mapping.silverpop api key:records', index=0).value
''
We set up the list of fields::
>>> fields = [self.testform.silverpop_email, self.testform.silverpop_name, self.testform.credits_to_admin,]
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', credits_to_admin='I like the high availability')
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>name</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
CUSTOM MAPPING
..............
Now, we check what happens when we change
the mapping.
We want to use 'FIRST NAME' as COLUMN
for silverpop, for the 'silverpop_name'
field.
We go to the 'FormSilverpopAdapter' s edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
We change the value inside the mapping::
>>> browser.getControl(name='mapping.silverpop api key:records', index=0).value = 'FIRST NAME'
>>> browser.getControl('Save').click()
The list of fields, is still the same.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', credits_to_admin='I like the high availability')
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>FIRST NAME</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
OPT-IN/OPT-OUT
++++++++++++++
We create a boolean field with the special id 'silverpop_opt_in'
in our form::
>>> self.testform.invokeFactory('FormBooleanField', 'silverpop_opt_in', title='Yes I want to get the newsletter')
'silverpop_opt_in'
This field mustn't occur in the testadapter's mapping grid.
We go to the 'FormSilverpopAdapter's edit form::
>>> browser.open(portal_url+'/testform/testadapter/edit')
The current mapping should still only contain one record, so
we check the id column::
>>> browser.getControl(name='mapping.id:records').value
'silverpop_name'
OPT-IN
......
The user wants to get the newsletter. A checked boolean field
will lead to a 'True' in the request.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', silverpop_opt_in='True')
The list of fields now looks as follows::
>>> fields = [self.testform.silverpop_email, self.testform.silverpop_name, self.testform.silverpop_opt_in]
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<AddRecipient>
<LIST_ID>1</LIST_ID>
<CREATED_FROM>2</CREATED_FROM>
<UPDATE_IF_FOUND>true</UPDATE_IF_FOUND>
<COLUMN>
<NAME>EMAIL</NAME>
<VALUE>x@x.com</VALUE>
</COLUMN>
<COLUMN>
<NAME>FIRST NAME</NAME>
<VALUE>Hans</VALUE>
</COLUMN>
</AddRecipient>
</Body>
</Envelope>
OPT-OUT
.......
The user doesn't want to get the newsletter, or
doesn't want it anymore. An unchecked boolean field
will lead to a 'False' in the request.
We set up a minimal request, containing the user's input::
>>> request = FakeRequest(silverpop_email='x@x.com', silverpop_name='Hans', silverpop_opt_in='False')
The list of fields is still the same.
We now call the adapter's onSuccess method::
>>> self.testadapter.onSuccess(fields,request)
http://url.com
{'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'}
<Envelope>
<Body>
<OptOutRecipient>
<LIST_ID>1</LIST_ID>
<EMAIL>x@x.com</EMAIL>
</OptOutRecipient>
</Body>
</Envelope>
Contributors
************
Hans-Peter Locher, Author
Stefan Eletzhofer
Download
********
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
collective.pfg.silverpop-0.5.zip
(45.0 kB
view hashes)
Close
Hashes for collective.pfg.silverpop-0.5.zip
Algorithm | Hash digest | |
---|---|---|
SHA256 | f57aa216e476eda4c47d9a2e51b6f6021126f033f63e702d4624b526aca2743f |
|
MD5 | 99fa288736bb86dcb17ed10ad7cf6f5e |
|
BLAKE2b-256 | f6d0ef1079f939608935441d7d1e906f6da1d69b101a299607d92f9673607d91 |