Skip to main content

Add tags to any model in Django via ModelViewSet

Project description

Django Flexi Tag

Build status Documentation status PyPI PyPI - Django version PyPI - Python version PyPI - License

A flexible and efficient tagging system for Django models that allows you to add, remove, and manage tags on any Django model with minimal configuration. Built with a service-only architecture for maximum compatibility and composability.

Features

  • 🚀 Service-Only Architecture: Clean and composable design with full QuerySet compatibility
  • 🔄 Composable Filtering: Preserves existing QuerySet filters when adding tag filtering
  • ⚡ Easy Integration: Works seamlessly with Django REST Framework ViewSets
  • 📦 Flexible Tag Storage: Uses PostgreSQL JSONField for efficient and flexible tag storage
  • 🤖 Automatic Model Generation: Generates auxiliary Tag models for your existing models
  • 📊 Bulk Operations: Support for bulk tag operations on multiple objects
  • 🔧 Custom Exception Support: Configure your own base exception class for seamless integration
  • 🌐 Django Compatibility: Works across multiple Django versions (1.11 to 5.0)
  • 🐍 Python Compatibility: Supports Python 3.5+

Installation

Installation using pip:

pip install dj-flexi-tag

For development and testing:

pip install dj-flexi-tag[dev,test]

For documentation development:

pip install dj-flexi-tag[docs]

For all dependencies:

pip install dj-flexi-tag[dev,test,docs]

Add flexi_tag to your INSTALLED_APPS:

INSTALLED_APPS = (
    # other apps here...
    'flexi_tag',
)

Testing

Run the tests with:

python runtests.py

Use the --verbose flag for more detailed output:

python runtests.py --verbose

Use the --interactive flag for interactive mode:

python runtests.py --interactive

Quick Start

1. Define Your Models with FlexiTagMixin

from django.db import models
from flexi_tag.utils.models import FlexiTagMixin

class Order(FlexiTagMixin):
    name = models.CharField(max_length=100)
    status = models.CharField(max_length=50)
    created_date = models.DateField()

    class Meta:
        app_label = 'myapp'

# Your custom QuerySet methods work perfectly!
class CustomManager(models.Manager):
    def active(self):
        return self.filter(status='active')

class Product(FlexiTagMixin):
    name = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)

    objects = CustomManager()  # Your custom logic is preserved!

    class Meta:
        app_label = 'myapp'

2. Generate Tag Models

Run the management command:

# Standard usage
python manage.py generate_tag_models

# For testing what would be generated
python manage.py generate_tag_models --dry-run

# If models aren't detected after recent changes (force reload)
python manage.py generate_tag_models --force-reload

This will create a flexi_generated_model.py file in the same directory as your model, containing OrderTag and ProductTag models. The command will automatically:

  • Generate the tag models with JSONField for tags
  • Add import statements to your models.py file
  • Run makemigrations to create migration files

3. Apply Migrations

python manage.py migrate

Service-Only Usage 🚀

Instance Operations

from flexi_tag.utils.service import TaggableService

# Create a service instance for instance operations
service = TaggableService()

# Add tags to an instance
order = Order.objects.create(name="Order #123", status="active")
service.add_tag(order, "urgent")
service.add_tag(order, "priority")

# Bulk add tags
service.bulk_add_tags(order, ["important", "customer_vip"])

# Remove tags
service.remove_tag(order, "urgent")

# Get all tags for an instance
tags = service.get_tags(order)  # Returns: ["priority", "important", "customer_vip"]

QuerySet Filtering (The Power of Service-Only!)

# This is where the service-only approach shines!
# You can compose querysets with your existing filters + tag filters

# Create a service instance for filtering operations
service = TaggableService()

# Start with your existing queryset
orders = Order.objects.filter(status='active').filter(created_date__gte='2024-01-01')

# Add tag filtering - preserves all existing filters!
urgent_orders = service.filter_by_tag(orders, 'urgent')

# Chain multiple tag filters
priority_urgent = service.filter_by_tag(urgent_orders, 'priority')

# Or exclude by tag
non_archived = service.exclude_by_tag(orders, 'archived')

# Multiple tags (AND logic)
multi_tagged = service.filter_by_tags(orders, ['urgent', 'priority'])

# Any tag (OR logic)
any_tagged = service.filter_by_any_tag(orders, ['urgent', 'priority', 'vip'])

# Prefetch tag data for performance
orders_with_tags = service.with_tags(orders)

API Integration

from rest_framework.generics import ListAPIView
from flexi_tag.utils.service import TaggableService
from .models import Order
from .serializers import OrderSerializer

class OrderListAPIView(ListAPIView):
    serializer_class = OrderSerializer

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.taggable_service = TaggableService()

    def get_queryset(self):
        queryset = Order.objects.all()

        # Apply regular filters
        status = self.request.query_params.get('status')
        if status:
            queryset = queryset.filter(status=status)

        created_date = self.request.query_params.get('created_date')
        if created_date:
            queryset = queryset.filter(created_date=created_date)

        # Apply tag filter - preserves all previous filters!
        tag = self.request.query_params.get('tag')
        if tag:
            queryset = self.taggable_service.filter_by_tag(queryset, tag)

        return queryset

# Usage: /api/v1/orders/?status=active&created_date=2024-01-01&tag=urgent

ViewSet Integration

from rest_framework import viewsets
from flexi_tag.utils.views import TaggableViewSetMixin
from .models import Order

class OrderViewSet(TaggableViewSetMixin, viewsets.ModelViewSet):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer

# This gives you endpoints like:
# POST /api/orders/1/add_tag/ - {"key": "urgent"}
# POST /api/orders/1/bulk_add_tag/ - {"keys": ["urgent", "priority"]}
# POST /api/orders/1/remove_tag/ - {"key": "urgent"}
# GET  /api/orders/1/get_tags/
# GET  /api/orders/filter_by_tag/?key=urgent
# GET  /api/orders/exclude_by_tag/?key=archived

Why Service-Only Architecture? 🤔

Preserves Existing QuerySets

# Your complex querysets work perfectly
orders = Order.objects.select_related('customer').prefetch_related('items')
filtered = TaggableService.filter_by_tag(orders, 'urgent')  # All relations preserved!

Clean Integration

# Your custom QuerySets integrate seamlessly
products = Product.objects.active()  # Your custom method
tagged = TaggableService.filter_by_tag(products, 'featured')  # Service layer

Composable and Chainable

# Chain multiple operations seamlessly
result = (Order.objects
          .filter(status='active')
          .pipe(lambda qs: TaggableService.filter_by_tag(qs, 'urgent'))
          .filter(created_date__gte=today))

Explicit and Clear

# It's always clear what you're doing
TaggableService.filter_by_tag(queryset, 'tag')  # Obvious service call
# Clean, explicit, and maintainable

Testing

Run the tests with:

python runtests.py

Use the --verbose flag for more detailed output:

python runtests.py --verbose

Use the --interactive flag for interactive mode:

python runtests.py --interactive

Custom Exception Integration

Configure your own base exception class for seamless integration with your project:

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

# Now all Flexi Tag exceptions inherit from your base class
from flexi_tag.exceptions import TagValidationException

try:
    service.add_tag(instance, 'duplicate_tag')
except TagValidationException as e:
    # Has your custom base class attributes!
    print(e.status_code)  # From your BaseAPIException

For detailed examples and integration patterns, see CUSTOM_EXCEPTIONS.md.

Documentation

For comprehensive documentation including advanced usage, custom validation, and detailed API reference, please visit: https://dj-flexi-tag.readthedocs.io

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

  1. Clone the repository
  2. Create a virtual environment
  3. Install dependencies: pip install -e .[dev,test,docs]
  4. Run tests: python runtests.py

License

This project is licensed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dj_flexi_tag-2.1.2.tar.gz (45.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dj_flexi_tag-2.1.2-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

Details for the file dj_flexi_tag-2.1.2.tar.gz.

File metadata

  • Download URL: dj_flexi_tag-2.1.2.tar.gz
  • Upload date:
  • Size: 45.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.7.17

File hashes

Hashes for dj_flexi_tag-2.1.2.tar.gz
Algorithm Hash digest
SHA256 b5183e039c63719b0e0e349a3dcee5b5dc6029b2c8fe9d5bd939ce635d8b35fb
MD5 a3c4316ea91041cfa15d550f9ec18dcd
BLAKE2b-256 745d8d9147f4d3b3e6faa7929dc3e4983eda4144ec50fb07b5f48c8d9cbe0f8b

See more details on using hashes here.

File details

Details for the file dj_flexi_tag-2.1.2-py3-none-any.whl.

File metadata

  • Download URL: dj_flexi_tag-2.1.2-py3-none-any.whl
  • Upload date:
  • Size: 15.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.7.17

File hashes

Hashes for dj_flexi_tag-2.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cfe136659b4d7c549b6f56ca224d817649990aeceba0f525c92c58e561366014
MD5 50b8f8098c27831305e170ca62386f48
BLAKE2b-256 3fc061d428eba0808d45aebdb5bce1d3e97749212566e3f5a0d5e64e91bb9387

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page