diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 30768be7..f97b7560 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,10 @@ Changelog ========= +UNRELEASED +---------- + * Added ``has_changed()`` method to ``taggit.forms.TagField``. + 0.24.0 (2019-02-19) ~~~~~~~~~~~~~~~~~~~ * The project has moved to `Jazzband `_. This is the diff --git a/taggit/forms.py b/taggit/forms.py index e7c493f8..a4ec88dd 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -10,7 +10,7 @@ class TagWidget(forms.TextInput): def format_value(self, value): if value is not None and not isinstance(value, six.string_types): - value = edit_string_for_tags([o.tag for o in value.select_related("tag")]) + value = edit_string_for_tags(value) return super(TagWidget, self).format_value(value) @@ -26,3 +26,12 @@ def clean(self, value): raise forms.ValidationError( _("Please provide a comma-separated list of tags.") ) + + def has_changed(self, initial_value, data_value): + initial_value = [tag.name for tag in initial_value] + initial_value.sort() + try: + data_value = self.clean(data_value) + except forms.ValidationError: + pass + return initial_value != data_value diff --git a/taggit/managers.py b/taggit/managers.py index 508cb8bb..814a3c8f 100644 --- a/taggit/managers.py +++ b/taggit/managers.py @@ -531,10 +531,13 @@ def formfield(self, form_class=TagField, **kwargs): defaults.update(kwargs) return form_class(**defaults) - def value_from_object(self, instance): - if instance.pk: - return self.through.objects.filter(**self.through.lookup_kwargs(instance)) - return self.through.objects.none() + def value_from_object(self, obj): + if obj.pk is None: + return [] + qs = self.through.objects.select_related("tag").filter( + **self.through.lookup_kwargs(obj) + ) + return [ti.tag for ti in qs] def related_query_name(self): return self.model._meta.model_name diff --git a/tests/tests.py b/tests/tests.py index 00daa8fc..df6ce802 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -885,6 +885,67 @@ def test_formfield(self): ff = tm.formfield() self.assertRaises(ValidationError, ff.clean, "") + def test_form_changed_data(self): + # new food, blank tag + pear = self.food_model() + request = {"name": "pear", "tags": ""} + fff = self.form_class(request, instance=pear) + self.assertFalse(fff.is_valid()) + + pear = self.food_model() + request = {"name": "pear", "tags": "sweat"} + fff = self.form_class(request, instance=pear) + self.assertTrue(fff.is_valid()) + self.assertIn("tags", fff.changed_data) + self.assertIn("name", fff.changed_data) + fff.save() + + request = {"name": "pear", "tags": "yellow"} + fff = self.form_class(request, instance=pear) + self.assertTrue(fff.is_valid()) + self.assertIn("tags", fff.changed_data) + self.assertNotIn("name", fff.changed_data) + fff.save() + + # same object nothing changed + fff = self.form_class(request, instance=pear) + self.assertTrue(fff.is_valid()) + self.assertFalse(fff.changed_data) + + # delete tag + request = {"name": "pear", "tags": ""} + fff = self.form_class(request, instance=pear) + self.assertFalse(fff.is_valid()) # tag not blank + + # change name, tags are the same + request = {"name": "apple", "tags": "yellow"} + fff = self.form_class(request, instance=pear) + self.assertTrue(fff.is_valid()) + self.assertIn("name", fff.changed_data) + self.assertNotIn("tags", fff.changed_data) + fff.save() + + # tags changed + apple = self.food_model.objects.get(name="apple") + request = {"name": "apple", "tags": "yellow, delicious"} + fff = self.form_class(request, instance=apple) + self.assertTrue(fff.is_valid()) + self.assertNotIn("name", fff.changed_data) + self.assertIn("tags", fff.changed_data) + fff.save() + + # only tags order changed + apple = self.food_model.objects.get(name="apple") + request = {"name": "apple", "tags": "delicious, yellow"} + fff = self.form_class(request, instance=apple) + self.assertTrue(fff.is_valid()) + self.assertFalse(fff.changed_data) + + # and nothing changed + fff = self.form_class(request, instance=apple) + self.assertTrue(fff.is_valid()) + self.assertFalse(fff.changed_data) + class TaggableFormDirectTestCase(TaggableFormTestCase): form_class = DirectFoodForm