===================================================
Python-Integration of Commendations into SchoolTool
===================================================

Now that we have developed the basic commendation component, we have to hook
up the commendations to the other SchoolTool objects. In his requirement
E-mail, Tom wrote specifically:

  The overall idea is to allow teachers, clerks and administrators to
  enter a commendation for a person or group (including sections).

The task of this section will be to attach any amount of commendations to
persons, groups and sections.


The ``Commendations`` container
-------------------------------

Since we have to be able to associate multiple commendations with a particular
SchoolTool component, it is necessary to implement a commendations
container. This is a common pattern in SchoolTool; in other words nearly every
content component comes with a special container. The first step is to create
a container interface that only allows commendations to be added; see
``interfaces.py``.

We also need to tell the ``Commendation`` class that it can only be contained
by components implementing ``ICommendations``, which is done using the
interface ``ICommendationContained``. Thus we now ensure that not only the
container can only contain commendations, but also that the commendations can
only be added to the commendations container. I will demonstrate the
constraints, once we have implemented our two new interfaces [#constraints]_.

The ``ICommendationContained`` interface extends ``IContained``, which ensures
that the component has a parent and name, allowing instances
of the component to be located and their paths (including URLs) to be
computed. This interface is simply added to the list of interfaces implemented
by the``Commendation`` class. Also, as you can see in the code, there exists a
nice base class called ``Contained`` that implements the ``IContained``
interface. Now it should not be possible anymore to add commendations to
arbitrary containers, such as a regular folder:

  >>> from schooltool.commendation import commendation
  >>> goodjob = commendation.Commendation(
  ...     u'Good Job!',
  ...     u'You have done a good job with the latest assignment.',
  ...     u'school-wide')

  >>> from zope.site.folder import Folder
  >>> from zope.container import constraints
  >>> constraints.checkObject(Folder(), u'goodjob', goodjob)
  Traceback (most recent call last):
  ...
  InvalidContainerType: (<zope.site.folder.Folder ...>,
                         (<InterfaceClass ...ICommendations>,))

The ``ICommendations`` interface also needs an implementation, since the
``IContainer`` interface specifies many methods. Luckily there is a great base
class called ``BTreeContainer`` that we can use, so that our implementation of
the interface becomes trivial as you can see in ``commendation.py``. This base
class implements the full ``IContainer`` interface using the ZODB's ``BTree``
package, which is particularly efficient for large amounts of items. Once a an
instance of the container is created,

  >>> myCommendations = commendation.Commendations()

you can add commendations

  >>> constraints.checkObject(myCommendations, u'goodjob', goodjob)

but not arbitrary objects:

  >>> constraints.checkObject(myCommendations, u'welldone', object())
  Traceback (most recent call last):
  ...
  InvalidItemType:
      (<Commendations for None>, <object ...>,
       (<InterfaceClass schooltool.commendation.interfaces.ICommendation>,))

Again, this is a very common development pattern in SchoolTool for any content
component. In the next section I will show you how to connect the
commendations to other components.

.. [#constraints] For the implementation of constraints, which contains
   several docstring-based doctests, see ``zope.container.constraints``.


Associating Commendations with SchoolTool Components
----------------------------------------------------

To connect our commendations to another component, we are using a feature of
Zope 3 called annotations. Annotations are data connected with a particular
object. In other words, annotations allow us to associate any data structure
with a particular object. The data structure itself is known as one
annotation, and an adapter (``IAnnotations``) is used to manage all
annotations of an object.

Annotations were developed to help components integrate into a system, such as
SchoolTool. For example, the core component "Person" only knows about a title
(full name), username, password, and the photo. But in order to be fully
functional in SchoolTool, we also need to know demographic information,
maintanance information (Dublin Core), and even store workflows of the
person. All this data is implemented using annotations, which are inturn
accessed by various adapters. Thus, annotations are essential to the
SchoolTool application and a crucial part of the framework.

Based on Tom's requirement E-mail, we do not just want to allow all components
to have commendations; instead, only persons, groups and sections should be
able to have commendations. This is a common problem in SchoolTool and has an
equally common solution. We are going to use a marker interface called
``IHaveCommentations`` that will be implemented by all components that can
have commendations.

So let's say we are given a person component:

  >>> import zope.interface
  >>> from zope import annotation
  >>> class Person(object):
  ...     zope.interface.implements(annotation.interfaces.IAttributeAnnotatable)
  ...     def __init__(self, id):
  ...         self.id = id
  ...
  ...     def __repr__(self):
  ...         return 'Person(%r)' %self.id

Now we want to state that this particular class can have commendations. So we
use the interface pacakge to attach the ``IHaveCommendations`` interface as
one that is *implemented* by the ``Person`` class.

  >>> import zope.interface
  >>> from schooltool.commendation import interfaces
  >>> zope.interface.classImplements(Person, interfaces.IHaveCommendations)

You might wonder, why did he not use the ``implements()`` function inside
the class directly to specify the interface? The reason is so that you would
ask yourself this question. I put it there to demonstrate how it will really
work. The classes, like ``Person``, we want to attach the commendation to,
already exists and it would be very bad if we would alter the class just to
assert this interface, since it would create a dependency. The better way to
do it is using the ``classImplements()`` function, that allows us to assert
implemented interfaces after the class is created.

Also note that the component must be annotatable, that is, they must implement
the ``IAttributeAnnotatable`` interface. In this particular case, the person
is attribute annotatable, which means that the annotations are stored in a
special attribute of the ``Person`` class instance, which is the most common
way for storing annotations. All SchoolTool content components implement
``IAttributeAnnotatable``.

Now that we have a class that can have commendations, we have to write an
adapter from ``IHaveCommendations`` to ``ICommendations`` that uses the
annotations. In short, an adapter adapts an object providing a particular
interface (here ``IHaveCommendations``) to another interface (here
``ICommendations``). The argument to the adapter is the component that is
adapted, in our example a ``Person`` class instance [#adapter]_. It does not
matter whether the adapter is a class or a function, as long as the adapter is
callable and takes one argument, everything is fine. While it is very common
to implement adapters using classes, for annotation-based adapters like this
one, SchoolTool usually uses a function. The function is called
``getCommendations()`` and is located in ``commmendation.py``. It's only
responsibility is to get the annotation from the object and return it. If it
does not exist, it creates the commendations annotation and returns it.

So before we add an annotation to a person, the attribute dictionary looks
pretty empty:

  >>> person = Person('stephan')
  >>> person.__dict__
  {'id': 'stephan'}

Next we register our new adapter function with the component architecture:

  >>> import zope.component
  >>> from schooltool.commendation import commendation
  >>> zope.component.provideAdapter(
  ...     commendation.getCommendations,
  ...     (interfaces.IHaveCommendations,), interfaces.ICommendations)

Now we can ask the person for its commendations:

  >>> commendations = interfaces.ICommendations(person)
  >>> commendations
  <Commendations for Person('stephan')>

Also, note that the adapter has added the annotations attribute to the person
now:

  >>> sorted(person.__dict__.keys())
  ['__annotations__', 'id']

We can now create and add commendations to the person:

  >>> commendations['goodjob'] = commendation.Commendation(
  ...     u'Good Job!',
  ...     u'You have done a good job with the latest assignment.',
  ...     u'school-wide')

Even if we look up the adapter again, the commendations will be available:

  >>> sorted(interfaces.ICommendations(person).keys())
  [u'goodjob']

And that's it. We are now able to define commendations for any external object
and store them inside annotations. As in the last section, this file itself
serves as the test for the new code again. I have added just another doc-test
file suite to the test setup in ``tests.py``.

.. [#adapter] For a good introduction to adapters see the ``human.txt`` in
   ``zope.interface``. If you feel brave and want to get a good logic training,
   you can also read ``adapter.txt`` in the same package for a complete
   description of adapters. Adapters are also discussed in detail in the
   ``socketexample.txt`` and ``README.txt`` files of the ``zope.component``
   package.
