Browse files

Added the get_or_create_by_full_name() method.

  • Loading branch information...
1 parent d31aa90 commit 9b2229973a76cf5a632bddd35c57e9f928f0b906 Malcolm Tredinnick committed Jul 12, 2009
Showing with 102 additions and 8 deletions.
  1. +32 −0 acacia/
  2. +40 −0 acacia/
  3. +30 −8 docs/basic-usage.rst
@@ -37,6 +37,31 @@ def get_subtree(self, full_name):
return self.model.get_tree(self.get_by_full_name(full_name))
+ def get_or_create_by_full_name(self, full_name):
+ """
+ Retrieves a topic with the given full_name. If the topic doesn't exist,
+ it is created (along with all the necessary parent topics).
+ Returns a pair: the topic object and a boolean flag indicating whether
+ or not a new object was created.
+ """
+ try:
+ node = self.get_by_full_name(full_name)
+ return node, False
+ except self.model.DoesNotExist:
+ pass
+ pieces = full_name.rsplit(self.model.separator, 1)
+ if len(pieces) == 1:
+ return self.model.create(name=pieces[0]), True
+ parent, created = self.get_or_create_by_full_name(pieces[0])
+ if not pieces[1]:
+ # full_name ended with a trailing separator (e.g. /foo/bar/).
+ return parent, created
+ node = self.model(name=pieces[-1])
+ parent.add_child(node)
+ return node, True
class AbstractTopic(treebeard_mods.MP_Node):
@@ -65,6 +90,12 @@ def __unicode__(self):
return self.full_name
def _set_full_name(self):
+ """
+ Sets the full_name attribute to the correct value. Generally called as
+ part of saving the class, but can also be called by other class methods
+ that need an accurate value before displaying the __unicode__ output,
+ for example.
+ """
if self.depth == 1:
self.full_name =
@@ -77,6 +108,7 @@ def save(self, *args, **kwargs):
Updates the full_name attribute prior to saving (incurs an extra lookup
query on each save, but saving is much less common than retrieval).
+ # FIXME: Need to handle the case of creating a duplicate node.
return super(AbstractTopic, self).save(*args, **kwargs)
@@ -162,6 +162,46 @@ def test_create_duplicate_entry(self):
self.assertRaises(db.IntegrityError, node.move, target,
+ def test_create_by_full_name1(self):
+ """
+ Tests that creating a new node parented to an existing node using the
+ full name works.
+ """
+ _, created = models.Topic.objects.get_or_create_by_full_name("a/b/e")
+ self.failUnlessEqual(created, True)
+ node = models.Topic.objects.get_by_full_name("a/b/e")
+ self.failUnlessEqual(unicode(node), u"a/b/e")
+ def test_create_by_full_name2(self):
+ """
+ Tests that creating a new node with new parents, using the full name,
+ works.
+ """
+ _, created = models.Topic.objects.get_or_create_by_full_name("a/z/z")
+ self.failUnlessEqual(created, True)
+ node = models.Topic.objects.get_by_full_name("a/z/z")
+ self.failUnlessEqual(unicode(node), u"a/z/z")
+ def test_create_by_full_name3(self):
+ """
+ Tests that attempting to create a new node using a full name that
+ already exists works as expected (the pre-existing node is returned and
+ nothing new is created).
+ """
+ _, created = models.Topic.objects.get_or_create_by_full_name("c/b/d")
+ self.failUnlessEqual(created, False)
+ def test_create_by_full_name4(self):
+ """
+ Tests that creating a new node with existing parents, using the full
+ name, normalises the full name properly (trailing and multiple
+ separators).
+ """
+ node, created = models.Topic.objects.get_or_create_by_full_name(
+ "a///b/e//")
+ self.failUnlessEqual(created, True)
+ self.failUnlessEqual(unicode(node), u"a/b/e")
class TreebeardTests(BaseTestSetup, test.TestCase):
Tests for my local modification/overrides to the default treebeard
@@ -83,9 +83,10 @@ Selecting Topics
It will be common ``Acacia`` usage to want to select :class:`Topic` objects
using their full, human-readable names. Those names are natural candidates for
using in URLs and the like, so you need to be able to return from the string
-form to the correct object or subtree of objects. This is the only reason
-``Acacia`` exists as an application on top of `treebeard`_: to provide
-alternative access methods.
+form to the correct object or subtree of objects. In fact, this is the only
+reason ``Acacia`` exists as an application, rather than simply using
+`treebeard`_ directly: to provide alternative access methods to the individual
.. _treebeard:
@@ -96,9 +97,30 @@ its string form::
-For convenience (particularly when working with URLs), repeated separators
-between name components (the ``"/"`` character) are collapsed into a single
-separator. So ``animal//cat`` is the same as ``animal/cat``. Leading and
-trailing separators are also ignored. Thus, ``/animal/cat/`` is also the same
-as ``animal/cat``.
+For convenience and consistency repeated separators between name components
+(the ``"/"`` character) are collapsed into a single separator. Leading and
+trailing separators are also removed. Thus, ``animal//cat`` and
+``/animal/cat/`` are both normalised to ``animal/cat``.
+If you call ``get_by_full_name()`` and pass in a name that does not exist as a
+topic node, a ``Topic.DoesNotExist`` exception is raised. This is similar to
+the behaviour of the ``get()`` method in Django's queryset API.
+Automatically Creating New Topics
+:class:`Topic` objects can be created using the object's full name. You do not
+need to worry about whether the necessary parent node exists, as Acacia will
+create any missing ancestor nodes. If a node with the given full name already
+exists, a duplicate node is not created. Rather, the existing node is returned
+as part of the call. This is all done using ``get_or_create_by_full_name()``::
+ node, created = Topic.objects.get_or_create_by_full_name(
+ "software/language/python")
+The returned ``node`` object is the ``Topic`` object that was requested. The
+``created`` value is ``True`` is the ``node`` was newly created and ``False``
+is it already existed in the tree. The analogy with Django's queryset`
+``get_or_create()`` method should be clear.

0 comments on commit 9b22299

Please sign in to comment.