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
  • 🌐 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

# 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 = TaggableService.filter_by_tag(orders, 'urgent')

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

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

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

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

# Prefetch tag data for performance
orders_with_tags = TaggableService.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 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 = TaggableService.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

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-1.1.3a5.tar.gz (41.1 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-1.1.3a5-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

File details

Details for the file dj-flexi-tag-1.1.3a5.tar.gz.

File metadata

  • Download URL: dj-flexi-tag-1.1.3a5.tar.gz
  • Upload date:
  • Size: 41.1 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-1.1.3a5.tar.gz
Algorithm Hash digest
SHA256 3ee091c2734429f69feb5e371f9d58d25225704a8554ffd519dc76fa65211748
MD5 3607efa1bb11b57e18941bbeb9523795
BLAKE2b-256 f053fefcfb5de84a5215788f6039684b99c5a4a09ab1bdd3552dbfface115608

See more details on using hashes here.

File details

Details for the file dj_flexi_tag-1.1.3a5-py3-none-any.whl.

File metadata

  • Download URL: dj_flexi_tag-1.1.3a5-py3-none-any.whl
  • Upload date:
  • Size: 14.5 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-1.1.3a5-py3-none-any.whl
Algorithm Hash digest
SHA256 0f5060130c0fce882e4a31cbb7d4185fa82e203750c594745b9aeeb9030baf91
MD5 8d152287eb08dc70078f24c834aaf3d6
BLAKE2b-256 cb2e6f86a3becd15d5da52295e2b80fccdb12a04b5c7cd26923922abdadfff3b

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