8
8
9
9
from dalf .admin import DALFChoicesField , DALFRelatedField , DALFRelatedFieldAjax , DALFRelatedOnlyField
10
10
11
- from .models import Post
11
+ from .models import Post , Tag
12
12
13
13
csrf_token_pattern = re .compile (r'name="csrfmiddlewaretoken" value="([^"]+)"' )
14
14
@@ -19,12 +19,14 @@ class MatchingTagValidator(HTMLParser):
19
19
Instances of this class are not reusable. Create a new one for every ``check`` call.
20
20
"""
21
21
22
- def __init__ (self , expected_attrs , matcher_attrs , tag ):
22
+ def __init__ (self , tag , matcher_attrs , expected_attrs , expected_content = None ):
23
23
super ().__init__ ()
24
- self .expected_attrs = expected_attrs
25
24
self .matcher_attrs = matcher_attrs
26
25
self .target_tag = tag
26
+ self .expected_attrs = expected_attrs
27
+ self .expected_content = expected_content
27
28
self .seen_target_tag = False
29
+ self .inside_target_tag = False
28
30
29
31
def check (self , content ):
30
32
self .feed (content )
@@ -35,19 +37,35 @@ def handle_starttag(self, tag, attrs):
35
37
return
36
38
attrs = dict (attrs )
37
39
if self .matcher_attrs .items () <= attrs .items ():
40
+ self .inside_target_tag = True
38
41
assert not self .seen_target_tag , 'Multiple matching tags found'
39
42
self .seen_target_tag = True
40
43
assert self .expected_attrs .items () <= attrs .items ()
41
44
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
+
42
56
43
57
@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 ):
45
60
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' ))
48
64
49
65
assert post_authors
50
66
assert post_audiences
67
+ assert post_tags
68
+ target_options = {'author' : post_authors , 'audience' : post_audiences , 'tags' : post_tags }
51
69
52
70
response = admin_client .get (reverse ('admin:testapp_post_changelist' ))
53
71
assert response .status_code == HTTPStatus .OK
@@ -60,36 +78,60 @@ def test_post_admin_filters_basics(admin_client, posts): # noqa: ARG001
60
78
61
79
assert len (filter_specs ) > 0
62
80
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
+
63
89
for spec in filter_specs :
64
90
if isinstance (spec , (DALFRelatedField , DALFChoicesField , DALFRelatedFieldAjax , DALFRelatedOnlyField )):
65
91
filter_choices = list (spec .choices (response .context ['cl' ]))
66
92
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 ]
68
97
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' ]:
71
99
validator = MatchingTagValidator (
100
+ 'select' ,
101
+ {'name' : option_field_name },
72
102
{
73
103
'class' : 'django-admin-list-filter admin-autocomplete' ,
74
- 'name' : option_field_name ,
75
- 'data-lookup-kwarg' : f'{ option_field_name } { maybe_id_suffix } __exact' ,
76
104
'data-theme' : 'admin-autocomplete' ,
105
+ 'name' : option_field_name ,
106
+ 'data-lookup-kwarg' : lookup_kwarg ,
77
107
},
78
- {'name' : option_field_name },
79
- 'select' ,
80
108
)
81
109
validator .check (content )
82
110
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 )
86
119
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 )
93
135
94
136
url_params = '&' .join (
95
137
[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
98
140
ajax_resonse = admin_client .get (f'/admin/autocomplete/?{ url_params } ' )
99
141
100
142
assert ajax_resonse ['Content-Type' ] == 'application/json'
101
-
102
143
json_response = ajax_resonse .json ()
103
144
assert json_response
104
145
105
146
results = json_response .get ('results' )
106
- pagination = json_response .get ('pagination' , {}).get ('more' , None )
107
-
108
147
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 )
110
152
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