From 24e2d681c476cbc12426903fb3640c71a0e046e5 Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Sat, 6 Jul 2024 10:56:32 -0400 Subject: [PATCH 01/15] Saving progress --- taggit/admin.py | 48 ++++++++++++++++++- taggit/forms.py | 8 ++++ .../admin/taggit/merge_tags_form.html | 29 +++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 taggit/templates/admin/taggit/merge_tags_form.html diff --git a/taggit/admin.py b/taggit/admin.py index a9339cfb..2ad04a87 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -1,7 +1,11 @@ from django.contrib import admin - +from django import forms +from django.db import transaction from taggit.models import Tag, TaggedItem +from django.shortcuts import render, redirect +from .forms import MergeTagsForm + class TaggedItemInline(admin.StackedInline): model = TaggedItem @@ -14,3 +18,45 @@ class TagAdmin(admin.ModelAdmin): ordering = ["name", "slug"] search_fields = ["name"] prepopulated_fields = {"slug": ["name"]} + actions = ["merge_tags"] + + def merge_tags(self, request, queryset): + print("🚀 merge_tags called") + print(f"Request method: ✅ {queryset}") + print(f"Request POST data: 😊{request.POST}") + + if request.method == "POST" and "csrfmiddlewaretoken" in request.POST: + form = MergeTagsForm(request.POST) + if request.method == "POST": + print("✅", "after form submission") + new_tag_name = "fruit" # hard coded value of the new tag + new_tag, created = Tag.objects.get_or_create(name=new_tag_name) + with transaction.atomic(): + for tag in queryset: + for tagged_item in tag.taggit_taggeditem_items.all(): + tagged_item.tag = new_tag + tagged_item.save() + # tag.delete() #we can uncomment this to also remove the selected tags + + self.message_user(request, "Tags merged successfully.") + return redirect(request.get_full_path()) + else: + print(f"Form errors: {form.errors}") + self.message_user(request, "Form is invalid.", level="error") + else: + form = MergeTagsForm() + + context = { + "title": "Merge selected tags into a new tag", + "form": form, + "action_checkbox_name": admin.helpers.ACTION_CHECKBOX_NAME, + "queryset": queryset, + } + + return render( + request, + "admin/taggit/merge_tags_form.html", + context, + ) + + merge_tags.short_description = "Merge selected tags" diff --git a/taggit/forms.py b/taggit/forms.py index cd68db6d..08a5c832 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -52,3 +52,11 @@ def has_changed(self, initial_value, data_value): initial_value.sort() return initial_value != data_value + + +class MergeTagsForm(forms.Form): + new_tag_name = forms.CharField( + label="New Tag Name", + max_length=255, + widget=forms.TextInput(attrs={"id": "id_new_tag_name"}), + ) diff --git a/taggit/templates/admin/taggit/merge_tags_form.html b/taggit/templates/admin/taggit/merge_tags_form.html new file mode 100644 index 00000000..a9aa89da --- /dev/null +++ b/taggit/templates/admin/taggit/merge_tags_form.html @@ -0,0 +1,29 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls %} + +{% block title %}{% trans 'Django site admin' %}{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} + {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+ {% endfor %} +
+ + +
+
+
+{% endblock %} \ No newline at end of file From 0df866d97cc48d68e53aa5e07abe76e0e265abb8 Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Sat, 13 Jul 2024 22:15:23 -0400 Subject: [PATCH 02/15] Tag merging functionality is done - Selected tag ids are appended to url query parameter - tag ids are store in the session temporaily - tags are merged into new tag name - session is cleared of the tag IDs --- taggit/admin.py | 80 ++++++++++++------- .../admin/taggit/merge_tags_form.html | 50 ++++++------ 2 files changed, 76 insertions(+), 54 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index 2ad04a87..13a15568 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin -from django import forms from django.db import transaction -from taggit.models import Tag, TaggedItem - +from django.http import JsonResponse from django.shortcuts import render, redirect from .forms import MergeTagsForm +from taggit.models import Tag, TaggedItem +from django.urls import path class TaggedItemInline(admin.StackedInline): @@ -18,45 +18,67 @@ class TagAdmin(admin.ModelAdmin): ordering = ["name", "slug"] search_fields = ["name"] prepopulated_fields = {"slug": ["name"]} - actions = ["merge_tags"] + actions = ["render_tag_form"] + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "merge-tags/", + self.admin_site.admin_view(self.merge_tags_view), + name="merge_tags", + ), + ] + return custom_urls + urls + + def render_tag_form(self, request, queryset): + selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME) + if not selected: + self.message_user(request, "Please select at least one tag.") + return redirect(request.get_full_path()) + + selected_tag_ids = ",".join(selected) + redirect_url = ( + f"{request.get_full_path()}merge-tags/?selected_tags={selected_tag_ids}" + ) + + return redirect(redirect_url) + + def merge_tags_view(self, request): + if request.method == "GET": + selected_tag_ids = request.GET.get("selected_tags", "").split(",") - def merge_tags(self, request, queryset): - print("🚀 merge_tags called") - print(f"Request method: ✅ {queryset}") - print(f"Request POST data: 😊{request.POST}") + # store selected_tag_ids in session data until they are merged + request.session["selected_tag_ids"] = selected_tag_ids + else: + selected_tag_ids = request.session.get("selected_tag_ids", []) - if request.method == "POST" and "csrfmiddlewaretoken" in request.POST: + if request.method == "POST": form = MergeTagsForm(request.POST) - if request.method == "POST": - print("✅", "after form submission") - new_tag_name = "fruit" # hard coded value of the new tag + if form.is_valid(): + new_tag_name = form.cleaned_data["new_tag_name"] new_tag, created = Tag.objects.get_or_create(name=new_tag_name) with transaction.atomic(): - for tag in queryset: - for tagged_item in tag.taggit_taggeditem_items.all(): + for tag_id in selected_tag_ids: + tag = Tag.objects.get(id=tag_id) + tagged_items = TaggedItem.objects.filter(tag=tag) + for tagged_item in tagged_items: tagged_item.tag = new_tag tagged_item.save() - # tag.delete() #we can uncomment this to also remove the selected tags + # tag.delete() #this will delete the selected tags after merge - self.message_user(request, "Tags merged successfully.") - return redirect(request.get_full_path()) + # clear the selected_tag_ids from session after merge is complete + request.session.pop("selected_tag_ids", None) + return redirect("..") else: print(f"Form errors: {form.errors}") self.message_user(request, "Form is invalid.", level="error") - else: - form = MergeTagsForm() context = { - "title": "Merge selected tags into a new tag", - "form": form, - "action_checkbox_name": admin.helpers.ACTION_CHECKBOX_NAME, - "queryset": queryset, + "form": MergeTagsForm(), + "selected_tag_ids": selected_tag_ids, } - return render( - request, - "admin/taggit/merge_tags_form.html", - context, - ) + return render(request, "admin/taggit/merge_tags_form.html", context) - merge_tags.short_description = "Merge selected tags" + render_tag_form.short_description = "Merge selected tags" diff --git a/taggit/templates/admin/taggit/merge_tags_form.html b/taggit/templates/admin/taggit/merge_tags_form.html index a9aa89da..8e038f39 100644 --- a/taggit/templates/admin/taggit/merge_tags_form.html +++ b/taggit/templates/admin/taggit/merge_tags_form.html @@ -1,29 +1,29 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_urls %} - -{% block title %}{% trans 'Django site admin' %}{% endblock %} - -{% block content %} -
-
- {% csrf_token %} - {% for field in form %} -
- {{ field.label_tag }} - {{ field }} - {% if field.errors %} +
+
+
+
+ + {% csrf_token %} {% for field in form %} +
+ {{ field.label_tag }} {{ field }} {% if field.errors %}
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %}
{% endif %} -
- {% endfor %} -
- - -
- +
+ {% endfor %} + +
+ +
+ +
+
+
-{% endblock %} \ No newline at end of file From f7a3741264cb4a3e0386d1e060da9af30e572e9d Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Sat, 13 Jul 2024 22:59:21 -0400 Subject: [PATCH 03/15] updating import syntax to pass isort test --- taggit/admin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index 13a15568..94591397 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -1,11 +1,12 @@ from django.contrib import admin from django.db import transaction -from django.http import JsonResponse -from django.shortcuts import render, redirect -from .forms import MergeTagsForm -from taggit.models import Tag, TaggedItem +from django.shortcuts import redirect, render from django.urls import path +from taggit.models import Tag, TaggedItem + +from .forms import MergeTagsForm + class TaggedItemInline(admin.StackedInline): model = TaggedItem From 8f98db8932270c10cd778f9827f1377c908ee3e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 03:30:45 +0000 Subject: [PATCH 04/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- taggit/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/taggit/admin.py b/taggit/admin.py index 94591397..7dc9df6f 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -32,6 +32,9 @@ def get_urls(self): ] return custom_urls + urls + @admin.action( + description="Merge selected tags" + ) def render_tag_form(self, request, queryset): selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME) if not selected: @@ -82,4 +85,3 @@ def merge_tags_view(self, request): return render(request, "admin/taggit/merge_tags_form.html", context) - render_tag_form.short_description = "Merge selected tags" From 1641737632909ded29c34cbb69db24d6b27ccb3f Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Sat, 13 Jul 2024 23:43:39 -0400 Subject: [PATCH 05/15] updating docs and CHANGELOG --- CHANGELOG.rst | 1 + docs/admin.rst | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 28f5cf50..8073a416 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Changelog We believe that this should not cause a noticable performance change, and the number of queries involved should not change. * Add Django 5.0 support (no code changes were needed, but now we test this release). * Add Python 3.12 support +* Added functionality for tag merging 5.0.1 (2023-10-26) ~~~~~~~~~~~~~~~~~~ diff --git a/docs/admin.rst b/docs/admin.rst index ea4c0cf6..6337f733 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -39,3 +39,19 @@ method to the :class:`~django.contrib.admin.ModelAdmin`, using def tag_list(self, obj): return u", ".join(o.name for o in obj.tags.all()) + + +Merging tags in the admin +======================= + +Functionality has been added to the admin app to allow for tag "merging". +Really what is happening is a "find and replace" where the selected tags are being used. + +To merge your tags follow these steps: + +1. Navigate to the Tags page inside of the Taggit app +2. Select the tags that you want to merge +3. Use the dropdown action list and select `Merge selected tags` and then click `Go` +4. This will redirect you onto a new page where you can insert the new tag name. +5. Click `Merge Tags` +6. This will redirect you back to the tag list \ No newline at end of file From f68d667f3d972d9c0127ef65d9569fd1dc8917b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 03:44:49 +0000 Subject: [PATCH 06/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/admin.rst | 4 ++-- taggit/admin.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/admin.rst b/docs/admin.rst index 6337f733..486cbf24 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -44,7 +44,7 @@ method to the :class:`~django.contrib.admin.ModelAdmin`, using Merging tags in the admin ======================= -Functionality has been added to the admin app to allow for tag "merging". +Functionality has been added to the admin app to allow for tag "merging". Really what is happening is a "find and replace" where the selected tags are being used. To merge your tags follow these steps: @@ -54,4 +54,4 @@ To merge your tags follow these steps: 3. Use the dropdown action list and select `Merge selected tags` and then click `Go` 4. This will redirect you onto a new page where you can insert the new tag name. 5. Click `Merge Tags` -6. This will redirect you back to the tag list \ No newline at end of file +6. This will redirect you back to the tag list diff --git a/taggit/admin.py b/taggit/admin.py index 7dc9df6f..04e7d7fd 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -32,9 +32,7 @@ def get_urls(self): ] return custom_urls + urls - @admin.action( - description="Merge selected tags" - ) + @admin.action(description="Merge selected tags") def render_tag_form(self, request, queryset): selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME) if not selected: @@ -84,4 +82,3 @@ def merge_tags_view(self, request): } return render(request, "admin/taggit/merge_tags_form.html", context) - From 198855879bd7133434f116dfd87659691bdfd748 Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Mon, 15 Jul 2024 00:59:28 -0400 Subject: [PATCH 07/15] updating our merge_tags_view method - We do not need to get the tag ids from the query params - Instead we just grab from session data --- sample_taggit/settings.py | 12 ++++++++++-- taggit/admin.py | 21 ++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/sample_taggit/settings.py b/sample_taggit/settings.py index 0cc0de84..bd1183fe 100644 --- a/sample_taggit/settings.py +++ b/sample_taggit/settings.py @@ -76,9 +76,17 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { + # "default": { + # "ENGINE": "django.db.backends.sqlite3", + # "NAME": BASE_DIR / "db.sqlite3", + # } "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": "django.db.backends.postgresql", + "NAME": "sample_taggit_app", + "USER": "postgres", + "PASSWORD": "postgres", + "HOST": "localhost", + "PORT": "5432", } } diff --git a/taggit/admin.py b/taggit/admin.py index 7dc9df6f..8d8084ec 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -32,9 +32,7 @@ def get_urls(self): ] return custom_urls + urls - @admin.action( - description="Merge selected tags" - ) + @admin.action(description="Merge selected tags") def render_tag_form(self, request, queryset): selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME) if not selected: @@ -42,21 +40,14 @@ def render_tag_form(self, request, queryset): return redirect(request.get_full_path()) selected_tag_ids = ",".join(selected) - redirect_url = ( - f"{request.get_full_path()}merge-tags/?selected_tags={selected_tag_ids}" - ) + redirect_url = f"{request.get_full_path()}merge-tags/" + + request.session["selected_tag_ids"] = selected_tag_ids return redirect(redirect_url) def merge_tags_view(self, request): - if request.method == "GET": - selected_tag_ids = request.GET.get("selected_tags", "").split(",") - - # store selected_tag_ids in session data until they are merged - request.session["selected_tag_ids"] = selected_tag_ids - else: - selected_tag_ids = request.session.get("selected_tag_ids", []) - + selected_tag_ids = request.session.get("selected_tag_ids", "").split(",") if request.method == "POST": form = MergeTagsForm(request.POST) if form.is_valid(): @@ -71,6 +62,7 @@ def merge_tags_view(self, request): tagged_item.save() # tag.delete() #this will delete the selected tags after merge + self.message_user(request, "Tags have been merged") # clear the selected_tag_ids from session after merge is complete request.session.pop("selected_tag_ids", None) return redirect("..") @@ -84,4 +76,3 @@ def merge_tags_view(self, request): } return render(request, "admin/taggit/merge_tags_form.html", context) - From f54d8c973c76a78fd51ee0382208a1c554f94f2b Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Mon, 15 Jul 2024 01:27:06 -0400 Subject: [PATCH 08/15] adding a filter and update method instead of save for tags merge --- taggit/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index 8d8084ec..1580ef55 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -59,8 +59,8 @@ def merge_tags_view(self, request): tagged_items = TaggedItem.objects.filter(tag=tag) for tagged_item in tagged_items: tagged_item.tag = new_tag - tagged_item.save() - # tag.delete() #this will delete the selected tags after merge + TaggedItem.objects.filter(tag=tag).update(tag=new_tag) + # tag.delete() #this will delete the selected tags after merge...leaving out for now self.message_user(request, "Tags have been merged") # clear the selected_tag_ids from session after merge is complete From cbf9bd219ac90578509ce11d163379add2cd9f9d Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Tue, 16 Jul 2024 22:59:13 -0400 Subject: [PATCH 09/15] removing the test postgres data from the sample app --- sample_taggit/settings.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/sample_taggit/settings.py b/sample_taggit/settings.py index bd1183fe..0cc0de84 100644 --- a/sample_taggit/settings.py +++ b/sample_taggit/settings.py @@ -76,17 +76,9 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { - # "default": { - # "ENGINE": "django.db.backends.sqlite3", - # "NAME": BASE_DIR / "db.sqlite3", - # } "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "sample_taggit_app", - "USER": "postgres", - "PASSWORD": "postgres", - "HOST": "localhost", - "PORT": "5432", + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } From d0fe810aa8ed4ad99c4fa9efae42481118d106f2 Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Wed, 17 Jul 2024 22:40:34 -0400 Subject: [PATCH 10/15] Addressing PR comments - Updated the success message to user - cleaned up some print statements - updated the form max_length to match the taggit model - updated the form html to extend the base so the styling matched --- taggit/admin.py | 5 ++--- taggit/forms.py | 2 +- taggit/templates/admin/taggit/merge_tags_form.html | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index 1580ef55..f55e5624 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -62,17 +62,16 @@ def merge_tags_view(self, request): TaggedItem.objects.filter(tag=tag).update(tag=new_tag) # tag.delete() #this will delete the selected tags after merge...leaving out for now - self.message_user(request, "Tags have been merged") + self.message_user(request, "Tags have been merged", level="success") # clear the selected_tag_ids from session after merge is complete request.session.pop("selected_tag_ids", None) + return redirect("..") else: - print(f"Form errors: {form.errors}") self.message_user(request, "Form is invalid.", level="error") context = { "form": MergeTagsForm(), "selected_tag_ids": selected_tag_ids, } - return render(request, "admin/taggit/merge_tags_form.html", context) diff --git a/taggit/forms.py b/taggit/forms.py index 08a5c832..11bd8837 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -57,6 +57,6 @@ def has_changed(self, initial_value, data_value): class MergeTagsForm(forms.Form): new_tag_name = forms.CharField( label="New Tag Name", - max_length=255, + max_length=100, widget=forms.TextInput(attrs={"id": "id_new_tag_name"}), ) diff --git a/taggit/templates/admin/taggit/merge_tags_form.html b/taggit/templates/admin/taggit/merge_tags_form.html index 8e038f39..a8cf4e16 100644 --- a/taggit/templates/admin/taggit/merge_tags_form.html +++ b/taggit/templates/admin/taggit/merge_tags_form.html @@ -1,3 +1,4 @@ +{% extends "admin/base.html" %} {% block content %}
@@ -20,10 +21,11 @@ {% endfor %}
- +
+{% endblock %} From 954cb5d1833a761457e3a8d16ad271ddaecb9c8a Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Wed, 17 Jul 2024 23:24:54 -0400 Subject: [PATCH 11/15] Fixing failing test for docs --- docs/admin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin.rst b/docs/admin.rst index 486cbf24..22242c33 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -42,7 +42,7 @@ method to the :class:`~django.contrib.admin.ModelAdmin`, using Merging tags in the admin -======================= +========================= Functionality has been added to the admin app to allow for tag "merging". Really what is happening is a "find and replace" where the selected tags are being used. From 4437d489104c0e95c86a0c19884d8077f395d857 Mon Sep 17 00:00:00 2001 From: Miguel Johnson Date: Fri, 19 Jul 2024 18:25:34 -0400 Subject: [PATCH 12/15] adding a small note under text field --- taggit/forms.py | 1 + taggit/templates/admin/taggit/merge_tags_form.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/taggit/forms.py b/taggit/forms.py index 11bd8837..863fe943 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -59,4 +59,5 @@ class MergeTagsForm(forms.Form): label="New Tag Name", max_length=100, widget=forms.TextInput(attrs={"id": "id_new_tag_name"}), + help_text="Enter new or existing tag name", ) diff --git a/taggit/templates/admin/taggit/merge_tags_form.html b/taggit/templates/admin/taggit/merge_tags_form.html index a8cf4e16..224cfc95 100644 --- a/taggit/templates/admin/taggit/merge_tags_form.html +++ b/taggit/templates/admin/taggit/merge_tags_form.html @@ -19,7 +19,7 @@ {% endif %} {% endfor %} - +

Enter new or existing tag name

From 15ed6e55ae5af6b92bc2a464dddc2f6414189800 Mon Sep 17 00:00:00 2001 From: Raphael Gaschignard Date: Thu, 25 Jul 2024 22:41:52 +1000 Subject: [PATCH 13/15] Clean up documentation --- CHANGELOG.rst | 2 +- docs/admin.rst | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8073a416..53cc11ec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,9 +8,9 @@ Changelog The previous behavior for this was that by default tag items were not ordered. In practice tag items often end up ordered by creation date anyways, just due to how databases work, but this was not a guarantee. If you wish to have the old behavior, set ``ordering=[]`` to your ``TaggableManager`` instance. We believe that this should not cause a noticable performance change, and the number of queries involved should not change. +* Added the ability to merge tags via the admin * Add Django 5.0 support (no code changes were needed, but now we test this release). * Add Python 3.12 support -* Added functionality for tag merging 5.0.1 (2023-10-26) ~~~~~~~~~~~~~~~~~~ diff --git a/docs/admin.rst b/docs/admin.rst index 22242c33..cb8084ca 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -42,10 +42,9 @@ method to the :class:`~django.contrib.admin.ModelAdmin`, using Merging tags in the admin -========================= +~~~~~~~~~~~~~~~~~~~~~~~~~ -Functionality has been added to the admin app to allow for tag "merging". -Really what is happening is a "find and replace" where the selected tags are being used. +Functionality has been added to the admin app to allow for tag "merging". Multiple tags can be selected, and all of their usages will be replaced by the tag that you choose to use. To merge your tags follow these steps: From c6c7bb219d81a4c9a191505486dc5241c03b7357 Mon Sep 17 00:00:00 2001 From: Raphael Gaschignard Date: Fri, 26 Jul 2024 10:59:37 +1000 Subject: [PATCH 14/15] Fix issue when merging tags when the item has both the old and new tag --- taggit/admin.py | 17 ++++++--- .../admin/taggit/merge_tags_form.html | 2 +- tests/test_admin.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index f55e5624..408a3d42 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -27,7 +27,7 @@ def get_urls(self): path( "merge-tags/", self.admin_site.admin_view(self.merge_tags_view), - name="merge_tags", + name="taggit_tag_merge_tags", ), ] return custom_urls + urls @@ -58,9 +58,18 @@ def merge_tags_view(self, request): tag = Tag.objects.get(id=tag_id) tagged_items = TaggedItem.objects.filter(tag=tag) for tagged_item in tagged_items: - tagged_item.tag = new_tag - TaggedItem.objects.filter(tag=tag).update(tag=new_tag) - # tag.delete() #this will delete the selected tags after merge...leaving out for now + if TaggedItem.objects.filter( + tag=new_tag, + content_type=tagged_item.content_type, + object_id=tagged_item.object_id, + ).exists(): + # we have the new tag as well, so we can just + # remove the tag association + tagged_item.delete() + else: + # point this taggedItem to the new one + tagged_item.tag = new_tag + tagged_item.save() self.message_user(request, "Tags have been merged", level="success") # clear the selected_tag_ids from session after merge is complete diff --git a/taggit/templates/admin/taggit/merge_tags_form.html b/taggit/templates/admin/taggit/merge_tags_form.html index 224cfc95..3a57d4be 100644 --- a/taggit/templates/admin/taggit/merge_tags_form.html +++ b/taggit/templates/admin/taggit/merge_tags_form.html @@ -6,7 +6,7 @@
{% csrf_token %} {% for field in form %}
diff --git a/tests/test_admin.py b/tests/test_admin.py index 3b33c829..be835623 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse +from taggit.models import Tag from .models import Food @@ -10,6 +11,11 @@ def setUp(self): super().setUp() self.apple = Food.objects.create(name="apple") self.apple.tags.add("Red", "red") + self.pear = Food.objects.create(name="pear") + self.pear.tags.add("red", "RED") + self.peach = Food.objects.create(name="peach") + self.peach.tags.add("red", "Yellow") + user = User.objects.create_superuser( username="admin", email="admin@mailinator.com", password="password" ) @@ -40,3 +46,33 @@ def test_get_change(self): reverse("admin:tests_food_change", args=(self.apple.pk,)) ) self.assertEqual(response.status_code, 200) + + def test_tag_merging(self): + response = self.client.get(reverse("admin:taggit_tag_changelist")) + + # merging red and RED into Red + pks_to_select = [Tag.objects.get(name="red").pk, Tag.objects.get(name="RED").pk] + response = self.client.post( + reverse("admin:taggit_tag_changelist"), + data={"action": "render_tag_form", "_selected_action": pks_to_select}, + ) + # we're redirecting + self.assertEqual(response.status_code, 302) + # make sure what we expected got into the session keys + assert "selected_tag_ids" in self.client.session.keys() + self.assertEqual( + self.client.session["selected_tag_ids"], ",".join(map(str, pks_to_select)) + ) + + # let's do the actual merge operation + response = self.client.post( + reverse("admin:taggit_tag_merge_tags"), {"new_tag_name": "Red"} + ) + self.assertEqual(response.status_code, 302) + + # time to check the result of the merges + self.assertSetEqual({tag.name for tag in self.apple.tags.all()}, {"Red"}) + self.assertSetEqual({tag.name for tag in self.pear.tags.all()}, {"Red"}) + self.assertSetEqual( + {tag.name for tag in self.peach.tags.all()}, {"Yellow", "Red"} + ) From ba489937851110ac290097524c0a3e50d2981c3d Mon Sep 17 00:00:00 2001 From: Raphael Gaschignard Date: Fri, 26 Jul 2024 11:12:38 +1000 Subject: [PATCH 15/15] isort fix --- tests/test_admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_admin.py b/tests/test_admin.py index be835623..d36a6249 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse + from taggit.models import Tag from .models import Food