Django Easy Scoping

Django Easy Scoping

  • Installation
  • Usage
  • API

Usage

Example Django Model located at bottom of page.

Basic Usage

Scoping

Here are some simple examples just to see the syntax.

Register the scope on models.py:

Widget.register_scope('blue', lambda qs: qs.filter(color='blue'))

Without easy scoping:

Widget.objects.filter(color='blue')

With easy scoping:

Widget.a().blue()

Chaining Scopes

Let's look at that same query where we chain these calls instead.

Register the scope on models.py:

Widget.register_scope('blue', lambda qs: qs.filter(color='blue'))
Widget.register_scope('small', lambda qs: qs.filter(size='small'))
Widget.register_scope('circle', lambda qs: qs.filter(shape='circle'))

Without easy scoping:

Widget.objects.filter(color='blue').filter(size='small').filter(shape='circle')

With easy scoping:

Widget.a().blue().small().circle()

Using Other Queryset Methods

As the return value is a queryset we can perform other django operations on these querysets. The full list of operations can be found here.

Let's consider ordering these by their color in ascending alphabetical order.

Widget.a().blue().small().circle().order_by('color')

Real-World Usage

Consider the example models located at the bottom of the page. We have Customers who make many purchases and purchases who have many widgets.

Scoping Example

Register the scopes on purchases/models.py:

MIDWEST = {
    'customer__state__in': ('Indiana', 'Illinois', 'Michigan', 'Ohio',
                            'Wisconsin', 'Iowa', 'Nebraska', 'Kansas',
                            'North Dakota', 'Minnesota', 'South Dakota', 'Missouri',)
}

Purchase.register_scope('male_seniors_midwest', 
                        lambda qs: qs.filter(customer__age__gte=65)
                                     .filter(customer__gender__in='M')
                                     .filter(**MIDWEST))

Purchase.register_scope('female_seniors_midwest', 
                        lambda qs: qs.filter(customer__age__gte=65)
                                     .filter(customer__gender__in='F')
                                     .filter(**MIDWEST))

# We can also just make one scope for this age/region combo which takes a gender
Purchase.register_scope('gender_seniors_midwest', 
                        lambda qs, g: qs.filter(customer__age__gte=65)
                                        .filter(customer__gender__in=g)
                                        .filter(**MIDWEST))

# Let's also make one for millenials
Purchase.register_scope('gender_millenials_midwest', 
                        lambda qs, g: qs.filter(customer__age__gte=22)
                                        .filter(customer__age_lte=37)
                                        .filter(customer__gender__in=g)
                                        .filter(**MIDWEST))

So now we have a scope for all customers of a particular gender, age, and geographical region.

>>> Purchase.objects.all().male_seniors_midwest()
<ScopingQuerySet[<Purchase: PurchaseObjects(1)>, ...]

>>> Purchase.objects.all().female_seniors_midwest()
<ScopingQuerySet[<Purchase: PurchaseObjects(2)>, ...]

# Or using our gender taking scope

>>> Purchase.objects.all().gender_seniors_midwest('M')
<ScopingQuerySet[<Purchase: PurchaseObjects(1)>, ...]
>>> Purchase.objects.all().gender_seniors_midwest('F')
<ScopingQuerySet[<Purchase: PurchaseObjects(2)>, ...]

Aggregate Example

So now you've created some scopes and want a way to compare them! Well, let's register some aggregates!

Register the aggregates on purchases/models.py:

import pytz
from datetime import datetime as dt, timedelta as tdse.register_aggregate('data_last_days',
Purchase.register_aggregate('data_last_days',
                            lambda qs, days: qs.filter(sale_date__gte=dt.utcnow().replace(tzinfo=pytz.utc) - td(days=days))
                                               .annotate(item_count=Count('items')) 
                                               .aggregate(total_sales=Count('customer'),
                                                          average_items_per_sale=Avg('item_count'),
                                                          total_profit=Sum('profit'),
                                                          average_profit=Avg('profit'))
                            )

So here our aggregate is called on a queryset and takes as an argument a number of days. It then returns a queryset of all purchases from today back that many days. We then annotate each purchase with the item count for it (my example implementation randomly chooses between 1 and 9 items). Finally, we aggregate the total amount of sales over that date range, the average amount of items per sale, our total profit, and our average profit.

>>> Purchase.objects.all().data_last_days(100)
{'total_sales': 155, 'total_profit': 174403.49, 'average_profit': 1125.18, 'average_items_per_sale': 4.9}

Putting it Together

Ok, so we've got some scopes and an aggregate function. Let's use them to compare!

>>> Purchase.objects.all().male_seniors_midwest().data_last_days(100)
{'total_sales': 6, 'total_profit': 5313.0, 'average_profit': 885.5, 'average_items_per_sale': 3.83}

>>> Purchase.objects.all().female_seniors_midwest().data_last_days(100)
{'total_sales': 5, 'total_profit': 6380.5, 'average_profit': 1276.1, 'average_items_per_sale': 5.6}

So, from our data it seems like we aren't selling many widgets to seniors in the Northwest but when we do females are buying more items and returning much more profit!

Let's check out millenials.

>>> Purchase.objects.all().gender_millenials_midwest('M').data_last_days(100)
{'total_sales': 18, 'total_profit': 24107.1, 'average_profit': 4017.84, 'average_items_per_sale': 7.98}

So male millenials in the midwest are buying a lot of our products!

Example Django Models

These are the django models used in the examples above for your reference. You can find these on our github aswell.

widgets/models.py


from django.db import models
from .options import COLORS, SIZES, SHAPES


class Widget(models.Model):
    name = models.CharField(max_length=30)
    color = models.CharField(choices=COLORS, max_length=30)
    size = models.CharField(choices=SIZES, max_length=30)
    shape = models.CharField(choices=SHAPES, max_length=30)

customers/models.py

from django.db import models


class Customer(ScopingMixin, models.Model):
    name = models.CharField(max_length=30, blank=True)
    state = models.CharField(max_length=30, blank=True)
    gender = models.CharField(max_length=1, blank=True)
    age = models.IntegerField(blank=True)


    def get_purchases(self):
        from purchases.models import Purchase
        return Purchase.objects.all().filter(customer=self)

purchases/models.py

import pytz
import django
from django.db import models
from .state_regions import NORTHEAST, MIDWEST, SOUTH, WEST
from django.db.models import Sum, Count, Avg
from widgets.models import Widget
from customers.models import Customer
from datetime import datetime as dt, timedelta as td
from DjangoEasyScoping.ScopingMixin import ScopingMixin, ScopingQuerySet


class Purchase(ScopingMixin, models.Model):
    items = models.ManyToManyField(Widget, blank=True)
    sale_date = models.DateTimeField(default=django.utils.timezone.now)
    sale_price = models.FloatField(default=0, blank=True)
    profit = models.FloatField(default=0, blank=True)
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)

    objects = ScopingQuerySet.as_manager()

    def get_items(self):
        return self.items.all()

    def get_item_count(self):
        return self.items.all().count()

    def get_sale_date(self):
        return self.sale_date

    def get_sale_price(self):
        return self.sale_price

    def get_cost(self):
        return round(self.items.aggregate(cost=Sum('cost'))['cost'], 2)

    def get_profit(self):
        return self.profit

    def set_sale_price(self):
        cost_plus_profit = 1.1
        cost = self.get_cost()
        self.sale_price = round(cost*cost_plus_profit, 2)

    def set_profit(self):
        profit_margin = .1
        cost = self.get_cost()
        self.profit = round(cost*profit_margin, 2)

    def save(self, *args, **kwargs):
        if self.id:
            for item in Widget.objects.filter(purchase=self):
                self.items.add(item)
                self.set_sale_price()
                self.set_profit()
                super(Purchase, self).save(*args, **kwargs)

        else:
            super(Purchase, self).save(*args, **kwargs)


Purchase.register_scope('male_seniors_midwest',
                        lambda qs: qs.filter(customer__age__gte=65)
                                     .filter(customer__gender__in='M')
                                     .filter(**MIDWEST)
                        )

Purchase.register_scope('female_seniors_midwest',
                        lambda qs: qs.filter(customer__age__gte=65)
                                     .filter(customer__gender__in='F')
                                     .filter(**MIDWEST)
                        )

Purchase.register_scope('gender_seniors_midwest',
                        lambda qs, g: qs.filter(customer__age__gte=65)
                                        .filter(customer__gender__in=g)
                                        .filter(**MIDWEST)
                        )

Purchase.register_aggregate('data_last_days',
                            lambda qs, days:
                            qs.filter(sale_date__gte=dt.utcnow().replace(tzinfo=pytz.utc) - td(days=days))
                            .annotate(item_count=Count('items'))
                            .aggregate(total_sales=Count('customer'),
                                       average_items_per_sale=Avg('item_count'),
                                       total_profit=Sum('profit'),
                                       average_profit=Avg('profit'))
                            )

Purchase.register_scope('senior',
                        lambda qs: qs.filter(customer__age__gte=65))
Purchase.register_scope('millenial',
                        lambda qs: qs.filter(customer__age__gte=22)
                                     .filter(customer__age__lte=37))

Purchase.register_scope('male',
                        lambda qs: qs.filter(customer__gender__in='M'))
Purchase.register_scope('female',
                        lambda qs: qs.filter(customer__gender__in='F'))

Purchase.register_scope('northeast',
                        lambda qs: qs.filter(**NORTHEAST))

Purchase.register_scope('midwest',
                        lambda qs: qs.filter(**MIDWEST))

Purchase.register_scope('southern',
                        lambda qs: qs.filter(**SOUTH))

Purchase.register_scope('western',
                        lambda qs: qs.filter(**WEST))
  • Basic Usage
    • Scoping
    • Chaining Scopes
    • Using Other Queryset Methods
  • Real-World Usage
    • Scoping Example
    • Aggregate Example
    • Putting it Together
  • Example Django Models
    • widgets/models.py
    • customers/models.py
    • purchases/models.py
Copyright © 2018 Net Prophet Technologies