Buildout
========

$ mkdir tdd-dojo
$ curl -O http://python-distribute.org/bootstrap.py

buildout.cfg::

    [buildout]
    parts = instance
    extends = http://dist.plone.org/release/4.2.3/versions.cfg

    [instance]
    recipe = plone.recipe.zope2instance
    http-address = 8080
    user = admin:admin
    eggs =
        Plone
        templer.core
        templer.plone

$ python bootstrap.py
$ bin/buildout


Package
=======

$ bin/templer plone_basic
Register Profile (Should this package register a GS Profile) [False]: True

$ mkdir src
$ mv collective.ploneboard src

buildout.cfg::

    [buildout]
    parts = instance test
    develop = src/collective.ploneboard

    eggs =
        ...
        collective.ploneboard [test]

    [test]
    recipe = zc.recipe.testrunner
    defaults = ['--auto-color', '--auto-progress']
    eggs = ${instance:eggs}

Test:

    def test_product_is_installed(self):
        """ Validate that our products GS profile has been run and the product
            installed
        """
        pid = 'collective.ploneboard'
        installed = [p['id'] for p in self.qi_tool.listInstalledProducts()]
        self.assertTrue(pid in installed,
                        'package appears not to have been installed')

Run Tests::

    $ bin/test -s collective.ploneboard

- add travis-ci conf
- configure travis-ci
- run travis-ci

User Stories
============

- "As an administrator I can add a message board."
- "As an administrator I can add a topic to a message board."
- "As an administrator I can add a conversation to a topic."
- "As a member I can add a reply to a conversation"


Test Setup
==========

TEST
----

test_setup.py::

    def test_dexterity_is_installed(self):
        pid = 'plone.app.dexterity'
        installed = [p['id'] for p in self.qi_tool.listInstalledProducts()]
        self.assertTrue(
            pid in installed,
            'package %s appears not to have been installed' % pid
        )

FAILURE
-------

    $ bin/test -s collective.ploneboard

Output::

    AssertionError: False is not True : package plone.app.dexterity appears not to have been installed

FIX
---

setup.py::

      install_requires=[
          'setuptools',
          'plone.app.dexterity',
      ],

=> run buildout and test => FAILURE

profiles/default/metadata.xml::

    <?xml version="1.0"?>
    <metadata>
      <version>0001</version>
      <dependencies>
        <dependency>profile-plone.app.dexterity:default</dependency>
      </dependencies>
    </metadata>

=> KEY ERROR

configure.zcml::

    <includeDependencies package="." />

=> GREEN


Message Board Content Type
==========================

"As an administrator I can add a message board."

test_messageboard.py::

    from zope.component import createObject
    from zope.component import queryUtility
    from plone.dexterity.interfaces import IDexterityFTI
    import unittest

    from plone.app.testing import setRoles
    from plone.app.testing import TEST_USER_ID

    from collective.ploneboard.testing import \
        COLLECTIVE_PLONEBOARD_INTEGRATION_TESTING


    class MessageBoardIntegrationTest(unittest.TestCase):

        layer = COLLECTIVE_PLONEBOARD_INTEGRATION_TESTING

        def setUp(self):
            self.portal = self.layer['portal']
            self.request = self.layer['request']
            self.request['ACTUAL_URL'] = self.portal.absolute_url()
            setRoles(self.portal, TEST_USER_ID, ['Manager'])

        def test_fti(self):
            from collective.ploneboard.interfaces import IMessageboard
            fti = queryUtility(
                IDexterityFTI,
                name='messageboard'
            )

            self.assertTrue(
                fti,
                'Factory Type Information for messageboard can not be looked up.'
            )

RED
---

    $ bin/test -t test_messageboard

=> RED: ImportError: No module named interfaces

interfaces.py::

    from zope.interface import Interface


    class IMessageboard(Interface):
        """
        """

=> GREEN

profile/default/types.xml::

    <?xml version="1.0"?>
    <object meta_type="Plone Types Tool" name="portal_types">
      <object meta_type="Dexterity FTI" name="messageboard" />
    </object>

$ mkdir profiles/default/types

profiles/default/types/messageboard.xml::

    <?xml version="1.0"?>
    <object name="messageboard" meta_type="Dexterity FTI"
        xmlns:i18n="http://xml.zope.org/namespaces/i18n"
        i18n:domain="collective.ploneboard">

      <!-- Basic properties -->
      <property name="title" i18n:translate="">Message Board</property>
      <property name="description" i18n:translate=""></property>
      <property name="content_icon">++resource++collective.ploneboard/images/ploneboard.gif</property>

      <property name="allow_discussion">False</property>

      <!-- Hierarchy control -->
      <property name="global_allow">True</property>
      <property name="filter_content_types">True</property>
      <property name="allowed_content_types">
        <element value="topic" />
      </property>

      <!-- Schema, class and security -->
      <property name="schema">collective.ploneboard.interfaces.IMessageboard</property>
      <property name="klass">plone.dexterity.content.Container</property>
      <property name="add_permission">ploneboard.AddMessageboard</property>

      <!-- enabled behaviors -->
      <property name="behaviors">
        <element value="plone.app.dexterity.behaviors.metadata.IDublinCore" />
        <element value="plone.app.dexterity.behaviors.exclfromnav.IExcludeFromNavigation" />
        <element value="plone.app.content.interfaces.INameFromTitle" />
      </property>

      <!-- Views and aliases -->
      <property name="default_view">view</property>
      <property name="default_view_fallback">False</property>
      <property name="view_methods">
        <element value="view"/>
      </property>

      <alias from="(Default)" to="(selected layout)"/>
      <alias from="edit" to="@@edit"/>
      <alias from="sharing" to="@@sharing"/>
      <alias from="view" to="@@view"/>

      <action title="View" action_id="view" category="object" condition_expr=""
              url_expr="string:${folder_url}/" visible="True">
        <permission value="View"/>
      </action>
      <action title="Edit" action_id="edit" category="object" condition_expr=""
              url_expr="string:${object_url}/edit" visible="True">
        <permission value="Modify portal content"/>
      </action>

    </object>


Further tests::

        def test_fti(self):
            from collective.ploneboard.interfaces import IMessageboard
            fti = queryUtility(
                IDexterityFTI,
                name='messageboard'
            )
            self.assertTrue(
                fti,
                'Factory Type Information for messageboard can not be looked up.'
            )
            self.assertEqual(fti.lookupSchema(), IMessageboard)

        def test_factory(self):
            from collective.ploneboard.interfaces import IMessageboard
            fti = queryUtility(
                IDexterityFTI,
                name='messageboard'
            )
            factory = fti.factory
            new_object = createObject(factory)
            self.failUnless(IMessageboard.providedBy(new_object))

        def test_adding(self):
            from collective.ploneboard.interfaces import IMessageboard
            self.portal.invokeFactory(
                'messageboard',
                'board')
            board = self.portal['board']
            self.failUnless(IMessageboard.providedBy(board))

RED::

    raise AccessControl_Unauthorized('Cannot create %s' % self.getId())
    Unauthorized: Cannot create messageboard

configure.zcml::

  <include file="permissions.zcml" />

permissions.zcml::

  <configure
      xmlns="http://namespaces.zope.org/zope"
      xmlns:i18n="http://namespaces.zope.org/i18n"
      i18n_domain="collective.ploneboard">

    <permission
        id="ploneboard.AddMessageboard"
        title="Collective Ploneboard: Add Message Board"
        />

  </configure>


FUNCTIONAL TESTS
----------------

    # -*- coding: utf-8 -*-
    from plone.app.testing import SITE_OWNER_NAME
    from plone.app.testing import SITE_OWNER_PASSWORD
    from plone.testing.z2 import Browser
    import unittest

    from collective.ploneboard.testing import \
        COLLECTIVE_PLONEBOARD_INTEGRATION_TESTING


    class PloneboardContenttypesIntegrationTest(unittest.TestCase):

        layer = COLLECTIVE_PLONEBOARD_INTEGRATION_TESTING

        def setUp(self):
            app = self.layer['app']
            self.portal = self.layer['portal']
            self.request = self.layer['request']
            self.portal_url = self.portal.absolute_url()
            self.browser = Browser(app)
            self.browser.handleErrors = False
            self.browser.addHeader(
                'Authorization',
                'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD),
            )

        def test_add_ploneboard(self):
            """As an administrator I can add a message board.
            """
            self.browser.open(self.portal_url)
            self.browser.getLink('Message Board').click()
            self.browser.getControl(
                name='form.widgets.IBasic.title'
            ).value = "My Message Board"
            self.browser.getControl(
                name='form.widgets.IBasic.description'
            ).value = "Lorem ipsum"
            self.browser.getControl("Save").click()
            self.assertTrue("My Message Board" in self.browser.contents)
            self.assertTrue("Lorem ipsum" in self.browser.contents)


Topic Content Type
==================

"As an administrator I can add a topic."

- Copy messageboard.

"As an administrator I can add a topic to a message board."

- Set "Global allow" to false in the FTI
=> failing test

    raise ValueError('Disallowed subobject type: %s' % type_name)
    ValueError: Disallowed subobject type: topic

Skip test_adding in test_topic.py::

    @unittest.skip("demonstrating skipping")
    def test_adding(self):
        self.portal.invokeFactory(
            'topic',
            'mytopic')
        new_object = self.portal['mytopic']
        self.failUnless(ITopic.providedBy(new_object))

Create new test::

    def test_global_allow_not_allowed(self):
        self.assertRaises(
            ValueError,
            self.portal.invokeFactory,
            'topic',
            'my-topic',
        )

Remove decorator from test_adding => failing test.

Fix test_adding::


    def test_adding(self):
        self.portal.invokeFactory(
            'messageboard',
            'board'
        )
        self.portal.board.invokeFactory(
            'topic',
            'mytopic'
        )
        obj = self.portal.board['mytopic']
        self.failUnless(ITopic.providedBy(obj))


Conversation Content Type
=========================

=> Copy topic.

test_conversation.py::

    def test_conversation_enabled(self):
        from plone.app.discussion.interfaces import IDiscussionLayer
        from zope.interface import alsoProvides
        alsoProvides(
            self.portal.REQUEST,
            IDiscussionLayer
        )

        self.portal.invokeFactory(
            'messageboard',
            'board'
        )
        self.portal.board.invokeFactory(
            'topic',
            'topic'
        )
        self.portal.board.topic.invokeFactory(
            'conversation',
            'conversation'
        )
        obj = self.portal.board.topic['conversation']
        conv = obj.restrictedTraverse('@@conversation_view')
        self.assertTrue(conv.enabled())


test_integration.py::

        # Add reply
        self.browser.getControl(
            name='form.widgets.text'
        ).value = "This is my first reply."
        self.browser.getControl(name="form.buttons.comment").click()
        self.assertTrue("This is my first reply" in self.browser.contents)


Discussion for conversation type.
---------------------------------

Allow globally - test_setup.py::

    def test_discussion_is_globally_allowed(self):
        from zope.component import queryUtility
        from plone.registry.interfaces import IRegistry
        from plone.app.discussion.interfaces import IDiscussionSettings
        registry = queryUtility(IRegistry)
        settings = registry.forInterface(IDiscussionSettings)
        self.assertEqual(settings.globally_enabled, True)

profiles/default/registry.xml::

    <registry>
      <record
        field="globally_enabled"
        interface="plone.app.discussion.interfaces.IDiscussionSettings"
        name="plone.app.discussion.interfaces.IDiscussionSettings.globally_enabled">
        <value>True</value>
      </record>
    </registry>

Allow for conversation content type - conversation.xml::

  <property name="allow_discussion">True</property>


Messageboard View
-----------------

"As a member I can view the latest conversation on the message board grouped by topics."

test_messageboard.py::

    def test_conversations_method(self):
        self.portal.board.invokeFactory('topic', 'topic1')
        self.portal.board.invokeFactory('topic', 'topic2')
        from collective.ploneboard.browser.messageboard import MessageboardView

        view = MessageboardView(self.portal.board, self.request)

        self.assertEqual(len(view.conversations()), 2)

Test failure::

    AttributeError: 'MessageboardView' object has no attribute 'conversations'

browser/messageboard.py MessageboardView::

    def conversations(self):
        return ['foo', 'bar']

=> Test green.

=> Refactor

test::
        self.assertEqual(
            [x['title'] for x in conversations],
            ['Topic 1', 'Topic 2']
        )

code::
        return [
            {'title': 'Topic 1', 'conversations': []},
            {'title': 'Topic 2', 'conversations': []},
        ]

code refactor::

    def conversations(self):
        output = []
        for topic_id in self.context.objectIds():
            topic = self.context[topic_id]
            topic.title
            output.append({'title': topic.title, 'conversations': []})
        return output

test_conversations::

    def test_conversation_returns_conversations(self):
        self.portal.board.invokeFactory('topic', id='topic1', title='Topic 1')
        self.portal.board.topic1.invokeFactory(
            'conversation',
            id='conv1',
            title='Conversation 1'
        )
        self.portal.board.topic1.invokeFactory(
            'conversation',
            id='conv2',
            title='Conversation 2'
        )
        from collective.ploneboard.browser.messageboard import MessageboardView
        view = MessageboardView(self.portal.board, self.request)

        conversations = view.conversations()

        self.assertEqual(len(conversations), 1)
        self.assertEqual(
            conversations[0]['conversations'],
            []
        )


REFACTOR: conversations method => topics method::

    <div tal:repeat="topic view/topics">
      <h2 tal:content="topic/title"></h2>
      <div tal:repeat="conversation topic/conversations">
        <a tal:content="conversation/title"
           tal:attributes="href conversation/url">Conversation</a>
      </div>
    </div>

=> does not make sense with tal:repeat="topics view/topics"

=> refactor with confidence

TODO templer:

- remove initialize function
- remove testing profile
