88from django .contrib .auth .tokens import PasswordResetTokenGenerator
99from django .core .mail import send_mail
1010from django .db import connection , models
11- from django .db .models import Exists , OuterRef , Q , UniqueConstraint
11+ from django .db .models import (
12+ Count ,
13+ Exists ,
14+ ExpressionWrapper ,
15+ F ,
16+ IntegerField ,
17+ Max ,
18+ OuterRef ,
19+ Q ,
20+ UniqueConstraint ,
21+ Value ,
22+ )
23+ from django .db .models .functions import ExtractDay , Now
1224from django .http import HttpResponse , HttpResponseNotFound
1325from django .template .loader import render_to_string
1426from django .urls import reverse
1830from djangoql .exceptions import DjangoQLParserError
1931from djangoql .parser import DjangoQLParser
2032from djangoql .queryset import DjangoQLQuerySet
21- from djangoql .schema import BoolField , DjangoQLSchema
33+ from djangoql .schema import BoolField , DjangoQLSchema , IntField
2234from modelcluster .models import ClusterableModel
2335from wagtail .admin .forms import WagtailAdminPageForm
2436from wagtail .admin .panels import FieldPanel , ObjectList , TabbedInterface
3143from bakeup .newsletter .panels import NewsletterPanel
3244from bakeup .pages .blocks import AllBlocks
3345from bakeup .pages .models import BrandSettings , EmailSettings
34- from bakeup .shop .models import Customer , PointOfSale
46+ from bakeup .shop .models import Customer , CustomerOrder , PointOfSale
3547from bakeup .users .models import User
3648
3749from .blocks import StoryBlock
@@ -372,7 +384,19 @@ def members(self):
372384 members = self .audience .contacts .all ()
373385 if self .filter_query :
374386 members = members .annotate (
375- is_customer = Exists (Customer .objects .filter (user = OuterRef ("user" )))
387+ is_customer = Exists (Customer .objects .filter (user = OuterRef ("user" ))),
388+ has_ordered = Exists (
389+ CustomerOrder .objects .filter (customer__user = OuterRef ("user" ))
390+ ),
391+ order_count = Count ("user__customer__orders" ),
392+ )
393+ members = members .annotate (
394+ last_order_date = Max ("user__customer__orders__created" ),
395+ ).annotate (
396+ months_since_last_order = ExpressionWrapper (
397+ ExtractDay (Now () - F ("last_order_date" )) / Value (30 ),
398+ output_field = IntegerField (),
399+ ),
376400 )
377401 members = members .djangoql (self .filter_query , ContactSchema )
378402 return members
@@ -548,6 +572,46 @@ def _make_hash_value(self, contact, timestamp):
548572 return str (bool (contact .is_active )) + str (contact .pk ) + str (timestamp )
549573
550574
575+ class MonthsSinceLastOrderField (IntField ):
576+ """
577+ Allows filtering like:
578+ months_since_last_order > 12
579+ months_since_last_order = 0 (ordered this month)
580+ """
581+
582+ name = "months_since_last_order"
583+
584+ def get_lookup_name (self ):
585+ return "months_since_last_order"
586+
587+
588+ class OrderCountField (IntField ):
589+ """
590+ Allows filtering like:
591+ order_count = 0 (never ordered)
592+ order_count > 5 (more than 5 orders)
593+ order_count >= 1 (has ordered at least once)
594+ """
595+
596+ name = "order_count"
597+
598+ def get_lookup_name (self ):
599+ return "order_count"
600+
601+
602+ class HasOrderedField (BoolField ):
603+ """
604+ Allows filtering like:
605+ has_ordered = True
606+ has_ordered = False
607+ """
608+
609+ name = "has_ordered"
610+
611+ def get_lookup_name (self ):
612+ return "has_ordered"
613+
614+
551615class ContactSchema (DjangoQLSchema ):
552616 exclude = (NewsletterPage , Receipt )
553617
@@ -560,6 +624,9 @@ def get_fields(self, model):
560624 "email" ,
561625 "user" ,
562626 "is_active" ,
627+ HasOrderedField (),
628+ OrderCountField (),
629+ MonthsSinceLastOrderField (),
563630 ]
564631 if model == Group :
565632 return ["name" ]
0 commit comments