Chandler 0.6
This tutorial will step you through the creation of an extension for Chandler. Chandler extensions are usually called 'parcels'.
The Feeds parcel is an RSS reader that integrates with the existing data model and allows the user to treat RSS data like other data in Chandler. This means that RSS articles can be stamped as tasks, placed on the calendar, and shared with others. This tutorial will step you through the process of writing this extension.
First download the 0.6 release of Chandler from the Chandler Downloads Page. For normal Python development, you should only need the compressed "End-Users' Distribution" for your platform. Using the compressed version (such as the .zip, .tar.gz, or .dmg) will be easier to work with than the installer version.
Unpack the file into a well known directory. For the purposes of
this tutorial, this file will be unpacked in ~/chandler. On Windows,
the Chandler executable is in ~/chandler/chandler.exe.
New extensions will be installed in the parcels
subdirectory. The Chandler 0.6 distribution includes a finished version
of the Feeds parcel in parcels/feeds. Delete the feeds directory or move it
outside of the parcels directory, so that it doesn't conflict with the
extension that you'll develop in this tutorial. Create a new feeds subdirectory in the parcels directory. This is where all of your files for this extension will be stored.
When developing for Chandler, you'll often need to pass parameters
to Chandler on startup. To make this easy, you should run Chandler from
the command line using the RunChandler startup scripts located in the release subdirectory. Here are a few parameters that will be useful
while developing your parcel:
-c, --create-W-P <directory>For example, to start Chandler with a fresh repository stored in the current directory, with the web server running, use all three parameters.
On Linux or Macintosh, type
> ./release/RunChandler --create -W -P.
On Windows, when using cygwin, type:
> ./release/RunChandler.bat --create -W -P.
If you're not using cygwin, type:
> release\RunChandler.bat --create -W -P.
Note: The web server can also be started from the Test menu, using the "Activate built-in webserver" menu item.
Chandler stores all of its data objects in the Repository. The Repository is a database used to persistently store Python objects. When an object is created "in the Repository" then certain attributes in the object will persist even if Chandler is shut down and restarted. Any type of attribute can be stored in the Repository, including complex data structures like dictionaries, lists, and references to other objects. In all other respects, these Repository objects behave just like Python objects.
Instead of storing data in structured tables like a traditional database, the Repository keeps all of its objects in a "soup". Objects can be retrieved by queries into this soup, or simply by following references from one object to another.
You may be curious and excited to jump right in and write some code. But in the world of Chandler, data is king. The sooner you define your data types, the more easily you can build a user interface around it. Don't worry, you can change your data types later, but you can't develop any meaningful application without at least some scaffolding.
Start thinking about the units of information that your extension will manage. This will help develop your schema. A schema is a description of your data and how the different data items relate to each other.
Here are some questions that may help you get started:
For the Feeds parcel, there are a few key pieces of data:
Python classes are the fundamental data type that help us define our schema. Data is defined as attributes of a given class. The schema syntax is similar to that used in the Django project.
Below is the definition of an RSS channel, FeedChannel. We'll store our data definitions in feeds/channel.py.
definition of an RSS channel, FeedChannel.
from osaf import pim
from application import schema
class FeedChannel(pim.ListCollection):
link = schema.One(schema.URL, displayName=u"RSS Feed URL")
category = schema.One(schema.Text)
author = schema.One(schema.Text)
date = schema.One(schema.DateTime) def UpdateFeeds(self): # TODO: refresh the feed list from the url pass
The link, category, author, and date attributes are all basic
attributes using simple types defined in the schema module. The UpdateFeeds() method is like any other Python method.
Behind the scenes: If you are very familiar with Python, you may notice that Chandler-specific attributes are defined as class
attributes, but in practice they are actually used as instance
attributes. If you have an instance of FeedChannel called fc, then fc.author refers specifically to an instance attribute that stores strings.
The attributes are all defined using schema.One, which indicates that there is just one of them per instance of the class. You can also use schema.Many, schema.Sequence, or schema.Mapping to define sets, lists, and dicts, respectively.
The displayName parameter provides a short text description of the actual attribute.
Notice that the FeedChannel class inherits from pim.ListCollection.
This class is a simple Chandler-based list of items. We will use this
collection to represent the fact that a feed contains one or more
feed items. Note that we have not declared anything about the type of
data that this channel will contain. Chandler collections can contain
any type of item. Each entry in Chandler's sidebar is a collection of
some kind.
Now lets look at the definition of a news item:
class FeedItem(pim.ContentItem):
link = schema.One(schema.URL)
author = schema.One(schema.Text)
date = schema.One(schema.DateTime)
content = schema.One(schema.Lob)
The definition is very similar to FeedChannel. The one important distinction here is that FeedItem is inheriting from pim.ContentItem. ContentItem is the base class for all user-visible data in Chandler.
Now that we have defined our types, we need to create a Python
package that will include these classes. Create feeds/__init__.py and
add the following line:
from channels import FeedChannel, FeedItem
When Chandler refreshes the Repository, it will look at all of the
Python packages in the parcels directory, and include any data
definitions.
To see if this works, start Chandler with a fresh repository, and run the web server:
> ./chandler --create -W
Now load your favorite web browser and go to http://localhost:1888/repo/. On the page that is displayed, you should see an area like this:
- parcels
- amazon: AmazonCollection, AmazonController, AmazonDetailBlock, AmazonItem
- feeds: FeedChannel, FeedItem
Now that we have defined some of the data types that the Feeds parcel
will manage, we will look at how they integrate into the existing user
interface. There are many ways to integrate into the Chandler UI, but
the simplest (and potentially richest) way is to work within the
framework of the Sidebar, Summary View, and Detail View.
These are the key parts of the Chandler UI:
RSS's two level hierarchy lends itself well to Chandler's existing user
interface. Feeds (FeedChannel) contain RSS Items (FeedItem)
and News Items have details. (including a body, url, author, and so
forth)
In the simplest case, a FeedChannel is a collection similar to the All, In, or Out collections. These collections are implemented with the Collection Kind.
Note: ACollection is just that: a generic collection of Items. Collections can contain any kind of Item, including mail messages, calendar events, and RSS news items. Single Items can actually be a member of multiple Collections.
Since the Chandler UI understands these basic constructs, we can create a Collection for each FeedChannel and Chandler will do its best to display the members of that collection.
We'll use the Event system to receive notification that our menu item has been selected, and that in turn will run some Python code that we write.
In feeds/__init__.py, we'll write a function called installParcel.
This function is called whenever the Repository installs your parcel into Chandler.
from channels import FeedChannel, FeedItem from application import schema
from osaf.framework.blocks import MenuItem, BlockEvent def installParcel(parcel, oldVersion=None):
mainview = schema.ns("osaf.views.main", parcel.itsView) MenuItem.update(parcel, "NewFeedChannel", blockName="NewFeedChannelItem", title=_(u"New Feed Channel"), parentBlock=mainview.CollectionMenu)
The installParcel() function will be called whenever Chandler starts with a fresh Repository. Be sure to use the --create parameter when starting Chandler, to ensure that this parcel is installed. You'll need to use --create every time you change anything in installParcel().
There are two key concepts being used here:
First, the mainview variable refers to a specific area
of the Repository using schema.ns(), called a "parcel". Parcels are persistent containers for Python
objects in the Repository. In this case mainview.CollectionMenu refers to
the object in the "osaf.views.main" parcel, named "CollectionMenu". You
can use the web-based Repository Viewer to view the items in this particular parcel here: http://localhost:1888/repo/parcels/osaf/views/main.
You can also navigate to this directory by clicking on the "//parcels"
link in the top of the Repository Viewer at http://localhost:1888/repo/.
Note: Don't worry about second
parameter (parcel.itsView) to schema.ns just yet. The view is a connection to the Repository. You'll learn more about repository views later.
Second, we're not necessarily creating a MenuItem.
Instead, we're updating one named "NewFeedChannel" that may
already be in the Repository. If it doesn't already exist, then it is
created. The name "NewFeedChannel" uniquely identifies it within the parcel object that is passed to installParcel. The parcel's name is more or
less the same as the current module's name. In this case, the parcel's
name is "feeds" because it exists in the parcels/feeds directory. The named parameters to update() will be mapped directly to
attributes on the MenuItem.
There is a connection between the use of the update()
method, and schema.ns(). The first parameter passed to
schema.ns() above is the name of a parcel. Any
attributes of the mainview variable actually reference the parcel named "osaf.views.main" above. Another module
could refer to the "NewFeedChannel" MenuItem like this:
feeds = schema.ns("feeds", view)
feedMenuItem = feeds.NewFeedChannel
Is this too early to introduce bidirectional refs? Its nice because its easy to understand, and we don't have to go into great detail.
But how is this menu item connected to the target menu?
Each parameter to update() sets an attribute in that object. The parentBlock attribute is used to attach NewFeedsChannel to the CollectionMenu. This is done with Bidirectional References. The schema for MenuItem and Menu are defined such that if a Menu is a parent of a MenuItem, then the MenuItem is automatically a child of the Menu.
When CollectionMenu is assigned to NewFeedsChannel's parentBlock, then NewFeedsChannel is automatically added to the CollectionMenu's childrenBlocks.
The Repository will maintain the integrity of this relationship. We'll see more uses of bidirectional references later in the tutorial.
Note: The parentBlock/childrenBlocks relationship is true for all elements in the UI.
Now that we have a menu item created, we want to make it call some existing code. We'll need to create two objects:
In this case we know exactly what controller we wish to dispatch to, so we will dispatch our menu's BlockEvent directly to the controller.
First, we will define the event as a persistent object in our parcel in feeds/__init__.py.
from osaf.framework.blocks import BlockEvent
def installParcel(parcel, oldVersion=None):
.
.
newChannelEvent = \
BlockEvent.update(parcel, "NewFeedChannelEvent",
blockName="NewFeedChannel",
commitAfterDispatch=True)
The commitAfterDispatch attribute on BlockEvent guarantees that the Repository data will be saved after the event has been dispatched. The result of the call to update() is actually a reference to the object itself.
We're storing the result of BlockEvent.update() in newChannelEvent. This variable will now reference the actual object, whether it was created or updated. We can use this variable later when we want to refer to the actual event object.
Note that we haven't actually defined acontroller yet. We'll do that next. For now we'll just use a print statement to make sure everything has been hooked up correctly. Add the following code to feeds/blocks.py:
from osaf.framework.blocks import Block
class FeedController(Block):
def onNewFeedChannelEvent(self, event):
import wx
from application.dialogs import Util
url = Util.promptUser(wx.GetApp().mainFrame,
u"New Channel", u"Enter a URL for the RSS Channel",
"http://")
print "We want to load %s" % url
Now we'll add an instance of this controller to our parcel in feeds/__init__.py:
from blocks import FeedController
def installParcel(parcel, oldVersion=None):
.
.
controller = FeedController.update(parcel, "FeedController")
One of the first things you may noticed about FeedController is the name of the first method, onNewFeedChannelEvent. The "NewFeedChannel" part of the method name corresponds to the blockName of the event that is being called. When a BlockEvent is dispatched, a method called on + blockName + Event is looked up in the target object. If it exists, then it is called. This BlockEvent is also passed into onNewFeedChannel as the parameter event.
Chandler uses the wxWidgets library for its user interface. The Util library provides a set of routines for prompt and confirmation dialogs. The code in onNewFeedChannelEvent() will simply prompt the user for a URL and then print it to the console.
Now that we have created a MenuItem, a BlockEvent, and a Controller, we need to connect them together. The chain references created with these objects looks something like this:
MenuItem -> BlockEvent -> Controller
You can hook them up in installParcel(). The code below defines them in the reverse order as we defined them above, so that we have the references available to build this chain of objects.
from osaf.framework.blocks import BlockEvent, MenuItem
from blocks import controller
def installParcel(parcel, oldVersion=None):
controller = FeedController.update(parcel, "FeedController")
newChannelEvent = \
BlockEvent.update(parcel, "NewFeedChannelEvent",
blockName="NewFeedChannel",
dispatchEnum="SendToBlockByReference",
destinationBlockReference=controller,
commitAfterDispatch=True)
MenuItem.update(parcel, "NewFeedChannel",
blockName="NewFeedChannelItem",
title=_(u"New Feed Channel"),
event=newChannelEvent,
parentBlock=mainview.CollectionMenu)
First, the use of dispatchEnum tells the BlockEvent that it will be dispatched to a specific Block instance. The controller block is retrieved in the call to FeedController.update() and passed as the attribute destinationBlockReference.
Next, newChannelEvent is attached to the MenuItem by passing it as the attribute event.
This completes the chain so that when the user clicks "New Feed Channel" in the "Collection" menu, onNewFeedChannelEvent() is called.
There is a reason for the levels of abstraction just to make a menu call some code:
This flexibility allows for a very dynamic interface.
We will now add new RSS Feeds to Chandler, so that they appear in the Sidebar. We'll also need to display the list of articles in the summary view, and the actual article itself in the Detail View.
We'll be using the Python feedparser library available at http://www.feedparser.org/
The feed download will occur in FeedChannel's UpdateFeeds() method. We'll need to download the feed information, and then create FeedItems for any items not already in the FeedChannel.
We can create FeedItem objects just like any other Python objects by simply calling FeedItem(). Since FeedChannel derives from Collection, we can add the new FeedITem to the FeedChannel by using Collection's add() method.
class FeedChannel(ContentItem):
.
.
.
def UpdateFeeds(self):
feeddata = feedparser.parse(self.url)
existingEntryURLs = [entry.url for entry in self]
for entry in feeddata.entries:
# skip articles we already have
if entry.url in existingEntryURLs:
continue
# create the new FeedItem, and populate it
newFeedItem = FeedItem(title=entry.title,
url=entry.link,
author=entry.author,
content=entry.content[0].value)
self.add(newFeedItem)
Recall from above that FeedChannel derived from ListCollection. When we create the FeedChannel collection object, we will add it to the Sidebar so that the user can see it. From here we will focus on the code called in the FeedController's onNewFeedCollectionEvent method.
Creating a FeedChannel object is just like creating any other object in Python, except that you can also pass any attribute values along in the constructor and they will be set automatically.
After creating the channel, we just have to add it to the list of collections that the sidebar uses.
class FeedController(Block):
def onNewFeedChannelEvent(self, event):
import wx
from application.dialogs import Util
url = Util.promptUser(wx.GetApp().mainFrame,
_(u"New Channel"), _(u"Enter a URL for the RSS Channel"),
"http://")
# create the feed
newFeed = FeedChannel(url=url)
# add it to the sidebar
sidebarCollections = schema.ns("osaf.app", self.itsView).sidebarCollections
sidebarCollections.add(newFeed)
# download the data
newFeed.UpdateFeeds()
An RSS reader is only useful if it can periodically fetch news stories without the user's intervention.
Chandler leverages the Twisted network library for task management. For the purposes of the Feeds parcel, we'll use the PeriodicTask class to regularly call the RSS update code.
First, we'll make a class whose method will be invoked on a regular basis. In channels.py, add the following class:
class FeedUpdateTaskClass:
def __init__(self, item):
# create a new view so that our changes are isolated
self.view = item.itsView
def run(self):
# bring in changes from other views
self.view.refresh()
# update all FeedChannel items
for channel in FeedChannel.iterItems(self.view):
channel.UpdateFeeds()
# now commit our changes to this view
self.view.commit()
# run this task again
return True
The FeedUpdateClass implements the basic interface in a PeriodicTask: the run() method is called on a regular interval.
This code introduces some more complex use of repository views, and even stores one in self.view. This is the same type of view as seen earlier in the self.schema() code.
A repository view is an interface to the objects in the Repository. As mentioned above, it is sort of a connection to the Repository. However a view is also a way to accumulate a set of changes to the Repository without worrying about changes interfering from other code.
All changes made to the Repository are made through a view. Each view maintains its own set of changes that are written back to the central Repository when the commit() method is called. Changes that have been committed from other views are brought into the current view by using the refresh() method.
The Repository is tightly coupled with the Python language. All repository object instances have an implicit itsView attribute that refers to the view where changes are made. When an object's attributes are read or written, the changes happen through the object's itsView view. This is in contrast to other common patterns found in other object databases, such as passing a 'view' parameter along as a parameter to any read/write methods.
Chandler's user interface runs on one thread, and all repository operations on this "UI Thread" share the same view.
The FeedUpdateTaskClass may be running on a background thread, and it would be unfortunate if a user changed something in the user interface at the same time that same data is being updated in a background thread. FeedUpdateTaskClass's __init__() method saves the view of the item that was passed in. Chandler will ensure that if this task is running on a separate thread, that it also gets its own view. In this case this view is never shared outside of the current class. This allows the feed updating code make a series of updates without worrying about changes that may happen in another view. When all the updates have happened, then the results are committed back to the Repository.
The FeedUpdateTaskClass needs to find all FeedChannel instances, and call the UpdateFeeds() method on each one. The method iterItems() is a class method on all classes that inherit from Item. It finds all items of the given class in the Repository. The view parameter to iterItems() is necessary because classes themselves don't exist in any specific view. There is no object with an implicit itsView attribute so it must be specified.
The last step in writing a periodic task is to persist the task information in the repository.
The PeriodicTask class is used to persist information about how and when a task should be run. We need to create a PeriodicTask in the repository with information about how often the task should be run. We'll simply create the class in installParcel() as we did with the blocks earlier.
def installParcel(parcel, oldName=None):
.
.
.
from osaf.startup import PeriodicTask
import datetime
PeriodicTask.update(parcel, "FeedUpdateTask",
invoke="feeds.FeedUpdateTaskClass",
run_at_startup=True,
interval=datetime.timedelta(minutes=30))
At startup, Chandler will use PeriodicTask.iterItems() to find all PeriodicTask instances, just like FeedUpdateTaskClass itself does to find FeedChannel classes. It will then register each task with Twisted, which will handle the instantiation and running of our task.
So far, this tutorial has explained how to create collections, add them to the sidebar and update them periodically. If you launch Chandler, you will be able to subscribe to an RSS feed and it will appear in the sidebar. When you select the feed in the sidebar with the "All" button pressed in the main toolbar, you should see a table widget in the Summary view with a series of blank lines. Each of these lines represents an article from the RSS feed, even though it isn't displaying any specifics about the article. If you click on one of these blank lines, the detail view will display a mostly-blank view of that particular article, but again it will be mostly blank.
We need to tell Chandler a little more about our data in order for it to display its attributes in the UI.
Many different classes of ContentItem may be stored in the summary table. The table needs to be able to ask each item for a common attribute. Instead of requiring each class to manually store data in a shared about attribute from a base class, each class can store its data in a class-specific attribute, and then forward about to the class-specific attribute.
The summary table displays three attributes of an item: who, about, and date. These attributes are usually not true repository attributes; they are not defined as distinct values within a Python class.
Instead repository attribute redirection is used to forward their references to other attributes in the class. To define this redirection, schema.Role() is used.
class FeedItem(pim.ContentItem):
.
.
.
about = schema.Role(redirectTo="title")
who = schema.Role(redirectTo="author")
In the case of FeedItem, we have already defined a date attribute, so we only need to redirect about and who. Whenever the Summary View wants to know what data to display in the 'about' column, it will call getattr(item, 'about'). This will automatically forward to the title attribute, so it is as if the Summary View had called getattr(item, 'title').
The detail view is a more complex user interface than just a set of columns. Every important attribute should be displayed to the user, and each attribute may have a special way to display itself, or may require special behavior when it is edited.
The detail view is divided into rows. Generally there is one row per attribute, though more complex displays are possible. Typically a row consists of a label, followed by the value of an attribute.
To display a particular item, you need to define some of the actual user interface elements, and indicate where they should be displayed. User interface components are constructed with Blocks. Blocks are persistent Python objects that wrap wxWidgets classes.
The first step is to define the list of UI elements, and the attributes they will display:
def installParcel(parcel, oldVersion=None):
.
.
.
from osaf.framework.blocks import detail
detailblocks = schema.ns("osaf.framework.blocks.detail", parcel.itsView)
feedItemRootBlocks = [
# The markup bar
detailblocks.MarkupBar,
detail.makeSpacer(parcel, height=6, position=0.01).install(parcel),
# Author area
detail.makeArea(parcel, "AuthorArea",
position=0.19,
childrenBlocks = [
detail.makeLabel(parcel, _(u"author"), borderTop=2),
detail.makeSpacer(parcel, width=8),
detail.makeEditor(parcel, 'author',
viewAttribute=u'author',
border=RectType(0,2,2,2),
readOnly=True),
]
).install(parcel),
# URL
detail.makeArea(parcel, "LinkArea",
position=0.3,
childrenBlocks = [
detail.makeLabel(parcel, _(u'link'), borderTop=2),
detail.makeSpacer(parcel, width=8),
detail.makeEditor(parcel, 'link',
viewAttribute=u'link',
border=RectType(0,2,2,2),
readOnly=True),
]
).install(parcel),
]
There are 4 elements of the feedItemRootBlocks:
detailblocks.MarkupBar detail.makeSpacer() detail.makeArea(parcel, "AuthorArea", ...) detail.makeArea(parcel, "LinkArea", ...) Chandler gives users the ability to "Stamp" items to make the actual item appear to be multiple kinds of items at once. (such as a Task and a Calendar Event) When the detail view is asked to display an Item, it will go find the relevant blocks for that item's class, and render them on the screen. Because of Stamping, the rules for displaying data can get complex when you are mixing different classes together.
There are separate sets of widgets for each class displayed in the Detail View. If an Item is stamped as more than one class, then it needs to display some combination of the widgets from both classes. For instance, if a FeedItem were stamped as a Calendar Event, then the Detail View must display both the author (from FeedItem) and the duration (from CalendarEvent) of the item, even though these two items appear in different classes.
This problem is alleviated by use of the position attribute. You'll notice that each entry after MarkupBar in feedItemRootBlocks has a position attribute between 0 and 1. This position is used to sort the widgets in the display. 0 represents the top of the Detail View, and 1 represents the bottom. This means that if another Detail View widget has a position of 0.25, then it will appear between AuthorArea and LinkArea.
Finally, we need to register these Detail View blocks with the Detail View itself. Once again, we just need to create an item in the repository and the Detail View will find it automatically:
def installParcel(parcel, oldVersion=None):
.
.
.
detail.DetailTrunkSubtree.update(parcel, "ChannelSubtree",
key=feeds.FeedItem.getKind(parcel.itsView),
rootBlocks=feedItemBlocks)
One small detail here is the key attribute. You'll notice that it refers to FeedItem.getKind(). Kinds were briefly discussed at the beginning of this document. Here we use getKind() to get a value that can be stored in the repository. Like iterItems(), getKind() operates on a class, so there is no implicit view. The view that is used comes from the parcel that is being installed.
Most of the items in the childrenBlocks lists should be relatively self explanatory. makeLabel() and makeSpacer() simply make a label and a spacer to the left of the actual detail view. The third item, makeEditor(), creates a widget called an Attribute Editor. An Attribute Editor is a special type of Block that will dynamically create the correct widget to display and/or edit the given attribute, even if that attribute is a complex type like a Python datetime or a number.
Attribute Editors can be modified on a per-use basis allowing for read-only widgets or specialized display of certain types. It is not yet possible to customize the actual types of widgets that Attribute Editors can display, but that is planned for future versions of Chandler.
For most basic attributes, Attribute Editors are the appropriate mechanism to display attribute data. If you need a more complex display beyond what is provided, you can write your own Blocks to wrap some of the existing wxWidgets, or use some of the Blocks that are built into Chandler.
The Feeds parcel needs to display the body of a feed article. We'll use the existing HTMLDetailArea Block that is provided by Chandler to provide a friendly display with proportioned text.
The HTMLDetailArea class is a Block that the detail area can use to display HTML code. To use it, derive from HTMLDetailArea and implement the function getHTMLText(). The base class will call getHTMLText() when it is time to display the data on screen.
class FeedItemDetail(detail.HTMLDetailArea):
def getHTMLText(self, item):
if item is None:
return
HTMLText = u'<html><body>\n\n'
HTMLText += u'<h5><a href="%s">%s</a></h5>' % \
(item.link, displayName)
content = getattr(item, 'content', u'')
content.replace("<", "<").replace(">", ">")
HTMLText += u'<p>%s</p>' % content
HTMLText += u'</body></html>'
return HTMLText
To use this block, we just have to make sure that an instance of it is included in the feedItemBlocks:
def installParcel(parcel, oldVersion=None):
.
.
.
feedItemRootBlocks = [
.
.
.
FeedItemDetail.update(parcel, "ItemBodyArea",
position=0.9,
blockName="ItemBodyArea",
minimumSize=SizeType(100, 50)),
]
This ensures that it is part of the DetailTrunkSubtree that we declared earlier.
In this tutorial, you've learned how to:
The next step is to spend some time developing your own data types and application behavior. The tutorial will get you some basic functionality out of your data but Chandler's true potential comes to light when you begin to explore some of its more advanced capabilities. When items have been properly defined, many of these capabilities "just work" for your new class.