Skip to content

Commit

Permalink
Merge pull request #169 from rootstrap/develop
Browse files Browse the repository at this point in the history
Develop to master. v.1.15.3
  • Loading branch information
jumcorredorro authored Aug 3, 2022
2 parents 9df28ed + b7275d7 commit d248be0
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 10 deletions.
Binary file modified docs/images/users_lookup_fields.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion docs/source/content/create.drip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ On the other hand, when you click in the ``FIELD NAME OF USER`` input, you will

Here you can relate the Drip to the corresponding ``Campaign``. Grouping several drips under a campaign.

Lookup fields
-------------
You can make queries using the User's model fields and the fields of the models related to the user.
For example, Groups model has 3 fields: `id`, `name`, and `id`. It will show you 4 items, these fields plus the groups relationship itself.
Another nice example is the SentDrip model, it has 8 fields but the drip field is also a relationship, so it will allow you to filter also over these fields in Drip model. This will allow you to filter Users that match a specific drip name sent to that user, using `sent_drips__drip__name` field.
This will extend to any fields you have in your project related to User model.

.. image:: ../../images/users_lookup_fields.png
:width: 600
:alt: User fields

In the previous image, for example, `last_login` is the field in the User model, and `groups__user__id` is the user id from the Groups model that is related to it. So you can enter the name of the field or select it from the list that you see when you click on the input.
In the previous image, for example, `last_login` is the field in the User model, and `sent_drips__subject` is the subject of the SentDrips model that is related to it.
So you can enter the name of the field or select it from the list that you see when you click on the input.

After the selection of the field name, you have to choose the type of lookup that you want to do over the field. The possibilities are `exactly`, `exactly (case insensitive)`, `contains`, `contains (case insensitive)`, `greater than`, `greater than or equal to`, `less than`, etc. This lookup type will be done over the user field and the `FIELD VALUE` that you enter.
The `FIELD VALUE` input can be a string, a number, or a regular expression. The correctness of the queryset rule will depend on the type of the user field, the lookup type, and the field value.

Expand Down
2 changes: 1 addition & 1 deletion drip/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.15.2"
__version__ = "1.15.3"
3 changes: 3 additions & 0 deletions drip/campaigns/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ def delete(self, using=None, keep_parents=False):
if self.delete_drips:
self.drip_set.all().delete()
super().delete(using, keep_parents)

def __str__(self) -> str:
return self.name
18 changes: 18 additions & 0 deletions drip/migrations/0005_alter_querysetrule_lookup_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-07-29 09:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('drip', '0004_auto_20220713_1317'),
]

operations = [
migrations.AlterField(
model_name='querysetrule',
name='lookup_type',
field=models.CharField(choices=[('exact', 'exactly'), ('iexact', 'exactly (case insensitive)'), ('contains', 'contains'), ('icontains', 'contains (case insensitive)'), ('regex', 'regex'), ('iregex', 'regex (case insensitive)'), ('gt', 'greater than'), ('gte', 'greater than or equal to'), ('lt', 'less than'), ('lte', 'less than or equal to'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('istartswith', 'starts with (case insensitive)'), ('iendswith', 'ends with (case insensitive)')], default='exact', max_length=12),
),
]
6 changes: 3 additions & 3 deletions drip/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ class SentDrip(AbstractSentDrip):
("contains", "contains"),
("icontains", "contains (case insensitive)"),
("regex", "regex"),
("iregex", "contains (case insensitive)"),
("iregex", "regex (case insensitive)"),
("gt", "greater than"),
("gte", "greater than or equal to"),
("lt", "less than"),
("lte", "less than or equal to"),
("startswith", "starts with"),
("endswith", "starts with"),
("istartswith", "ends with (case insensitive)"),
("endswith", "ends with"),
("istartswith", "starts with (case insensitive)"),
("iendswith", "ends with (case insensitive)"),
)

Expand Down
44 changes: 44 additions & 0 deletions drip/tests/test_rules.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
from typing import Optional

import pytest
from django.core.exceptions import ValidationError
from faker import Faker

from drip.models import Drip, QuerySetRule
from drip.utils import get_simple_fields, get_user_model

pytestmark = pytest.mark.django_db


class TestCaseRules:
faker: Faker

@classmethod
def setup_class(cls):
cls.faker = Faker()

def setup_method(self, test_method):
self.drip = Drip.objects.create(
name="A Drip just for Rules",
subject_template="Hello",
body_html_template="KETTEHS ROCK!",
)

def _get_field_value(self, field_type: str) -> Optional[str]:
field_types = {
"AutoField": self.faker.pyint(min_value=1),
"CharField": self.faker.word(),
"DateTimeField": f"now-{self.faker.pyint(min_value= 1, max_value=60)} days",
"BooleanField": self.faker.pybool(),
"EmailField": self.faker.email(),
"TextField": self.faker.word(),
"PositiveIntegerField": self.faker.pyint(min_value=1),
"ForeignKey": self.faker.pyint(min_value=1),
"OneToOneField": self.faker.pyint(min_value=1),
"RelatedObject": self.faker.pyint(min_value=1),
"ManyToManyField": self.faker.pyint(min_value=1),
}
field_value = field_types.get(field_type)
return str(field_value) if field_value else None

def test_valid_rule(self):
rule = QuerySetRule(
drip=self.drip,
Expand All @@ -39,3 +66,20 @@ def test_raise_errors(self, field_name: str, lookup_type: str, field_value: str)
)
with pytest.raises(ValidationError):
rule.clean()

def test_drip_fields_validation_success(self):
User = get_user_model()
users_fields = get_simple_fields(User)
# Using exact because it matches most of the field types
lookup_type = "exact"
for field in users_fields:
field_name, field_type = field
field_value = self._get_field_value(field_type)
if field_value:
rule = QuerySetRule(
drip=self.drip,
field_name=field_name,
lookup_type=lookup_type,
field_value=field_value,
)
rule.clean()
6 changes: 1 addition & 5 deletions drip/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def get_full_field(parent_field: str, field_name: str) -> str:


def get_rel_model(field: FieldType, RelatedObject: RelatedObject) -> Type[models.Model]:
if isinstance(field, RelatedObject): # type: ignore
if not is_valid_instance(field): # type: ignore
RelModel = field.model
# field_names.extend(get_fields(RelModel, full_field, True))
else:
Expand Down Expand Up @@ -178,10 +178,6 @@ def give_model_field(full_field: str, Model: Type[models.Model]) -> tuple:
def get_simple_fields(Model: Type[models.Model], **kwargs) -> List:
ret_list: List = []
for f in get_fields(Model, **kwargs):
if "__" in f[0]:
# Add __user__ to the fields in related models
parts = f[0].split("__")
f[0] = parts[0] + "__user__" + parts[1]
if f[0] not in [x[0] for x in ret_list]:
# Add field if not already in list
ret_list.append([f[0], f[3].__name__])
Expand Down

0 comments on commit d248be0

Please sign in to comment.