Skip to content

Commit 531b6c0

Browse files
cofiemmikethemannlhkabu
authored
Make it easier to see the available wheels per package release (#15087)
* initial investigation of filtering project release wheel files * created controller for generic filterable list Can be filtered using input or select elements. * style UI for filtering release built distributions * add noscript display and js test Fixed tests. Added some missing translation blocks. Slightly improved js. * use js translations * update to use js translations Serialize in html attribute to JSON instead of custom format. Add ability to copy filter url and filter from querystring. Update tests. * update translations * format html template * update translations * Update warehouse/static/js/warehouse/controllers/filter_list_controller.js Co-authored-by: Mike Fiedler <[email protected]> * Update tests/frontend/filter_list_controller_test.js Co-authored-by: Mike Fiedler <[email protected]> * update translations --------- Co-authored-by: Mike Fiedler <[email protected]> Co-authored-by: Nicole Harris <[email protected]>
1 parent dbae193 commit 531b6c0

File tree

9 files changed

+718
-60
lines changed

9 files changed

+718
-60
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/* SPDX-License-Identifier: Apache-2.0 */
2+
3+
/* global expect, beforeEach, afterEach, describe, it, jest */
4+
5+
6+
import {Application} from "@hotwired/stimulus";
7+
import FilterListController from "../../warehouse/static/js/warehouse/controllers/filter_list_controller";
8+
9+
10+
const testFixtureHTMLVisibilityToggle = `
11+
<p id="initial-toggle-visibility-shown" class="hidden initial-toggle-visibility">Initially hidden, should end up shown.</p>
12+
<p id="initial-toggle-visibility-hidden" class="initial-toggle-visibility">Initially shown, should end up hidden.</p>
13+
`;
14+
const testFixtureHTMLShowing = `
15+
<p id="shown-and-total" data-filter-list-target="summary"></p>
16+
`;
17+
const testFixtureHTMLFilters = `
18+
<input id="filter-input" type="text" data-action="filter-list#filter" data-filter-list-target="filter" data-filtered-source="contentType">
19+
<select id="filter-select" data-action="filter-list#filter" data-filter-list-target="filter" data-filtered-source="myattr">
20+
<option selected value="">My Attrs</option>
21+
<option value="myattr1">myattr 1</option>
22+
<option value="myattr2">myattr 2</option>
23+
<option value="myattr3">myattr 3</option>
24+
</select>
25+
`;
26+
const testFixtureHTMLItems = `
27+
<a id="url-update" href="https://example.com#testing" data-filter-list-target="url"></a>
28+
<div id="item-1" data-filter-list-target="item" data-filtered-target-content-type='["contentType1","contentType1a"]' data-filtered-target-myattr='["myattr1"]'>Item 1</div>
29+
<div id="item-2" data-filter-list-target="item" data-filtered-target-content-type='["contentType2","contentType2a"]' data-filtered-target-myattr='["myattr2"]'>Item 2</div>
30+
<div id="item-3" data-filter-list-target="item" data-filtered-target-content-type='["contentType3","contentType3a"]' data-filtered-target-myattr='["myattr3"]'>Item 3</div>
31+
`;
32+
33+
34+
describe("Filter list controller", () => {
35+
describe("is initialized as expected", () => {
36+
describe("makes expected elements visible", () => {
37+
let application;
38+
beforeEach(() => {
39+
document.body.innerHTML = `
40+
<div id="controller" data-controller="filter-list">
41+
${testFixtureHTMLVisibilityToggle}
42+
</div>
43+
`;
44+
45+
application = Application.start();
46+
application.register("filter-list", FilterListController);
47+
});
48+
afterEach(() => {
49+
document.body.innerHTML = "";
50+
application.stop();
51+
});
52+
53+
it("toggles visibility", () => {
54+
const elShown = document.getElementById("initial-toggle-visibility-shown");
55+
expect(elShown.classList).not.toContain("hidden");
56+
57+
const elHidden = document.getElementById("initial-toggle-visibility-hidden");
58+
expect(elHidden.classList).toContain("hidden");
59+
});
60+
});
61+
62+
describe("finds filters and items", () => {
63+
let application;
64+
beforeEach(() => {
65+
document.body.innerHTML = `
66+
<div id="controller" data-controller="filter-list">
67+
${testFixtureHTMLFilters}
68+
${testFixtureHTMLItems}
69+
</div>
70+
`;
71+
72+
application = Application.start();
73+
application.register("filter-list", FilterListController);
74+
});
75+
afterEach(() => {
76+
document.body.innerHTML = "";
77+
application.stop();
78+
});
79+
80+
81+
it("has expected items and filters", () => {
82+
const elController = document.getElementById("controller");
83+
const controller = application.getControllerForElementAndIdentifier(elController, "filter-list");
84+
85+
expect(controller.itemTargets).toHaveLength(3);
86+
expect(controller.itemTargets[0]).toHaveTextContent("Item 1");
87+
expect(controller.itemTargets[1]).toHaveTextContent("Item 2");
88+
expect(controller.itemTargets[2]).toHaveTextContent("Item 3");
89+
90+
expect(controller.filterTargets).toHaveLength(2);
91+
expect(controller.filterTargets[0].id).toEqual("filter-input");
92+
expect(controller.filterTargets[1].id).toEqual("filter-select");
93+
94+
expect(Object.keys(controller.mappingItemFilterData)).toHaveLength(3);
95+
expect(controller.mappingItemFilterData["0"]).toEqual({
96+
"contentType": ["contentType1", "contentType1a"],
97+
"myattr": ["myattr1"],
98+
});
99+
expect(controller.mappingItemFilterData["1"]).toEqual({
100+
"contentType": ["contentType2", "contentType2a"],
101+
"myattr": ["myattr2"],
102+
});
103+
expect(controller.mappingItemFilterData["2"]).toEqual({
104+
"contentType": ["contentType3", "contentType3a"],
105+
"myattr": ["myattr3"],
106+
});
107+
108+
const elP = document.getElementById("url-update");
109+
expect(elP.textContent).toEqual("https://example.com/#testing");
110+
});
111+
});
112+
});
113+
114+
describe("displays count of visible items", () => {
115+
let application;
116+
beforeEach(() => {
117+
document.body.innerHTML = `
118+
<div id="controller" data-controller="filter-list">
119+
${testFixtureHTMLShowing}
120+
${testFixtureHTMLFilters}
121+
${testFixtureHTMLItems}
122+
</div>
123+
`;
124+
125+
application = Application.start();
126+
application.register("filter-list", FilterListController);
127+
});
128+
afterEach(() => {
129+
document.body.innerHTML = "";
130+
application.stop();
131+
});
132+
133+
it("all items begin shown", () => {
134+
const elP = document.getElementById("shown-and-total");
135+
expect(elP.textContent).toEqual("Showing 3 of 3 files.");
136+
});
137+
});
138+
139+
140+
describe("allows filtering", () => {
141+
describe("input text filters the items", () => {
142+
let application;
143+
beforeEach(() => {
144+
document.body.innerHTML = `
145+
<div id="controller" data-controller="filter-list">
146+
${testFixtureHTMLFilters}
147+
${testFixtureHTMLItems}
148+
</div>
149+
`;
150+
151+
application = Application.start();
152+
application.register("filter-list", FilterListController);
153+
});
154+
afterEach(() => {
155+
document.body.innerHTML = "";
156+
application.stop();
157+
});
158+
159+
it("the item classes are updated", () => {
160+
161+
const elFilter = document.getElementById("filter-input");
162+
const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent");
163+
164+
elFilter.value = "2";
165+
166+
// Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated.
167+
// Also ensure the event has been dispatched.
168+
const event = new Event("input");
169+
elFilter.dispatchEvent(event);
170+
expect(dispatchEventSpy).toHaveBeenCalledWith(event);
171+
172+
const elItem1 = document.getElementById("item-1");
173+
expect(elItem1.classList).toContainEqual("hidden");
174+
175+
const elItem2 = document.getElementById("item-2");
176+
expect(elItem2.classList).not.toContainEqual("hidden");
177+
178+
const elItem3 = document.getElementById("item-3");
179+
expect(elItem3.classList).toContainEqual("hidden");
180+
181+
const elP = document.getElementById("url-update");
182+
expect(elP.textContent).toEqual("https://example.com/?contentType=2#testing");
183+
});
184+
});
185+
186+
describe("selecting an option filters the items", () => {
187+
let application;
188+
beforeEach(() => {
189+
document.body.innerHTML = `
190+
<div id="controller" data-controller="filter-list">
191+
${testFixtureHTMLFilters}
192+
${testFixtureHTMLItems}
193+
</div>
194+
`;
195+
196+
application = Application.start();
197+
application.register("filter-list", FilterListController);
198+
});
199+
afterEach(() => {
200+
document.body.innerHTML = "";
201+
application.stop();
202+
});
203+
204+
it("the item classes are updated", () => {
205+
const elFilter = document.getElementById("filter-select");
206+
const dispatchEventSpy = jest.spyOn(elFilter, "dispatchEvent");
207+
208+
elFilter.value = "myattr3";
209+
210+
// Manually trigger the 'input' event to get the MutationObserver that Stimulus uses to be updated.
211+
// Also ensure the event has been dispatched.
212+
const event = new Event("change");
213+
elFilter.dispatchEvent(event);
214+
expect(dispatchEventSpy).toHaveBeenCalledWith(event);
215+
216+
const elItem1 = document.getElementById("item-1");
217+
expect(elItem1.classList).toContainEqual("hidden");
218+
219+
const elItem2 = document.getElementById("item-2");
220+
expect(elItem2.classList).toContainEqual("hidden");
221+
222+
const elItem3 = document.getElementById("item-3");
223+
expect(elItem3.classList).not.toContainEqual("hidden");
224+
225+
const elP = document.getElementById("url-update");
226+
expect(elP.textContent).toEqual("https://example.com/?myattr=myattr3#testing");
227+
});
228+
});
229+
});
230+
});

tests/unit/packaging/test_views.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ def test_detail_rendered(self, db_request):
215215
"maintainers": sorted(users, key=lambda u: u.username.lower()),
216216
"license": None,
217217
"PEP740AttestationViewer": views.PEP740AttestationViewer,
218+
"wheel_filters_all": {"interpreters": [], "abis": [], "platforms": []},
219+
"wheel_filters_params": {
220+
"filename": "",
221+
"interpreters": "",
222+
"abis": "",
223+
"platforms": "",
224+
},
218225
}
219226

220227
def test_detail_renders_files_natural_sort(self, db_request):
@@ -225,17 +232,27 @@ def test_detail_renders_files_natural_sort(self, db_request):
225232
files = [
226233
FileFactory.create(
227234
release=release,
228-
filename=f"{project.name}-{release.version}-{py_ver}.whl",
235+
filename="-".join(
236+
[project.name, release.version, py_ver, py_abi, py_platform]
237+
)
238+
+ ".whl",
229239
python_version="py2.py3",
230240
packagetype="bdist_wheel",
231241
)
232242
for py_ver in ["cp27", "cp310", "cp39"] # intentionally out of order
243+
for py_abi in ["none"]
244+
for py_platform in ["any"]
233245
]
234246
sorted_files = natsorted(files, reverse=True, key=lambda f: f.filename)
235247

236248
result = views.release_detail(release, db_request)
237249

238250
assert result["files"] == sorted_files
251+
assert [file.wheel_filters for file in result["files"]] == [
252+
{"interpreters": ["cp310"], "abis": ["none"], "platforms": ["any"]},
253+
{"interpreters": ["cp39"], "abis": ["none"], "platforms": ["any"]},
254+
{"interpreters": ["cp27"], "abis": ["none"], "platforms": ["any"]},
255+
]
239256

240257
def test_license_from_classifier(self, db_request):
241258
"""A license label is added when a license classifier exists."""

0 commit comments

Comments
 (0)