如果是我,我会把规则存储在数据库中,然后使用Celery定期处理它们。
对于模型部分,我认为多表继承是正确的选择,因为不同的规则需要存储不同的数据。在我看来,django-polymorphic 在这里是你的好帮手:
我建议使用以下内容:
from django.db import models
from polymorphic import PolymorphicModel
class AbtractRuleObject(models.Model):
class Meta:
abstract = True
def filter_queryset(self, queryset):
"""Will handle actual filtering of the event queryset"""
raise NotImplementedError
def match_instance(self, instance):
raise NotImplementedError
class RuleSet(AbtractRuleObject):
"""Will manage the painful part o handling the OR / AND logic inside the database"""
NATURE_CHOICES = (
('or', 'OR'),
('and', 'AND'),
)
nature = models.CharField(max_length=5, choices=NATURE_CHOICES, default='and')
parent_set = models.ForeignKey('self', null=True, blank=True, related_name='children')
def filter_queryset(self, queryset):
"""This is rather naive and could be optimized"""
if not self.parent_set:
for rule in self.rules:
if self.nature == 'and':
queryset = rule.filter_queryset(queryset)
elif self.nature == 'or':
queryset = queryset | rule.filter_queryset(queryset)
else:
for rule_set in self.children:
if self.nature == 'and':
queryset = rule_set.filter_queryset(queryset)
elif self.nature == 'or':
queryset = queryset | rule_set.filter_queryset(queryset)
return queryset
def match_instance(self, instance):
if not self.parent_set:
if self.nature == 'and':
return all([rule_set.match_instance(instance) for rule_set in self.children])
if self.nature == 'any':
return any([rule_set.match_instance(instance) for rule_set in self.children])
else:
if self.nature == 'and':
return all([rule_set.match_instance(instance) for rule_set in self.children])
if self.nature == 'any':
return any([rule_set.match_instance(instance) for rule_set in self.children])
class Rule(AbtractRuleObject, PolymorphicModel):
"""Base class for all rules"""
attribute = models.CharField(help_text="Attribute of the model on which the rule will apply")
rule_set = models.ForeignKey(RuleSet, related_name='rules')
class DateRangeRule(Rule):
start = models.DateField(null=True, blank=True)
end = models.DateField(null=True, blank=True)
def filter_queryset(self, queryset):
filters = {}
if self.start:
filters['{0}__gte'.format(self.attribute)] = self.start
if self.end:
filters['{0}__lte'.format(self.attribute)] = self.end
return queryset.filter(**filters)
def match_instance(self, instance):
start_ok = True
end_ok = True
if self.start:
start_ok = getattr(instance, self.attribute) >= self.start
if self.end:
end_ok = getattr(instance, self.attribute) <= self.end
return start_ok and end_ok
class MatchStringRule(Rule):
match = models.CharField()
def filter_queryset(self, queryset):
filters = {'{0}'.format(self.attribute): self.match}
return queryset.filter(**filters)
def match_instance(self, instance):
return getattr(instance, self.attribute) == self.match
class StartsWithRule(Rule):
start = models.CharField()
def filter_queryset(self, queryset):
filters = {'{0}__startswith'.format(self.attribute): self.start}
return queryset.filter(**filters)
def match_instance(self, instance):
return getattr(instance, self.attribute).startswith(self.start)
现在假设您的
Event
和
City
模型如下:
class Country(models.Model):
continent = models.CharField()
name = models.CharField(unique=True)
class City(models.Model):
name = models.CharField(unique=True)
country = models.ForeignKey(Country)
founded_date = models.DateField()
class Event(models.Model):
name = models.CharField(unique=True)
city = models.ForeignKey(City)
start = models.DateField()
end = models.DateField()
然后,您可以按照我的示例使用:
global_set = RuleSet(nature='and')
global_set.save()
set1 = RuleSet(nature='and', parent_set=global_set)
set1.save()
year_range = DateRangeRule(start=datetime.date(1200, 1, 1),
end=datetime.date(1400, 1, 1),
attribute='city__founded_date',
rule_set=set1)
year_range.save()
set2 = RuleSet(nature='or', parent_set=global_set)
set2.save()
startswith_f = StartsWithRule(start='F',
attribute='city__country__name')
rule_set=set2)
startswith_f.save()
exact_match = MatchStringRule(match='South Africa',
attribute='city__country__continent')
rule_set=set2)
exact_match.save()
queryset = Event.objects.all()
filtered_queryset = global_set.filter_queryset(queryset)
assert global_set.match_instance(filtered_queryset[0]) == True
代码完全未经过测试,但我认为它最终可能会起作用,或者至少能够给您提供实现思路。希望这有所帮助!