{"id":1403,"date":"2015-01-01T20:14:31","date_gmt":"2015-01-02T01:14:31","guid":{"rendered":"http:\/\/yuguangzhang.com\/blog\/?p=1403"},"modified":"2015-01-01T20:19:33","modified_gmt":"2015-01-02T01:19:33","slug":"merge-tags-with-django-taggit","status":"publish","type":"post","link":"http:\/\/yuguangzhang.com\/blog\/merge-tags-with-django-taggit\/","title":{"rendered":"Merge Tags with django-taggit"},"content":{"rendered":"<p>Today I cleaned up the database for <a href=\"http:\/\/fiddlesalad.com\/\">Fiddle Salad<\/a> and <a href=\"http:\/\/pythonfiddle.com\">Python Fiddle<\/a>. Both use <a href=\"https:\/\/github.com\/yuguang\/django-cloud-ide\">the same Django back-end for code storage<\/a>. While browsing tags, I noticed that often both CamelCase and lowercase spellings were used for tags. Since I was working on a tag suggest feature earlier this week, I decided to convert all tags to lowercase so that tag suggestions would not be redundant. An additional benefit is further normalization of the data. Fortunately, I found a fork of django-taggit, the Django app I used for tagging, that supported enforcing lowercase tags everywhere. Two management commands were already present for normalizing data, <tt>mergetags<\/tt> and <tt>lowercasetags<\/tt>. django-taggit had two fields for each tag, a name and slug. <tt>lowercasetags<\/tt> converted all tag names to their lowercase form. <tt>mergetags<\/tt> takes at least two tag slugs and merges all tags into a single destination tag. The result is that all associations are moved to a single tag. While <tt>mergetags<\/tt> is suitable for manually resolving redundant data, the number of tags on Fiddle Salad is too large. I wrote an command to automate this process:<\/p>\n<p>[cc lang=&#8221;python&#8221;]<br \/>\nfrom django.core.management.base import BaseCommand, CommandError<br \/>\nfrom taggit.models import Tag, TaggedItem<br \/>\nfrom django.core.exceptions import ObjectDoesNotExist<\/p>\n<p>class Command(BaseCommand):<br \/>\n    help = &#8216;merges all tags automatically&#8217;<\/p>\n<p>    def merge(self, extra_slugs, dest_slug):<br \/>\n        try:<br \/>\n            dest_tag = Tag.objects.get(slug=dest_slug)<br \/>\n        except ObjectDoesNotExist:<br \/>\n            raise CommandError(&#8216;Destination Tag &#8220;%s&#8221; does not exist&#8217; % dest_slug)<\/p>\n<p>        for slug in extra_slugs:<br \/>\n            try:<br \/>\n                tag = Tag.objects.get(slug=slug)<br \/>\n            except ObjectDoesNotExist:<br \/>\n                raise CommandError(&#8216;Tag &#8220;%s&#8221; does not exist&#8217; % slug)<\/p>\n<p>            items = TaggedItem.objects.filter(tag=tag)<br \/>\n            count = items.count()<br \/>\n            for i, item in enumerate(items):<br \/>\n                if i % 20 == 0:<br \/>\n                    self.stdout.write(&#8216;Merging %s %d\/%d\\n&#8217; % (slug, i+1, count))<br \/>\n                obj = item.content_object<br \/>\n                if not obj:<br \/>\n                    return<br \/>\n                obj.tags.remove(tag)<br \/>\n                obj.tags.add(dest_tag)<br \/>\n            tag.delete()<\/p>\n<p>            self.stdout.write(&#8216;Successfully merged tags into &#8220;%s&#8221;\\n&#8217; % dest_slug)<\/p>\n<p>    def handle(self, *args, **options):<br \/>\n        for tag in Tag.objects.all():<br \/>\n            if Tag.objects.filter(name=tag.name).count() > 1:<br \/>\n                tags = Tag.objects.filter(name=tag.name).order_by(&#8216;id&#8217;)<br \/>\n                dest = tags[0].slug<br \/>\n                extras = []<br \/>\n                for tag in tags[1::]:<br \/>\n                    extras.append(tag.slug)<br \/>\n                self.merge(extras, dest)<br \/>\n[\/cc]<br \/>\nBecause performance is not a concern for a single-time data processing script, I did not bother to optimize the queries nor run-time. This script would be useful for anyone who wants to normalize tags in the same manner, so it is in a <a href=\"https:\/\/github.com\/yuguang\/django-taggit\/blob\/8e87af2d02adf7784a1c931764ee00dcb06d20ae\/taggit\/management\/commands\/mergealltags.py\">git repository<\/a>. Finally, I tested the new command on a clone of the production database.<br \/>\n<code><br \/>\nbash-4.1$ python manage.py lowercasetags<br \/>\nLowercasing 1\/1621<br \/>\nLowercasing 21\/1621<br \/>\n.<br \/>\n.<br \/>\n.<br \/>\nLowercasing 1621\/1621<br \/>\nbash-4.1$ python manage.py mergealltags<br \/>\nMerging jquery_1 1\/46<br \/>\nMerging jquery_1 21\/46<br \/>\nMerging jquery_1 41\/46<br \/>\nSuccessfully merged tags into \"jquery\"<br \/>\nSuccessfully merged tags into \"jquery\"<br \/>\nMerging stylus_1 1\/7<br \/>\nSuccessfully merged tags into \"stylus\"<br \/>\nMerging hello_1 1\/10<br \/>\nSuccessfully merged tags into \"hello\"<br \/>\nMerging test_1 1\/147<br \/>\nMerging test_1 21\/147<br \/>\nMerging test_1 41\/147<br \/>\nMerging test_1 61\/147<br \/>\nMerging test_1 81\/147<br \/>\nMerging test_1 101\/147<br \/>\nMerging test_1 121\/147<br \/>\nMerging test_1 141\/147<br \/>\nSuccessfully merged tags into \"test\"<br \/>\nMerging me_1 1\/2<br \/>\nSuccessfully merged tags into \"me\"<br \/>\nMerging no_1 1\/5<br \/>\nSuccessfully merged tags into \"no\"<br \/>\nMerging one_1 1\/16<br \/>\nSuccessfully merged tags into \"one\"<br \/>\nMerging things_1 1\/3<br \/>\nSuccessfully merged tags into \"things\"<br \/>\nMerging learning_1 1\/4<br \/>\nSuccessfully merged tags into \"learning\"<br \/>\nSuccessfully merged tags into \"body\"<br \/>\nMerging week-one_1 1\/1<br \/>\nSuccessfully merged tags into \"week-one\"<br \/>\nMerging studio_1 1\/33<br \/>\nMerging studio_1 21\/33<br \/>\nSuccessfully merged tags into \"studio\"<br \/>\nMerging internet_1 1\/36<br \/>\nMerging internet_1 21\/36<br \/>\nSuccessfully merged tags into \"internet\"<br \/>\nMerging assignment_1 1\/6<br \/>\nSuccessfully merged tags into \"assignment\"<br \/>\nMerging homework_1 1\/6<br \/>\nSuccessfully merged tags into \"homework\"<br \/>\nMerging lessons_1 1\/1<br \/>\nSuccessfully merged tags into \"lessons\"<br \/>\nMerging code_1 1\/12<br \/>\nMerging tags_1 1\/7<br \/>\nSuccessfully merged tags into \"tags\"<br \/>\nMerging two_1 1\/6<br \/>\nSuccessfully merged tags into \"two\"<br \/>\nMerging salcedo_1 1\/3<br \/>\nSuccessfully merged tags into \"salcedo\"<br \/>\nMerging page_1 1\/15<br \/>\nSuccessfully merged tags into \"page\"<br \/>\nMerging music_1 1\/4<br \/>\nSuccessfully merged tags into \"music\"<br \/>\nMerging table_1 1\/7<br \/>\nSuccessfully merged tags into \"table\"<br \/>\nMerging band_1 1\/9<br \/>\nMerging texas_1 1\/1<br \/>\nSuccessfully merged tags into \"texas\"<br \/>\nMerging biography_1 1\/2<br \/>\nMerging assignment-two_1 1\/2<br \/>\nSuccessfully merged tags into \"assignment-two\"<br \/>\nMerging website_1 1\/9<br \/>\nMerging a_1 1\/2<br \/>\nSuccessfully merged tags into \"a\"<br \/>\nMerging words_1 1\/1<br \/>\nSuccessfully merged tags into \"words\"<br \/>\nMerging section_1 1\/2<br \/>\nSuccessfully merged tags into \"section\"<br \/>\nMerging header_1 1\/1<br \/>\nSuccessfully merged tags into \"header\"<br \/>\nMerging ui_1 1\/4<br \/>\nSuccessfully merged tags into \"ui\"<br \/>\nMerging first_1 1\/8<br \/>\nSuccessfully merged tags into \"first\"<br \/>\nMerging random_1 1\/1<br \/>\nSuccessfully merged tags into \"random\"<br \/>\nMerging internet-studio_1 1\/5<br \/>\nSuccessfully merged tags into \"internet-studio\"<br \/>\nMerging angularjs_1 1\/11<br \/>\nSuccessfully merged tags into \"angularjs\"<br \/>\nMerging i_1 1\/2<br \/>\nSuccessfully merged tags into \"i\"<br \/>\nMerging lines_1 1\/1<br \/>\nSuccessfully merged tags into \"lines\"<br \/>\nMerging row_1 1\/1<br \/>\nSuccessfully merged tags into \"row\"<br \/>\nMerging alex-alpha_1 1\/1<br \/>\nSuccessfully merged tags into \"alex-alpha\"<br \/>\nMerging assignment-one_1 1\/2<br \/>\nSuccessfully merged tags into \"assignment-one\"<br \/>\nMerging google_1 1\/1<br \/>\nSuccessfully merged tags into \"google\"<br \/>\nMerging man_1 1\/4<br \/>\nSuccessfully merged tags into \"man\"<br \/>\nMerging nick_1 1\/1<br \/>\nSuccessfully merged tags into \"nick\"<br \/>\nMerging cartoon_1 1\/1<br \/>\nSuccessfully merged tags into \"cartoon\"<br \/>\nMerging batman_1 1\/2<br \/>\nSuccessfully merged tags into \"batman\"<br \/>\nMerging code_1 1\/7<br \/>\nMerging the_1 1\/1<br \/>\nSuccessfully merged tags into \"the\"<br \/>\nMerging animation_1 1\/2<br \/>\nSuccessfully merged tags into \"animation\"<br \/>\nMerging band_1 1\/4<br \/>\nMerging assignment-one-of-three_1 1\/2<br \/>\nSuccessfully merged tags into \"assignment-one-of-three\"<br \/>\nMerging status_1 1\/1<br \/>\nSuccessfully merged tags into \"status\"<br \/>\nMerging python_1 1\/2<br \/>\nSuccessfully merged tags into \"python\"<br \/>\nMerging cat_1 1\/1<br \/>\nSuccessfully merged tags into \"cat\"<br \/>\nMerging none_1 1\/7<br \/>\nSuccessfully merged tags into \"none\"<br \/>\nMerging adam_1 1\/2<br \/>\nSuccessfully merged tags into \"adam\"<br \/>\nMerging school_1 1\/3<br \/>\nSuccessfully merged tags into \"school\"<br \/>\nMerging website_1 1\/9<br \/>\nMerging biography_1 1\/2<br \/>\nMerging bootstrap_1 1\/8<br \/>\nSuccessfully merged tags into \"bootstrap\"<br \/>\nMerging datamill_1 1\/5<br \/>\nSuccessfully merged tags into \"datamill\"<br \/>\nMerging gentoo_1 1\/2<br \/>\nSuccessfully merged tags into \"gentoo\"<br \/>\nMerging dobschal_1 1\/1<br \/>\nSuccessfully merged tags into \"dobschal\"<br \/>\nMerging weimar_1 1\/1<br \/>\nSuccessfully merged tags into \"weimar\"<br \/>\n<\/code><br \/>\nWhen all went fine, I ran <tt>lowercasetags<\/tt> and <tt>mergealltags<\/tt> on both <a href=\"http:\/\/fiddlesalad.com\/\">Fiddle Salad<\/a> and <a href=\"http:\/\/pythonfiddle.com\">Python Fiddle<\/a>. Now I was really impressed with the results as I clicked through the tags on both sites. <a href=\"http:\/\/fiddlesalad.com\/fiddles\/tags\/\">The tags on Fiddle Salad<\/a> were much better organized as they were ordered by popularity. While looking through the tags, I noticed that &#8220;test&#8221; was among the top. I decided to add &#8216;test&#8217; to the list of stopwords for django-taggit. These stopwords are removed during save so that they are not associated with new snippets.<br \/>\nNow that the tags are normalized, I am ready to move on and deploy tag suggestions. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today I cleaned up the database for Fiddle Salad and Python Fiddle. Both use the same Django back-end for code storage. While browsing tags, I noticed that often both CamelCase and lowercase spellings were used for tags. Since I was working on a tag suggest feature earlier this week, I decided to convert all tags [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[203,23,146],"tags":[214,212,213],"class_list":["post-1403","post","type-post","status-publish","format-standard","hentry","category-fiddle-salad","category-programming","category-pythonfiddle","tag-database","tag-django","tag-normalization"],"aioseo_notices":[],"_links":{"self":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts\/1403","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/comments?post=1403"}],"version-history":[{"count":4,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts\/1403\/revisions"}],"predecessor-version":[{"id":1410,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts\/1403\/revisions\/1410"}],"wp:attachment":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/media?parent=1403"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/categories?post=1403"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/tags?post=1403"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}