API Reference

Core Components

FlexiTagMixin

class FlexiTagMixin(models.Model):
    """
    Simple abstract model mixin for tag model generation.

    This mixin doesn't add any fields to the model itself and provides
    clean integration with existing Django patterns. It's used by the
    generate_tag_models command to identify models that should have a
    corresponding tag model generated.

    Benefits of service-only approach:
    - Clean integration with existing Django patterns
    - Clear separation of concerns
    - Full QuerySet composition support
    """

    class Meta:
        abstract = True

TaggableService

The core service class that provides all tagging functionality through instance methods.

class TaggableService:
    """
    Service class for managing tags on model instances.

    Service-only architecture benefits:
    - Works with any QuerySet (preserves existing filters)
    - Clean integration with existing Django patterns
    - Explicit and composable operations
    - Enhanced filtering capabilities
    """

Instance Operations

def add_tag(self, instance, key: str) -> object:
    """
    Add a single tag to an instance.

    Args:
        instance: The model instance to tag
        key: The tag key (string)

    Returns:
        The tagged instance

    Raises:
        TagValidationException: If the tag already exists
    """

def bulk_add_tags(self, instance, keys: list) -> object:
    """
    Add multiple tags to an instance.

    Args:
        instance: The model instance to tag
        keys: List of tag keys (strings)

    Returns:
        The tagged instance

    Raises:
        TagValidationException: If any tag already exists
    """

def bulk_add_tags_with_many_instances(self, instances: QuerySet, keys: list) -> QuerySet:
    """
    Add multiple tags to multiple instances.

    Args:
        instances: QuerySet of model instances to tag
        keys: List of tag keys (strings)

    Returns:
        The QuerySet of tagged instances

    Raises:
        TagValidationException: If any tag already exists on any instance
    """

def remove_tag(self, instance, key: str) -> object:
    """
    Remove a tag from an instance.

    Args:
        instance: The model instance to untag
        key: The tag key (string)

    Returns:
        The tag instance

    Raises:
        TagNotFoundException: If the instance is not tagged
        TagValidationException: If the tag doesn't exist on the instance
    """

def bulk_remove_tags(self, instance, keys: list) -> object:
    """
    Remove multiple tags from an instance.

    Args:
        instance: The model instance to untag
        keys: List of tag keys (strings)

    Returns:
        The tag instance

    Raises:
        TagNotFoundException: If the instance is not tagged
    """

def get_tags(self, instance) -> list:
    """
    Get all tags for an instance.

    Args:
        instance: The model instance

    Returns:
        List of tag keys (strings)
    """

QuerySet Operations

The power of service-only architecture - compose with any existing QuerySet!

def filter_by_tag(self, queryset: QuerySet, key: str) -> QuerySet:
    """
    Filter QuerySet by tag key, preserving existing filters.

    Args:
        queryset: The QuerySet to filter
        key: The tag key to filter by

    Returns:
        Filtered QuerySet

    Example:
        # Compose with existing QuerySet filters
        active_products = Product.objects.filter(is_active=True)
        featured_active = service.filter_by_tag(active_products, 'featured')
    """

def exclude_by_tag(self, queryset: QuerySet, key: str) -> QuerySet:
    """
    Exclude QuerySet by tag key, preserving existing filters.

    Args:
        queryset: The QuerySet to filter
        key: The tag key to exclude by

    Returns:
        Filtered QuerySet
    """

def with_tags(self, queryset: QuerySet) -> QuerySet:
    """
    Add prefetch_related for tag objects, preserving existing QuerySet.
    Use this to avoid N+1 queries when accessing tags.

    Args:
        queryset: The QuerySet to optimize

    Returns:
        QuerySet with prefetched tag data
    """

def filter_by_tags(self, queryset: QuerySet, tags: list) -> QuerySet:
    """
    Filter QuerySet by multiple tags (AND logic).

    Args:
        queryset: The QuerySet to filter
        tags: List of tag keys (all must be present)

    Returns:
        Filtered QuerySet
    """

def filter_by_any_tag(self, queryset: QuerySet, tags: list) -> QuerySet:
    """
    Filter QuerySet by any of the tags (OR logic).

    Args:
        queryset: The QuerySet to filter
        tags: List of tag keys (any can be present)

    Returns:
        Filtered QuerySet
    """

TaggableViewSetMixin

class TaggableViewSetMixin(object):
    """
    Mixin for Django REST Framework ViewSets that adds tag-related endpoints.
    """

    @action(detail=True, methods=["post"])
    def add_tag(self, request, pk=None):
        """
        Add a tag to an instance.

        POST /model/<pk>/add_tag/
        {"key": "tag_key"}
        """

    @action(detail=True, methods=["post"])
    def bulk_add_tag(self, request, pk=None):
        """
        Add multiple tags to an instance.

        POST /model/<pk>/bulk_add_tag/
        {"keys": ["tag1", "tag2"]}
        """

    @action(detail=False, methods=["post"])
    def bulk_add_tags(self, request, pk=None):
        """
        Add multiple tags to multiple instances.

        POST /model/bulk_add_tags/
        {"objects": [1, 2, 3], "keys": ["tag1", "tag2"]}
        """

    @action(detail=True, methods=["post"])
    def remove_tag(self, request, pk=None):
        """
        Remove a tag from an instance.

        POST /model/<pk>/remove_tag/
        {"key": "tag_key"}
        """

    @action(detail=True, methods=["post"])
    def bulk_remove_tags(self, request, pk=None):
        """
        Remove multiple tags from an instance.

        POST /model/<pk>/bulk_remove_tags/
        {"keys": ["tag1", "tag2"]}
        """

    @action(detail=False, methods=["post"])
    def bulk_remove_tags_with_many_instances(self, request, pk=None):
        """
        Remove multiple tags from multiple instances.

        POST /model/bulk_remove_tags_with_many_instances/
        {"objects": [1, 2, 3], "keys": ["tag1", "tag2"]}
        """

Management Commands

generate_tag_models

class Command(BaseCommand):
    """
    Management command to generate tag models for all models inheriting from FlexiTagMixin.

    Usage:
        python manage.py generate_tag_models [--dry-run]

    Options:
        --dry-run: Show what would be generated without creating files
    """

Generated Models

When you run the generate_tag_models command, it creates a new model for each model that inherits from FlexiTagMixin. The generated model will look like this:

class YourModelTag(models.Model):
    """
    Generated tag model for YourModel.
    """
    instance = models.OneToOneField(
        "app_label.YourModel",
        on_delete=models.CASCADE,
        primary_key=True,
    )
    tags = JSONField(default=list)

    class Meta:
        app_label = "app_label"
        db_table = "app_label_yourmodel_tag"
        indexes = [GinIndex(fields=["tags"])]

    def __str__(self):
        return "Tags for {}".format(self.instance)"

Exceptions

Flexi Tag provides customizable exception classes that can inherit from your project’s base exception class.

Configuration

Configure your base exception class in Django settings:

# settings.py
FLEXI_TAG_BASE_EXCEPTION_CLASS = 'myproject.exceptions.MyBaseException'

Available Exceptions

class ProjectBaseException(Exception):
    """
    Base exception class. Can be customized via FLEXI_TAG_BASE_EXCEPTION_CLASS setting.
    Default: Uses DefaultProjectBaseException
    """

class TagNotFoundException(ProjectBaseException):
    """
    Raised when a tag is not found.

    Default message: "Tag not found"
    """

class TagNotDefinedException(ProjectBaseException):
    """
    Raised when a tag key is not provided.

    Default message: "Tag key not defined"
    """

class TagValidationException(ProjectBaseException):
    """
    Raised when tag validation fails.

    Default message: "Tag validation failed"
    """

class ObjectIDsNotDefinedException(ProjectBaseException):
    """
    Raised when object IDs are not provided for bulk operations.

    Default message: "Object IDs not defined"
    """

Usage Examples

from flexi_tag.exceptions import TagNotFoundException, TagValidationException

try:
    service.add_tag(instance, "nonexistent_tag")
except TagNotFoundException as e:
    logger.error(f"Tag error: {e}")

try:
    service.add_tag(instance, "")
except TagValidationException as e:
    logger.error(f"Validation error: {e}")

Compatibility

The library includes compatibility functions to work with different Django versions:

# JSONField location changed in Django 3.1
if django.VERSION >= (3, 1):
    from django.db.models import JSONField
else:
    from django.contrib.postgres.fields import JSONField

Utility Functions

def parse_tag_string(tag_string, delimiter=","):
    """
    Parse a string of tags into a list of cleaned tag names.

    Args:
        tag_string: Comma-separated string of tags
        delimiter: Character to split on (default: comma)

    Returns:
        List of cleaned tag strings
    """

def get_tag_cloud(queryset_or_model, min_count=None, steps=4):
    """
    Generate a tag cloud for the given queryset or model.

    Args:
        queryset_or_model: QuerySet or model class to analyze
        min_count: Minimum tag count to include
        steps: Number of font size steps (1-steps)

    Returns:
        Tags with a 'font_size' attribute based on frequency.
    """

def related_objects_by_tags(obj, model_class, min_tags=1):
    """
    Find objects of the given model class that share tags with obj.

    Args:
        obj: Object with tags to match against
        model_class: Model class to search in
        min_tags: Minimum number of shared tags required

    Returns:
        QuerySet ordered by number of shared tags.
    """