Skip to content

Commit

Permalink
Merge pull request #721 from jazzband/through-defaults-2
Browse files Browse the repository at this point in the history
  • Loading branch information
rtpg committed Apr 16, 2021
2 parents 048b110 + 37d9e80 commit dbafa8d
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 7 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Unreleased
* Add Django 3.2 support.


* Add support for custom fields on through table models with `through_defaults` for ``TaggedManager.add`` and ``TaggedManager.set``.


1.3.0 (2020-05-19)
~~~~~~~~~~~~~~~~~~

Expand Down
13 changes: 11 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ playing around with the API.
information.
:param blank: Controls whether this field is required.

.. method:: add(*tags)
.. method:: add(*tags, through_defaults=None, tag_kwargs=None)

This adds tags to an object. The tags can be either ``Tag`` instances, or
strings::
Expand All @@ -21,6 +21,12 @@ playing around with the API.
[]
>>> apple.tags.add("red", "green", "fruit")

Use the ``through_defaults`` argument to specify values for your custom
``through`` model, if needed.

The ``tag_kwargs`` argument allows one to specify parameters for the tags
themselves.

.. method:: remove(*tags)

Removes a tag from an object. No exception is raised if the object
Expand All @@ -30,12 +36,15 @@ playing around with the API.

Removes all tags from an object.

.. method:: set(*tags, clear=False)
.. method:: set(*tags, through_defaults=None, clear=False)

If ``clear = True`` removes all the current tags and then adds the
specified tags to the object. Otherwise sets the object's tags to those
specified, removing only the missing tags and adding only the new tags.

Use the ``through_defaults`` argument to specify values for your custom
``through`` model, if needed.

.. method: most_common()
Returns a ``QuerySet`` of all tags, annotated with the number of times
Expand Down
13 changes: 8 additions & 5 deletions taggit/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def clone(self):


class _TaggableManager(models.Manager):
# TODO investigate whether we can use a RelatedManager instead of all this stuff
# to take advantage of all the Django goodness
def __init__(self, through, model, instance, prefetch_cache_name):
super().__init__()
self.through = through
Expand Down Expand Up @@ -143,9 +145,10 @@ def _lookup_kwargs(self):
return self.through.lookup_kwargs(self.instance)

@require_instance_manager
def add(self, *tags, **kwargs):
tag_kwargs = kwargs.pop("tag_kwargs", {})
def add(self, *tags, through_defaults=None, tag_kwargs=None, **kwargs):

if tag_kwargs is None:
tag_kwargs = {}
db = router.db_for_write(self.through, instance=self.instance)

tag_objs = self._to_tag_model_instances(tags, tag_kwargs)
Expand Down Expand Up @@ -173,7 +176,7 @@ def add(self, *tags, **kwargs):

for tag in tag_objs:
self.through._default_manager.using(db).get_or_create(
tag=tag, **self._lookup_kwargs()
tag=tag, **self._lookup_kwargs(), defaults=through_defaults
)

signals.m2m_changed.send(
Expand Down Expand Up @@ -252,7 +255,7 @@ def slugs(self):
return self.get_queryset().values_list("slug", flat=True)

@require_instance_manager
def set(self, *tags, **kwargs):
def set(self, *tags, through_defaults=None, **kwargs):
"""
Set the object's tags to the given n tags. If the clear kwarg is True
then all existing tags are removed (using `.clear()`) and the new tags
Expand Down Expand Up @@ -289,7 +292,7 @@ def set(self, *tags, **kwargs):
new_objs.append(obj)

self.remove(*old_tag_strs)
self.add(*new_objs, **kwargs)
self.add(*new_objs, through_defaults=through_defaults, **kwargs)

@require_instance_manager
def remove(self, *tags):
Expand Down
1 change: 1 addition & 0 deletions tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ class Migration(migrations.Migration):
to="tests.OfficialTag",
),
),
("extra_field", models.CharField(max_length=10)),
],
),
migrations.CreateModel(
Expand Down
1 change: 1 addition & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class OfficialThroughModel(GenericTaggedItemBase):
tag = models.ForeignKey(
OfficialTag, related_name="tagged_items", on_delete=models.CASCADE
)
extra_field = models.CharField(max_length=10)

class Meta:
unique_together = [["content_type", "object_id", "tag"]]
Expand Down
13 changes: 13 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,19 @@ def test_taggeditem_str(self):
str(self.taggeditem_model.objects.first()), "apple tagged with juicy"
)

def test_taggeditem_through_defaults(self):
if self.taggeditem_model != OfficialThroughModel:
self.skipTest(
"Through default tests are only run when the tagged item model has extra_field"
)
apple = self.food_model.objects.create(name="kiwi")
apple.tags.add("juicy", through_defaults={"extra_field": "green"})

self.assertEqual(
str(self.taggeditem_model.objects.first()), "kiwi tagged with juicy"
)
self.assertEqual(self.taggeditem_model.objects.first().extra_field, "green")

def test_abstract_subclasses(self):
p = Photo.objects.create()
p.tags.add("outdoors", "pretty")
Expand Down

0 comments on commit dbafa8d

Please sign in to comment.