Skip to content

Commit b0277e9

Browse files
committed
Add a test model with renamed PK
1 parent f8ac846 commit b0277e9

File tree

5 files changed

+116
-28
lines changed

5 files changed

+116
-28
lines changed

tests/testproject/testapp/admin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
DALFRelatedOnlyField,
99
)
1010

11-
from .models import Category, Post, Tag
11+
from .models import Category, CategoryRenamed, Post, Tag
1212

1313

1414
@admin.register(Post)
@@ -18,6 +18,7 @@ class PostAdmin(DALFModelAdmin):
1818
('author', DALFRelatedField),
1919
('audience', DALFChoicesField),
2020
('category', DALFRelatedFieldAjax),
21+
('category_renamed', DALFRelatedFieldAjax),
2122
('tags', DALFRelatedOnlyField),
2223
)
2324

@@ -28,6 +29,12 @@ class CategoryAdmin(admin.ModelAdmin):
2829
ordering = ('name',)
2930

3031

32+
@admin.register(CategoryRenamed)
33+
class CategoryRenamedAdmin(admin.ModelAdmin):
34+
search_fields = ('name',)
35+
ordering = ('name',)
36+
37+
3138
@admin.register(Tag)
3239
class TagAdmin(admin.ModelAdmin):
3340
search_fields = ('name',)

tests/testproject/testapp/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@
1010
@pytest.fixture
1111
def posts():
1212
return PostFactory.create_batch(10)
13+
14+
@pytest.fixture
15+
def unused_tag():
16+
return TagFactory(name='Unused')

tests/testproject/testapp/factories.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.contrib.auth import get_user_model
33
from factory import fuzzy
44

5-
from .models import AudienceChoices, Category, Post, Tag
5+
from .models import AudienceChoices, Category, CategoryRenamed, Post, Tag
66

77
FAKE_USERNAMES = [
88
'vigo',
@@ -47,6 +47,14 @@ class Meta:
4747
name = factory.Iterator(FAKE_CATEGORIES)
4848

4949

50+
class CategoryRenamedFactory(factory.django.DjangoModelFactory):
51+
class Meta:
52+
model = CategoryRenamed
53+
django_get_or_create = ('name',)
54+
55+
name = factory.Iterator(FAKE_CATEGORIES)
56+
57+
5058
class TagFactory(factory.django.DjangoModelFactory):
5159
class Meta:
5260
model = Tag
@@ -62,6 +70,7 @@ class Meta:
6270

6371
author = factory.SubFactory(UserFactory)
6472
category = factory.SubFactory(CategoryFactory)
73+
category_renamed = factory.SubFactory(CategoryRenamedFactory)
6574
title = factory.Sequence(lambda n: f'Book about {FAKE_CATEGORIES[n % len(FAKE_CATEGORIES)]} - {n}')
6675
audience = fuzzy.FuzzyChoice(AudienceChoices.choices, getter=lambda c: c[0])
6776

tests/testproject/testapp/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
from django.conf import settings
24
from django.db import models
35

@@ -9,6 +11,14 @@ def __str__(self):
911
return self.name
1012

1113

14+
class CategoryRenamed(models.Model):
15+
renamed_id = models.UUIDField(default=uuid.uuid4, primary_key=True)
16+
name = models.CharField(max_length=255)
17+
18+
def __str__(self):
19+
return self.name
20+
21+
1222
class Tag(models.Model):
1323
name = models.CharField(max_length=255)
1424

@@ -33,6 +43,11 @@ class Post(models.Model):
3343
on_delete=models.CASCADE,
3444
related_name='posts',
3545
)
46+
category_renamed = models.ForeignKey(
47+
to='CategoryRenamed',
48+
on_delete=models.CASCADE,
49+
related_name='posts',
50+
)
3651
tags = models.ManyToManyField(to='Tag', blank=True)
3752
audience = models.CharField(
3853
max_length=100,

tests/testproject/testapp/tests.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from dalf.admin import DALFChoicesField, DALFRelatedField, DALFRelatedFieldAjax, DALFRelatedOnlyField
1010

11-
from .models import Post
11+
from .models import Post, Tag
1212

1313
csrf_token_pattern = re.compile(r'name="csrfmiddlewaretoken" value="([^"]+)"')
1414

@@ -19,12 +19,14 @@ class MatchingTagValidator(HTMLParser):
1919
Instances of this class are not reusable. Create a new one for every ``check`` call.
2020
"""
2121

22-
def __init__(self, expected_attrs, matcher_attrs, tag):
22+
def __init__(self, tag, matcher_attrs, expected_attrs, expected_content=None):
2323
super().__init__()
24-
self.expected_attrs = expected_attrs
2524
self.matcher_attrs = matcher_attrs
2625
self.target_tag = tag
26+
self.expected_attrs = expected_attrs
27+
self.expected_content = expected_content
2728
self.seen_target_tag = False
29+
self.inside_target_tag = False
2830

2931
def check(self, content):
3032
self.feed(content)
@@ -35,19 +37,35 @@ def handle_starttag(self, tag, attrs):
3537
return
3638
attrs = dict(attrs)
3739
if self.matcher_attrs.items() <= attrs.items():
40+
self.inside_target_tag = True
3841
assert not self.seen_target_tag, 'Multiple matching tags found'
3942
self.seen_target_tag = True
4043
assert self.expected_attrs.items() <= attrs.items()
4144

45+
def handle_endtag(self, tag):
46+
if tag == self.target_tag:
47+
# Yes, this will be incorrect with nested tags of same kind. We don't need
48+
# nested tags at all.
49+
self.inside_target_tag = False
50+
51+
def handle_data(self, data):
52+
if self.inside_target_tag and self.expected_content is not None:
53+
assert data.strip() == self.expected_content
54+
55+
4256

4357
@pytest.mark.django_db
44-
def test_post_admin_filters_basics(admin_client, posts): # noqa: ARG001
58+
@pytest.mark.usefixtures('posts')
59+
def test_post_admin_filters_basics(admin_client, unused_tag):
4560
posts_count = 10
46-
post_authors = set(Post.objects.values_list('author__username', flat=True))
47-
post_audiences = set(Post.objects.values_list('audience', flat=True))
61+
post_authors = dict(Post.objects.values_list('author__id', 'author__username'))
62+
post_audiences = {p.audience: p.get_audience_display() for p in Post.objects.all()}
63+
post_tags = dict(Tag.objects.filter(post__isnull=False).distinct().values_list('id', 'name'))
4864

4965
assert post_authors
5066
assert post_audiences
67+
assert post_tags
68+
target_options = {'author': post_authors, 'audience': post_audiences, 'tags': post_tags}
5169

5270
response = admin_client.get(reverse('admin:testapp_post_changelist'))
5371
assert response.status_code == HTTPStatus.OK
@@ -60,36 +78,60 @@ def test_post_admin_filters_basics(admin_client, posts): # noqa: ARG001
6078

6179
assert len(filter_specs) > 0
6280

81+
expected_lookup_kwargs = {
82+
'author': 'author__id__exact',
83+
'audience': 'audience__exact',
84+
'category': 'category__id__exact',
85+
'category_renamed': 'category_renamed__renamed_id__exact',
86+
'tags': 'tags__id__exact',
87+
}
88+
6389
for spec in filter_specs:
6490
if isinstance(spec, (DALFRelatedField, DALFChoicesField, DALFRelatedFieldAjax, DALFRelatedOnlyField)):
6591
filter_choices = list(spec.choices(response.context['cl']))
6692
filter_custom_options = filter_choices.pop()
67-
option_field_name = filter_custom_options.get('field_name', None)
93+
option_field_name = filter_custom_options['field_name']
94+
95+
lookup_kwarg = filter_custom_options['lookup_kwarg']
96+
assert lookup_kwarg == expected_lookup_kwargs[option_field_name]
6897

69-
if option_field_name in ['author', 'audience']:
70-
maybe_id_suffix = '__id' if option_field_name == 'author' else ''
98+
if option_field_name in ['author', 'audience', 'tags']:
7199
validator = MatchingTagValidator(
100+
'select',
101+
{'name': option_field_name},
72102
{
73103
'class': 'django-admin-list-filter admin-autocomplete',
74-
'name': option_field_name,
75-
'data-lookup-kwarg': f'{option_field_name}{maybe_id_suffix}__exact',
76104
'data-theme': 'admin-autocomplete',
105+
'name': option_field_name,
106+
'data-lookup-kwarg': lookup_kwarg,
77107
},
78-
{'name': option_field_name},
79-
'select',
80108
)
81109
validator.check(content)
82110

83-
if option_field_name == 'author':
84-
for author in post_authors:
85-
assert f'{author}</option>' in content
111+
for internal, human in target_options[option_field_name].items():
112+
validator = MatchingTagValidator(
113+
'option',
114+
{'value': f'?{lookup_kwarg}={internal}'},
115+
{},
116+
human
117+
)
118+
validator.check(content)
86119

87-
if option_field_name == 'audience':
88-
for audience in post_audiences:
89-
assert f'<option value="?audience__exact={audience}">' in content
90-
91-
if option_field_name == 'category':
92-
assert 'data-field-name="category"></select>' in content
120+
elif option_field_name in ['category', 'category_renamed']:
121+
validator = MatchingTagValidator(
122+
'select',
123+
{'data-field-name': option_field_name},
124+
{
125+
'class': 'django-admin-list-filter-ajax',
126+
'data-theme': 'admin-autocomplete',
127+
'data-allow-clear': 'true',
128+
'data-lookup-kwarg': lookup_kwarg,
129+
'data-app-label': 'testapp',
130+
'data-model-name': 'post',
131+
'data-field-name': option_field_name,
132+
},
133+
)
134+
validator.check(content)
93135

94136
url_params = '&'.join(
95137
[f'{key}={value}' for key, value in filter_custom_options.items() if key != 'selected_value']
@@ -98,13 +140,24 @@ def test_post_admin_filters_basics(admin_client, posts): # noqa: ARG001
98140
ajax_resonse = admin_client.get(f'/admin/autocomplete/?{url_params}')
99141

100142
assert ajax_resonse['Content-Type'] == 'application/json'
101-
102143
json_response = ajax_resonse.json()
103144
assert json_response
104145

105146
results = json_response.get('results')
106-
pagination = json_response.get('pagination', {}).get('more', None)
107-
108147
assert len(results) == 1
109-
assert pagination is not None
148+
# Even when not named `id`, autocomplete AJAX will helpfully call it so:
149+
assert 'id' in results[0]
150+
151+
pagination = json_response.get('pagination', {}).get('more', None)
110152
assert pagination is False
153+
else:
154+
pytest.fail(f'Unexpected field: {option_field_name}')
155+
156+
# Must not include tags that have no associated Posts.
157+
validator = MatchingTagValidator(
158+
'option',
159+
{'value': f'?tags__id__exact={unused_tag.pk}'},
160+
{}
161+
)
162+
validator.feed(content)
163+
assert not validator.seen_target_tag

0 commit comments

Comments
 (0)