DjangoQL

repository·master·Indexed 22 days ago

https://github.com/ivelum/djangoql

An advanced search language for Django that provides auto-completion, logical operators, and table joins. It can be integrated into the Django admin via DjangoQLSearchMixin or used independently with DjangoQLQuerySet and the apply_search utility. The library includes a completion widget for frontends and allows for custom search logic through DjangoQLSchema and custom field classes.

Tokens
5.8K
Snippets
14
Records
16
Agent score
28%

What's inside DjangoQL

  1. Create custom search fields in DjangoQL

    master

    You can extend DjangoQL's search capabilities by subclassing base field classes. This allows you to search by annotations, customize suggestion logic, or implement complex lookup logic.

    Available base field classes in djangoql.schema:

    • IntField
    • FloatField
    • StrField
    • BoolField
    • DateField
    • DateTimeField
    • RelationField
  2. Configure DjangoQL Schema to limit search scope

    master

    A DjangoQLSchema defines what users are allowed to search. If no schema is provided, DjangoQL defaults to a recursive schema that includes all fields and relations. Use a schema to improve performance or security by limiting models and fields.

    Schema Options

    • exclude: A tuple of models to exclude from search.
    • include: A tuple of models to include (limits search to these models only).
    • suggest_options: A dictionary mapping models to specific fields for auto-completion suggestions. Note: If not using a choices field, this performs a synchronous query; avoid using this on large querysets.
    • get_fields(self, model): A method to override which fields are available for a specific model.
    class UserQLSchema(DjangoQLSchema):
        exclude = (Book,)
        suggest_options = {
            Group: ['name'],
        }
    
        def get_fields(self, model):
            if model == Group:
                return ['name']
            return super(UserQLSchema, self).get_fields(model)
    
    
    @admin.register(User)
    class CustomUserAdmin(DjangoQLSearchMixin, UserAdmin):
        djangoql_schema = UserQLSchema
  3. Use the DjangoQL completion widget outside of Django admin

    master

    The DjangoQL completion widget is not tied to the Django admin and can be used in any part of your application. You can use it in two ways:

    1. As a standalone npm package: Install @djangoql/completion (or the relevant package name) via npm.
    2. Via the Python package: If you are not using a JavaScript bundler, you can use the pre-built assets included in the djangoql PyPI package.

    To use the pre-built assets, include the CSS and JS files from the djangoql/ static directory in your HTML template and initialize the DjangoQL object in a script block.

    {% load static %}
    <link rel="stylesheet" type="text/css" href="{% static 'djangoql/css/completion.css' %}" />
    <script src="{% static 'djangoql/js/completion.js%}"></script>
    
    <script>
      DjangoQL.DOMReady(function () {
        new DjangoQL({
          // JS object from DjangoQLSchema(MyModel).as_dict(), or a URL for async loading
          introspections: {{ introspections|safe }},
          // CSS selector for the textarea input
          selector: 'textarea[name=q]',
          // Optional: URL for Syntax Help link
          syntaxHelp: null,
          // Optional: enable automatic height adjustment for the textarea
          autoResize: true
        });
      });
    </script>
  4. Use DjangoQL outside of Django admin

    master

    You can use DjangoQL on any model by using DjangoQLQuerySet.as_manager() or by using the apply_search utility function.

    Option 1: Using DjangoQLQuerySet.as_manager()

    Attach the manager to your model to enable the .djangoql() method directly on the model's manager.

    Option 2: Using apply_search()

    Use apply_search(queryset, query_string, schema=...) to apply DjangoQL search to any existing Django queryset.

    # Option 1: Manager approach
    from django.db import models
    from djangoql.queryset import DjangoQLQuerySet
    
    class Book(models.Model):
        name = models.CharField(max_length=255)
        author = models.ForeignKey('auth.User')
        objects = DjangoQLQuerySet.as_manager()
    
    # Usage:
    qs = Book.objects.djangoql('name ~ "war" and author.last_name = "Tolstoy"')
    
    
    # Option 2: apply_search approach
    from django.contrib.auth.models import User
    from djangoql.queryset import apply_search
    
    qs = User.objects.all()
    qs = apply_search(qs, 'groups = None')
  5. Implement a DjangoQL completion backend

    master

    To power the completion widget, you need a view that handles two tasks:

    1. Schema Introspection: Serialize a DjangoQLSchema into JSON to provide the widget with field and model information.
    2. Query Execution: Use apply_search to transform a raw query string into a filtered Django QuerySet.

    Key Components:

    • DjangoQLSchema: Define which models and fields are available for search. Use suggest_options to provide specific field suggestions for related models.
    • DjangoQLSchemaSerializer().serialize(): Converts a schema instance into a dictionary suitable for JSON serialization.
    • apply_search(queryset, query_string, schema=...): Applies the DjangoQL query string to a queryset using the provided schema.
    import json
    from djangoql.exceptions import DjangoQLError
    from djangoql.queryset import apply_search
    from djangoql.schema import DjangoQLSchema
    from djangoql.serializers import DjangoQLSchemaSerializer
    
    # 1. Define the schema
    class UserQLSchema(DjangoQLSchema):
        include = (User, Group)
        suggest_options = {
            Group: ['name'],
        }
    
    # 2. In your view...
    @require_GET
    def completion_demo(request):
        q = request.GET.get('q', '')
        query = User.objects.all()
        
        if q:
            try:
                # Apply the DjangoQL query string
                query = apply_search(query, q, schema=UserQLSchema)
            except DjangoQLError as e:
                query = query.none()
                error = str(e)
    
        # 3. Serialize the schema for the frontend
        introspections = DjangoQLSchemaSerializer().serialize(
          UserQLSchema(query.model),
        )
        
        return render_to_response('template.html', {
            'q': q,
            'search_results': query,
            'introspections': json.dumps(introspections),
        })
  6. Add DjangoQL to the Django admin

    master

    To replace the standard Django search functionality with DjangoQL in the admin interface, add DjangoQLSearchMixin to your ModelAdmin class.

    from django.contrib import admin
    from djangoql.admin import DjangoQLSearchMixin
    from .models import Book
    
    @admin.register(Book)
    class BookAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
        pass
  7. Use DjangoQL alongside standard Django admin search

    master

    If you define search_fields in your ModelAdmin, DjangoQL will provide a checkbox near the search input to toggle between advanced DjangoQL search and standard Django search.

    By default, the DjangoQL checkbox is enabled (djangoql_completion_enabled_by_default = True). You can change this behavior in your ModelAdmin.

    @admin.register(Book)
    class BookAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
        search_fields = ('title', 'author__name')
        djangoql_completion_enabled_by_default = False
  8. Import and initialize the DjangoQL completion widget

    master

    To use the DjangoQL completion widget in your frontend, import the DjangoQL module and its associated CSS. The module is typically exposed via the djangoql-completion package. For convenience in some environments, the module can be attached to the window object to make it globally accessible.

    import DjangoQL from 'djangoql-completion';
    import 'djangoql-completion/dist/completion.css';
    
    // Optionally expose to window for global access
    window.DjangoQL = DjangoQL;
  9. Override search lookup name and value

    master

    For simple transformations, override these two methods in your custom field:

    • .get_lookup_name(): Returns the Django lookup string (e.g., date_joined__year) to use instead of the field name.
    • .get_lookup_value(value): Modifies the search value before it is applied to the filter.
    class UserDateJoinedYear(IntField):
        name = 'date_joined_year'
    
        def get_lookup_name(self):
            return 'date_joined__year'
    
    
    class UserQLSchema(DjangoQLSchema):
        def get_fields(self, model):
            fields = super(UserQLSchema, self).get_fields(model)
            if model == User:
                fields += [UserDateJoinedYear()]
            return fields
    
    
    @admin.register(User)
    class CustomUserAdmin(DjangoQLSearchMixin, UserAdmin):
        djangoql_schema = UserQLSchema
  10. Search by queryset annotations

    master

    To search using fields that are not part of the model but are added via .annotate() in the queryset, include them in your DjangoQLSchema by overriding get_fields(). The field name in the schema must match the annotation name.

    from djangoql.schema import DjangoQLSchema, IntField
    
    
    class UserQLSchema(DjangoQLSchema):
        def get_fields(self, model):
            fields = super(UserQLSchema, self).get_fields(model)
            if model == User:
                fields += [IntField(name='groups_count')]
            return fields
    
    
    @admin.register(User)
    class CustomUserAdmin(DjangoQLSearchMixin, UserAdmin):
        djangoql_schema = UserQLSchema
    
        def get_queryset(self, request):
            qs = super(CustomUserAdmin, self).get_queryset(request)
            return qs.annotate(groups_count=Count('groups'))
  11. Implement fully custom search logic with .get_lookup()

    master

    When .get_lookup_name() and .get_lookup_value() are insufficient, override the .get_lookup(self, path, operator, value) method. This allows you to return a custom Q object or complex queryset filter logic based on the operator and value provided.

    class UserAgeField(IntField):
        """
        Search by given number of full years
        """
        model = User
        name = 'age'
    
        def get_lookup_name(self):
            """
            We'll be doing comparisons vs. this model field
            """
            return 'date_joined'
    
        def get_lookup(self, path, operator, value):
            """
            The lookup should support with all operators compatible with IntField
            """
            if operator == 'in':
                result = None
                for year in value:
                    condition = self.get_lookup(path, '=', year)
                    result = condition if result is None else result | condition
                return result
            elif operator == 'not in':
                result = None
                for year in value:
                    condition = self.get_lookup(path, '!=', year)
                    result = condition if result is None else result & condition
                return result
    
            value = self.get_lookup_value(value)
            search_field = '__'.join(path + [self.get_lookup_name()])
            year_start = self.years_ago(value + 1)
            year_end = self.years_ago(value)
            if operator == '=':
                return (
                    Q(**{'%s__gt' % search_field: year_start}) &
                    Q(**{'%s__lte' % search_field: year_end})
                )
            elif operator == '!=':
                return (
                    Q(**{'%s__lte' % search_field: year_start}) |
                    Q(**{'%s__gt' % search_field: year_end})
                )
            elif operator == '>':
                return Q(**{'%s__lt' % search_field: year_start})
            elif operator == '>=':
                return Q(**{'%s__lte' % search_field: year_end})
            elif operator == '<':
                return Q(**{'%s__gt' % search_field: year_end})
            elif operator == '<=':
                return Q(**{'%s__gte' % search_field: year_start})
    
        def years_ago(self, n):
            timestamp = now()
            try:
                return timestamp.replace(year=timestamp.year - n)
            except ValueError:
                # February 29
                return timestamp.replace(month=2, day=28, year=timestamp.year - n)
    
    
    class UserQLSchema(DjangoQLSchema):
        def get_fields(self, model):
            fields = super(UserQLSchema, self).get_fields(model)
            if model == User:
                fields += [UserAgeField()]
            return fields
    
    
    @admin.register(User)
    class CustomUserAdmin(DjangoQLSearchMixin, UserAdmin):
        djangoql_schema = UserQLSchema