A reusable Django application for simple tagging with comprehensive manager and form support.
—
Utility functions for tag parsing and formatting, plus management commands for maintaining tag data integrity. Django-taggit provides various utilities for customizing tag behavior and management commands for database maintenance.
Functions for converting between string representations and tag lists.
def parse_tags(tagstring):
"""
Parse tag string into list of tag names.
Supports comma-separated and space-separated formats,
with quoted strings for multi-word tags.
Parameters:
- tagstring (str): String containing tags, or None
Returns:
list: Sorted list of unique tag names (empty list if input is None/empty)
Examples:
- "python, django" -> ["django", "python"]
- "python django" -> ["django", "python"]
- '"web development", python' -> ["python", "web development"]
- None -> []
- "" -> []
"""
def edit_string_for_tags(tags):
"""
Convert tag list to editable string representation.
Creates a string suitable for form editing that can be
parsed back to the same tag list.
Parameters:
- tags: List of Tag objects or tag names
Returns:
str: Formatted string representation of tags
"""Lower-level functions used internally by the parsing system.
def _parse_tags(tagstring):
"""
Internal tag parsing implementation.
Handles the actual parsing logic with support for
quoted strings and various delimiters.
"""
def _edit_string_for_tags(tags):
"""
Internal tag formatting implementation.
Handles the conversion of tag objects to formatted strings
with proper quoting for multi-word tags.
"""
def split_strip(string, delimiter=","):
"""
Split string on delimiter and strip whitespace.
Parameters:
- string (str): String to split
- delimiter (str): Delimiter character (default: comma)
Returns:
list: List of non-empty stripped strings
"""Functions for customizing tag behavior through Django settings.
def get_func(key, default):
"""
Get customizable function from Django settings.
Allows overriding default tag parsing and formatting
functions through Django settings.
Parameters:
- key (str): Settings key name
- default (callable): Default function to use
Returns:
callable: Function from settings or default
"""Decorators for tag manager methods.
def require_instance_manager(func):
"""
Decorator ensuring method is called on instance manager.
Prevents calling instance-specific methods on class-level
tag managers, raising TypeError if misused.
Parameters:
- func (callable): Method to decorate
Returns:
callable: Decorated method with instance validation
"""Command to clean up tags that are no longer associated with any objects.
# Command class
class Command(BaseCommand):
"""
Remove orphaned tags from the database.
Finds and deletes tags that have no associated TaggedItem
relationships, cleaning up unused tags automatically.
"""
help = "Remove orphaned tags"
def handle(self, *args, **options):
"""Execute the orphaned tag cleanup process."""Usage:
# Remove all orphaned tags
python manage.py remove_orphaned_tags
# Example output:
# Successfully removed 15 orphaned tagsCommand to identify and merge duplicate tags based on case-insensitive matching.
# Command class
class Command(BaseCommand):
"""
Remove duplicate tags based on case insensitivity.
Identifies tags with the same name (case-insensitive) and
merges them into a single tag, preserving all relationships.
"""
help = "Identify and remove duplicate tags based on case insensitivity"
def handle(self, *args, **kwargs):
"""Execute the tag deduplication process."""
def _deduplicate_tags(self, existing_tag, tag_to_remove):
"""
Merge two duplicate tags.
Parameters:
- existing_tag: Tag to keep
- tag_to_remove: Tag to merge and delete
"""Usage:
# Deduplicate tags (requires TAGGIT_CASE_INSENSITIVE = True)
python manage.py deduplicate_tags
# Example output:
# Tag deduplication complete.Creating custom tag parsing functions for specialized formats.
def custom_parse_tags(tagstring):
"""
Custom tag parser with special rules.
Example: Parse hashtag-style tags (#python #django)
"""
if not tagstring:
return []
# Extract hashtags
import re
hashtags = re.findall(r'#(\w+)', tagstring)
# Also parse regular comma-separated tags
regular_tags = []
cleaned = re.sub(r'#\w+', '', tagstring)
if cleaned.strip():
from taggit.utils import _parse_tags
regular_tags = _parse_tags(cleaned)
# Combine and deduplicate
all_tags = list(set(hashtags + regular_tags))
all_tags.sort()
return all_tags
def custom_format_tags(tags):
"""
Custom tag formatter for display.
Example: Format as hashtags for social media style
"""
names = [f"#{tag.name}" for tag in tags]
return " ".join(sorted(names))
# Configure in settings.py
TAGGIT_TAGS_FROM_STRING = 'myapp.utils.custom_parse_tags'
TAGGIT_STRING_FROM_TAGS = 'myapp.utils.custom_format_tags'Different parsing strategies for various use cases.
def scientific_tag_parser(tagstring):
"""
Parser for scientific/academic tags with categories.
Format: "category:value, category:value"
Example: "field:biology, method:pcr, organism:ecoli"
"""
if not tagstring:
return []
tags = []
parts = [part.strip() for part in tagstring.split(',')]
for part in parts:
if ':' in part:
# Structured tag
category, value = part.split(':', 1)
tags.append(f"{category.strip()}:{value.strip()}")
else:
# Regular tag
tags.append(part)
return sorted(set(tags))
def hierarchical_tag_parser(tagstring):
"""
Parser for hierarchical tags with path-like structure.
Format: "parent/child/grandchild"
Example: "technology/web/frontend, technology/web/backend"
"""
if not tagstring:
return []
tags = []
parts = [part.strip() for part in tagstring.split(',')]
for part in parts:
# Add all levels of hierarchy
levels = part.split('/')
for i in range(len(levels)):
hierarchical_tag = '/'.join(levels[:i+1])
tags.append(hierarchical_tag)
return sorted(set(tags))Django settings that customize tagging behavior.
# settings.py
# Case-insensitive tag matching and creation
TAGGIT_CASE_INSENSITIVE = True
# Strip unicode characters when creating slugs
TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING = True
# Custom tag parsing function
TAGGIT_TAGS_FROM_STRING = 'myapp.utils.custom_parse_tags'
# Custom tag formatting function
TAGGIT_STRING_FROM_TAGS = 'myapp.utils.custom_format_tags'How different settings affect tag behavior.
# With TAGGIT_CASE_INSENSITIVE = True
tag1 = Tag.objects.create(name="Python")
tag2 = Tag.objects.get_or_create(name="python") # Gets existing tag1
# With TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING = True
tag = Tag.objects.create(name="café")
print(tag.slug) # "cafe" instead of "café"
# With custom parsing function
article.tags.set("#python #django #tutorial") # Uses custom parserProgrammatic approaches to tag maintenance beyond management commands.
from django.db.models import Count
from taggit.models import Tag, TaggedItem
def cleanup_unused_tags():
"""Remove tags with no associated items."""
unused_tags = Tag.objects.annotate(
usage_count=Count('tagged_items')
).filter(usage_count=0)
count = unused_tags.count()
unused_tags.delete()
return count
def merge_similar_tags(similarity_threshold=0.8):
"""Merge tags with similar names."""
from difflib import SequenceMatcher
tags = list(Tag.objects.all())
merged_count = 0
for i, tag1 in enumerate(tags):
for tag2 in tags[i+1:]:
similarity = SequenceMatcher(
None, tag1.name.lower(), tag2.name.lower()
).ratio()
if similarity >= similarity_threshold:
# Merge tag2 into tag1
TaggedItem.objects.filter(tag=tag2).update(tag=tag1)
tag2.delete()
merged_count += 1
tags.remove(tag2)
return merged_count
def normalize_tag_names():
"""Normalize tag names (lowercase, strip whitespace)."""
tags = Tag.objects.all()
updated_count = 0
for tag in tags:
normalized_name = tag.name.strip().lower()
if tag.name != normalized_name:
# Check if normalized version already exists
existing = Tag.objects.filter(name=normalized_name).first()
if existing and existing != tag:
# Merge into existing tag
TaggedItem.objects.filter(tag=tag).update(tag=existing)
tag.delete()
else:
# Update tag name
tag.name = normalized_name
tag.save()
updated_count += 1
return updated_countFunctions for analyzing tag usage patterns.
def get_tag_statistics():
"""Get comprehensive tag usage statistics."""
from django.db.models import Count, Avg
stats = Tag.objects.aggregate(
total_tags=Count('id'),
avg_usage=Avg('tagged_items__count')
)
# Most used tags
popular_tags = Tag.objects.annotate(
usage_count=Count('tagged_items')
).order_by('-usage_count')[:10]
# Unused tags
unused_count = Tag.objects.filter(tagged_items__isnull=True).count()
return {
'total_tags': stats['total_tags'],
'average_usage': stats['avg_usage'] or 0,
'popular_tags': list(popular_tags.values('name', 'usage_count')),
'unused_tags': unused_count
}
def find_tag_patterns():
"""Analyze tag naming patterns."""
import re
from collections import Counter
tags = Tag.objects.values_list('name', flat=True)
# Find common prefixes
prefixes = Counter()
for tag in tags:
if ':' in tag:
prefix = tag.split(':')[0]
prefixes[prefix] += 1
# Find common word patterns
words = []
for tag in tags:
words.extend(re.findall(r'\w+', tag.lower()))
word_freq = Counter(words)
return {
'common_prefixes': dict(prefixes.most_common(10)),
'common_words': dict(word_freq.most_common(20))
}Setting up automated tag maintenance with Django management commands.
# management/commands/maintain_tags.py
from django.core.management.base import BaseCommand
from django.core.management import call_command
class Command(BaseCommand):
help = 'Comprehensive tag maintenance'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without making changes'
)
def handle(self, *args, **options):
dry_run = options['dry_run']
if not dry_run:
# Remove orphaned tags
call_command('remove_orphaned_tags')
# Deduplicate tags if case insensitive mode is enabled
from django.conf import settings
if getattr(settings, 'TAGGIT_CASE_INSENSITIVE', False):
call_command('deduplicate_tags')
# Show statistics
from myapp.utils import get_tag_statistics
stats = get_tag_statistics()
self.stdout.write(f"Total tags: {stats['total_tags']}")
self.stdout.write(f"Unused tags: {stats['unused_tags']}")
self.stdout.write(f"Average usage: {stats['average_usage']:.1f}")
# Usage: python manage.py maintain_tags --dry-runInstall with Tessl CLI
npx tessl i tessl/pypi-django-taggit