1+ from django .http import HttpRequest , QueryDict
2+ from django .template import Context , Template
13from django .test import TestCase
24from django .urls import reverse
5+
36from webstack_django_sorting import settings
7+ from webstack_django_sorting .common import render_sort_anchor
48
59from . import models
610
711
8- class IndexTest (TestCase ):
12+ class SortingTest (TestCase ):
13+ """Test Django template sorting functionality."""
14+
915 def setUp (self ):
1016 self .url = reverse ("secret_list" )
11-
1217 self .foo_file = models .SecretFile .objects .create (filename = "foo.txt" , order = 1 , size = 1024 )
1318 self .bar_file = models .SecretFile .objects .create (filename = "bar.txt" , order = 2 , size = 512 )
1419
@@ -19,77 +24,201 @@ def test_list(self):
1924
2025 def test_sorting_direction (self ):
2126 response = self .client .get (self .url , {"sort" : "order" , "dir" : "asc" })
22- values = [self .foo_file , self .bar_file ]
23- self .assertQuerySetEqual (response .context ["secret_files" ], values )
27+ self .assertQuerySetEqual (response .context ["secret_files" ], [self .foo_file , self .bar_file ])
2428
2529 response = self .client .get (self .url , {"sort" : "order" , "dir" : "desc" })
26- values = [self .bar_file , self .foo_file ]
27- self .assertQuerySetEqual (response .context ["secret_files" ], values )
30+ self .assertQuerySetEqual (response .context ["secret_files" ], [self .bar_file , self .foo_file ])
31+
32+ # Invalid direction defaults to asc
33+ response = self .client .get (self .url , {"sort" : "order" , "dir" : "INVALID" })
34+ self .assertQuerySetEqual (response .context ["secret_files" ], [self .foo_file , self .bar_file ])
35+
36+ def test_sorting_by_different_fields (self ):
37+ """Test sorting works on multiple field types."""
38+ response = self .client .get (self .url , {"sort" : "size" , "dir" : "asc" })
39+ self .assertQuerySetEqual (response .context ["secret_files" ], [self .bar_file , self .foo_file ])
2840
29- # Nothing wrong happens with an invalid direction and ASC is used
30- response = self .client .get (self .url , {"sort" : "order" , "dir" : "NOT" })
31- values = [self .foo_file , self .bar_file ]
32- self .assertQuerySetEqual (response .context ["secret_files" ], values )
41+ response = self .client .get (self .url , {"sort" : "filename" , "dir" : "asc" })
42+ self .assertQuerySetEqual (response .context ["secret_files" ], [self .bar_file , self .foo_file ])
3343
34- def test_sorting_argument (self ):
35- # Nothing wrong happens with invalid sort argument
36- response = self .client .get (self .url , {"sort" : "NOT EXISTING " })
44+ def test_invalid_sort_field (self ):
45+ """Invalid sort field is handled gracefully."""
46+ response = self .client .get (self .url , {"sort" : "NOT_EXISTING " })
3747 self .assertContains (response , "foo.txt" )
3848
49+ def test_empty_queryset (self ):
50+ """Sorting an empty queryset doesn't raise errors."""
51+ models .SecretFile .objects .all ().delete ()
52+ response = self .client .get (self .url , {"sort" : "order" , "dir" : "asc" })
53+ self .assertEqual (response .status_code , 200 )
3954
40- class NullsTestCase (TestCase ):
41- def setUp (self ):
42- self .nulls_first_url = reverse ("nulls_first" )
43- self .nulls_last_url = reverse ("nulls_last" )
4455
56+ class NullsOrderingTest (TestCase ):
57+ """Test NULL value ordering."""
58+
59+ def setUp (self ):
4560 self .foo_file = models .SecretFile .objects .create (filename = "foo.txt" , order = 1 , size = 1024 )
4661 self .bar_file = models .SecretFile .objects .create (filename = "bar.txt" , order = 2 , size = 512 )
4762 self .none_file = models .SecretFile .objects .create (filename = None , order = 3 , size = 512 )
4863
49- def test_sorting_nulls_first (self ):
50- """Verify None sorted field_name is in first places when sorting in asc and desc order """
51-
52- # asc order
53- values = [ self .none_file , self . bar_file , self . foo_file ]
54- response = self . client . get (
55- self .nulls_first_url , { "sort" : "filename" , "nulls" : "first" , "dir" : "asc" }
64+ def test_nulls_first (self ):
65+ """NULL values appear first when nulls=first. """
66+ url = reverse ( "nulls_first" )
67+ response = self . client . get ( url , { "sort" : "filename" , "nulls" : "first" , "dir" : "asc" })
68+ self .assertQuerySetEqual (
69+ list ( response . context [ "secret_files" ]),
70+ [ self .none_file , self . bar_file , self . foo_file ],
5671 )
57- self .assertQuerySetEqual (list (response .context ["secret_files" ]), values )
5872
59- # desc order
60- values = [self .none_file , self .foo_file , self .bar_file ]
61- response = self .client .get (
62- self .nulls_first_url ,
63- {"sort" : "filename" , "nulls" : "first" , "dir" : "desc" },
73+ def test_nulls_last (self ):
74+ """NULL values appear last when nulls=last."""
75+ url = reverse ("nulls_last" )
76+ response = self .client .get (url , {"sort" : "filename" , "nulls" : "last" , "dir" : "asc" })
77+ self .assertQuerySetEqual (
78+ list (response .context ["secret_files" ]),
79+ [self .bar_file , self .foo_file , self .none_file ],
6480 )
65- self .assertQuerySetEqual (list (response .context ["secret_files" ]), values )
6681
67- def test_sorting_nulls_last (self ):
68- """Verify None sorted field_name is in last places when sorting in asc and desc order."""
82+ def test_invalid_nulls_param_raises_404 (self ):
83+ """Invalid nulls parameter raises 404 when configured."""
84+ url = reverse ("nulls_last" )
85+ saved = settings .INVALID_FIELD_RAISES_404
86+ try :
87+ settings .INVALID_FIELD_RAISES_404 = True
88+ response = self .client .get (url , {"sort" : "filename" , "nulls" : "invalid" , "dir" : "asc" })
89+ self .assertEqual (response .status_code , 404 )
90+ finally :
91+ settings .INVALID_FIELD_RAISES_404 = saved
6992
70- # asc order
71- values = [self .bar_file , self .foo_file , self .none_file ]
72- response = self .client .get (
73- self .nulls_last_url , {"sort" : "filename" , "nulls" : "last" , "dir" : "asc" }
74- )
75- self .assertQuerySetEqual (list (response .context ["secret_files" ]), values )
7693
77- # desc order
78- values = [self .foo_file , self .bar_file , self .none_file ]
94+ class Jinja2Test (TestCase ):
95+ """Test Jinja2 template support."""
96+
97+ def setUp (self ):
98+ self .foo_file = models .SecretFile .objects .create (filename = "foo.txt" , order = 1 , size = 1024 )
99+ self .bar_file = models .SecretFile .objects .create (filename = "bar.txt" , order = 2 , size = 512 )
100+
101+ def test_jinja_sorting (self ):
102+ """Jinja2 templates sort correctly."""
103+ url = reverse ("jinja_secret_list" )
104+
105+ # Ascending
106+ response = self .client .get (url , {"sort" : "order" , "dir" : "asc" })
107+ content = response .content .decode ()
108+ self .assertLess (content .find ("foo.txt" ), content .find ("bar.txt" ))
109+
110+ # Descending
111+ response = self .client .get (url , {"sort" : "order" , "dir" : "desc" })
112+ content = response .content .decode ()
113+ self .assertGreater (content .find ("foo.txt" ), content .find ("bar.txt" ))
114+
115+ def test_jinja_nulls_ordering (self ):
116+ """Jinja2 templates handle NULL ordering."""
117+ models .SecretFile .objects .create (filename = None , order = 3 , size = 512 )
118+
79119 response = self .client .get (
80- self . nulls_last_url , {"sort" : "filename" , "nulls" : "last " , "dir" : "desc " }
120+ reverse ( "jinja_nulls_first" ) , {"sort" : "filename" , "nulls" : "first " , "dir" : "asc " }
81121 )
82- self .assertQuerySetEqual (list (response .context ["secret_files" ]), values )
122+ content = response .content .decode ()
123+ self .assertLess (content .find ("bar.txt" ), content .find ("foo.txt" ))
124+
125+
126+ class RenderSortAnchorTest (TestCase ):
127+ """Test the render_sort_anchor function - core anchor rendering logic."""
128+
129+ def _make_request (self , query_params = None ):
130+ request = HttpRequest ()
131+ request .path = "/test/"
132+ request .GET = QueryDict (mutable = True )
133+ if query_params :
134+ for key , value in query_params .items ():
135+ request .GET [key ] = value
136+ return request
137+
138+ def test_basic_anchor (self ):
139+ """Anchor renders with correct href and title."""
140+ request = self ._make_request ()
141+ result = render_sort_anchor (request , "name" , "Name" , "asc" )
142+ self .assertIn ('href="/test/?sort=name&dir=asc"' , result )
143+ self .assertIn ('title="Name"' , result )
144+ self .assertIn (">Name</a>" , result )
145+
146+ def test_three_way_toggle (self ):
147+ """Test the three-way sorting toggle: none -> asc -> desc -> none."""
148+ # No sort -> asc (with asc default)
149+ result = render_sort_anchor (self ._make_request (), "name" , "Name" , "asc" )
150+ self .assertIn ("dir=asc" , result )
151+
152+ # asc -> desc
153+ result = render_sort_anchor (
154+ self ._make_request ({"sort" : "name" , "dir" : "asc" }), "name" , "Name" , "asc"
155+ )
156+ self .assertIn ("dir=desc" , result )
157+ self .assertIn (settings .DEFAULT_SORT_UP , result )
83158
84- def test_check_values (self ):
85- # Internal INVALID_FIELD_RAISES_404 is set at loading (no override_settings)
86- saved = settings .INVALID_FIELD_RAISES_404
159+ # desc -> none (empty dir)
160+ result = render_sort_anchor (
161+ self ._make_request ({"sort" : "name" , "dir" : "desc" }), "name" , "Name" , "asc"
162+ )
163+ self .assertIn (settings .DEFAULT_SORT_DOWN , result )
164+ self .assertNotIn ("dir=asc" , result )
165+ self .assertNotIn ("dir=desc" , result )
166+
167+ def test_desc_default_direction (self ):
168+ """With desc default, first click sorts descending."""
169+ result = render_sort_anchor (self ._make_request (), "name" , "Name" , "desc" )
170+ self .assertIn ("dir=desc" , result )
171+
172+ def test_css_class_support (self ):
173+ """CSS classes are added to anchor when configured."""
174+ saved_asc = settings .SORTING_CSS_CLASS_ASC
175+ saved_desc = settings .SORTING_CSS_CLASS_DESC
87176 try :
88- settings .INVALID_FIELD_RAISES_404 = True
89- response = self .client .get (
90- self .nulls_last_url , {"sort" : "filename" , "nulls" : "foo" , "dir" : "asc" }
177+ settings .SORTING_CSS_CLASS_ASC = "sorted-asc"
178+ settings .SORTING_CSS_CLASS_DESC = "sorted-desc"
179+
180+ result = render_sort_anchor (
181+ self ._make_request ({"sort" : "name" , "dir" : "asc" }), "name" , "Name" , "asc"
91182 )
183+ self .assertIn ('class="sorted-asc"' , result )
184+
185+ result = render_sort_anchor (
186+ self ._make_request ({"sort" : "name" , "dir" : "desc" }), "name" , "Name" , "asc"
187+ )
188+ self .assertIn ('class="sorted-desc"' , result )
92189 finally :
93- settings .INVALID_FIELD_RAISES_404 = saved
190+ settings .SORTING_CSS_CLASS_ASC = saved_asc
191+ settings .SORTING_CSS_CLASS_DESC = saved_desc
192+
193+
194+ class TemplateTagParsingTest (TestCase ):
195+ """Test Django template tag parsing."""
196+
197+ def _render (self , template_str , context_vars = None ):
198+ template = Template (template_str )
199+ request = HttpRequest ()
200+ request .path = "/test/"
201+ request .GET = QueryDict ()
202+ context = Context ({"request" : request , ** (context_vars or {})})
203+ return template .render (context )
204+
205+ def test_anchor_with_quoted_title (self ):
206+ result = self ._render ('{% load sorting_tags %}{% anchor field_name "Title" %}' )
207+ self .assertIn ("field_name" , result )
208+ self .assertIn ("Title" , result )
209+
210+ def test_anchor_minimal (self ):
211+ """Field name is capitalized when no title given."""
212+ result = self ._render ("{% load sorting_tags %}{% anchor field_name %}" )
213+ self .assertIn ("Field_name" , result )
214+
215+ def test_anchor_with_variable_title (self ):
216+ result = self ._render (
217+ "{% load sorting_tags %}{% anchor field_name my_title %}" ,
218+ {"my_title" : "Dynamic Title" },
219+ )
220+ self .assertIn ("Dynamic Title" , result )
94221
95- self .assertEqual (response .status_code , 404 )
222+ def test_anchor_with_desc_default (self ):
223+ result = self ._render ('{% load sorting_tags %}{% anchor field_name "Title" "desc" %}' )
224+ self .assertIn ("dir=desc" , result )
0 commit comments