This example is taken from the PyCon? tutorial, shortened and changed to use the new Many-to-Many relationship methods of dBizobj. It's just a demonstration of some of these methods, hopefully the most important, in a running, if not very nice or user-friendly application. Some diagnostic print statements are commented out, but left in.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# mmtest.py
# Test Many-to-Many relationships


import logging
import dabo
dabo.ui.loadUI("wx")
<h2>class  BizCategories(dabo.biz.dBizobj):</h2>


        def initProperties(self):
                self.DataSource = "reccats"
                self.KeyField = "id"
                self.DataStructure = (
                                ("id", "I", True, "reccats", "id"),
                                ("descrp", "C", False, "reccats", "descrp"),
                )
                self.addFrom("reccats")
                self.addField("reccats.id")
                self.addField("descrp")

<h2>class BizRecipes(dabo.biz.dBizobj):</h2>


        def initProperties(self):
                self.DataSource = "recipes"
                self.KeyField = "id"
                self.DataStructure = (
                                ("id", "I", True, "recipes", "id"),
                                ("title", "C", False, "recipes", "title"),
                                ("date", "D", False, "recipes", "date")
                )
                self.addFrom("recipes")
                self.addField("recipes.id")
                self.addField("title")
                self.addField("date")



        def afterInit(self):
                # for SQLite
                self.executeSafe("PRAGMA foreign_keys = ON")


# no separate bizobj class for the 'joiner' table reccat


class CklCategories(dabo.ui.dCheckList):
        """
        nearly unchanged from the PyCon tutorial
        """

        def initProperties(self):
                self.Width = 200
                self._needChoiceUpdate = True


        def updateChoices(self):
                self._needChoiceUpdate = False
                (self.Choices, self.Keys) = self.Form.getCategoryChoicesAndKeys()


        def update(self):
                self.super()
                dabo.ui.callAfterInterval(200, self.doUpdate)


        def doUpdate(self):
                if self._needChoiceUpdate:
                        self.updateChoices()
                biz = self.Form.getBizobj("recipes")
                mmbiz = self.Form.getBizobj("reccats")
                keyvalues = []
                # categories for the currently selected recipe
                for mmval in biz.mmGetAssociatedValues(mmbiz, ["id"]):
                        keyvalues.append(mmval["id"])
                self.KeyValue = keyvalues
                # print "Categories for this recipe: %s" % keyvalues



        def onHit(self, evt):
                idx = evt.EventData["index"]
                idxKey = self.Keys[idx]
                # addCategory, delCategory now need value of field 'descrp', not PK
                idxVal = self.Choices[idx]
                # print "idx = %d, idxKey = %d, idxVal = %s" % (idx, idxKey, idxVal)
                # print self.KeyValue
                if idxKey in self.KeyValue:
                        self.Form.addCategory(idxVal)
                else:
                        self.Form.delCategory(idxVal)

<h2>class FrmTest(dabo.ui.dForm):</h2>


        def afterInit(self):
                self.Sizer = gps = dabo.ui.dGridSizer(HGap=3, VGap=3, MaxCols=2)
                pn = self.createGridPanel(self)
                gps.append(pn, "expand")
                pn = self.createCklPanel(self)
                gps.append(pn, "expand")
                pn = self.createTitlePanel(self)
                gps.append(pn, "expand")
                pn = self.createCatPanel(self)
                gps.append(pn, "expand")
                bt = dabo.ui.dButton(self, Caption="Save new", OnHit=self.saveNew)
                gps.append(bt, "expand", colSpan=2)
                gps.setColExpand(True, 0)
                # gps.setColExpand
                gps.setRowExpand(True, 0)
                self.layout()


        def afterInitAll(self):
                self.requery()
                self.grParentID.autoSizeCol("all")
                self.grParentID.setFocus()


        def createBizobjs(self):
                conn = self.Application.getConnectionByName("recipesConnPK")
                parentbiz = BizRecipes(conn)
                self.addBizobj(parentbiz)
                mmbiz = BizCategories(conn)
                self.addBizobj(mmbiz)
                # Set up the Many-to-Many relationship
                parentbiz.addMMBizobj(mmbiz, "reccat", "recid", "catid")


        def requeryCategory(self):
                biz = self.getBizobj("reccats")
                biz.UserSQL = "select * from reccats order by descrp"
                biz.requery()


        def getCategoryChoicesAndKeys(self, forceRequery=True):
                """     Return two lists, one for all descrp values and one for all id values."""
                cache = getattr(self, "_cachedCategories", None)
                if not forceRequery and cache is not None:
                        return cache
                (choices, keys) = ([], [])
                biz = self.getBizobj("reccats")
                if biz.RowCount <= 0 or forceRequery:
                        self.requeryCategory()
                for rownum in biz.bizIterator():
                        choices.append(biz.Record.descrp)
                        keys.append(biz.Record.id)
                self._cachedCategories = (choices, keys)
                return self._cachedCategories


        def addCategory(self, adescr, atitle=""):
                mmbiz = self.getBizobj("reccats")
                if atitle:
                        # New recipe
                        self.getBizobj("recipes").mmAddToBoth(mmbiz, "title", atitle, "descrp", adescr)
                else:
                        self.getBizobj("recipes").mmAssociateValue(mmbiz, "descrp", adescr)


        def delCategory(self, adescr):
                mmbiz = self.getBizobj("reccats")
                self.getBizobj("recipes").mmDissociateValues(mmbiz, "descrp", [adescr])


        def saveNew(self, evt=None):
                atitle = self.txTitleID.Value
                adescr = self.txCatID.Value
                # clear textboxes so the values aren't reused inadvertently
                self.txTitleID.Value = ""
                self.txCatID.Value = ""
                if adescr:
                        # new category and possibly new recipe added, both related
                        self.addCategory(adescr, atitle)
                        self.requeryCategory()
                else:
                        # only the recipe is new, no new category added
                        biz = self.getBizobj("recipes")
                        biz.new()
                        biz.setFieldVal("title", atitle)
                self.save()
                self.requery()


        def createGridPanel(self, parent):
                pn = dabo.ui.dPanel(parent)
                pn.Sizer = vsp = dabo.ui.dSizer("v")
                grParent = dabo.ui.dGrid(pn, AlternateRowColoring=True, ColumnCount=3,
                                SelectionMode="Row", RegID="grParentID", DataSource="recipes")
                for (idx, (cpt, fld)) in enumerate([("ID", "id"), ("Title", "title"),
                                ("Date", "date")]):
                        grParent.Columns[idx].Caption = cpt
                        grParent.Columns[idx].DataField = fld
                vsp.append1x(grParent, border=10)
                return pn


        def createCklPanel(self, parent):
                pn = dabo.ui.dPanel(parent)
                pn.Sizer = vsp = dabo.ui.dSizer("v")
                vsp.append1x(CklCategories(pn), border=10)
                return pn


        def createTitlePanel(self, parent):
                pn = dabo.ui.dPanel(parent)
                pn.Sizer = vsp = dabo.ui.dSizer("v")
                vsp.append(dabo.ui.dLabel(pn, Caption="Title for new recipe"))
                vsp.append(dabo.ui.dTextBox(pn, RegID="txTitleID"), "expand")
                return pn


        def createCatPanel(self, parent):
                pn = dabo.ui.dPanel(parent)
                pn.Sizer = vsp = dabo.ui.dSizer("v")
                vsp.append(dabo.ui.dLabel(pn, Caption="New category"))
                vsp.append(dabo.ui.dTextBox(pn, RegID="txCatID"), "expand")
                return pn


if __name__ == "__main__":
        app = dabo.dApp()
        # just for showing what the new methods really do
        dabo.dbConsoleLogHandler.setLevel(logging.DEBUG)
        app.MainFormClass = FrmTest
        app.start()