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: No manager conflicts, works with any existing QuerySet
  • 🔄 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 Manager Friendly: Preserves your existing custom managers
  • � 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 managers 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()  # This 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!

No Manager Conflicts

# Your custom managers are never touched
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
# vs old approach with mysterious managers

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.0.1a1.tar.gz (46.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.0.1a1-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

Details for the file dj-flexi-tag-2.0.1a1.tar.gz.

File metadata

  • Download URL: dj-flexi-tag-2.0.1a1.tar.gz
  • Upload date:
  • Size: 46.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.0.1a1.tar.gz
Algorithm Hash digest
SHA256 2bd4d7b3381bb6ac6f1eb6b849529c6d7a72915070960c957e7a6a4f254a0f7e
MD5 c09899daf5aac8b5911a229de45e8491
BLAKE2b-256 994d8383ed2859b7036b6ead36e05b0dd0256b0313c073ad0b3c487d0764f570

See more details on using hashes here.

File details

Details for the file dj_flexi_tag-2.0.1a1-py3-none-any.whl.

File metadata

  • Download URL: dj_flexi_tag-2.0.1a1-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.0.1a1-py3-none-any.whl
Algorithm Hash digest
SHA256 8f6010ee64d9ba883117ece433e99b557d9ce23d4fe4aea3c3b159b19ff08317
MD5 c52a49d3ec8ff28292c7e23ee9163924
BLAKE2b-256 5f2699a299af3b8ddba8c3427a48753c0a3acd8e7ae53b82f13a538692f02680

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