Skip to content

Commit 1ccdfd3

Browse files
committed
Use a custom filter field to execute filter set queryset method
1 parent e795e57 commit 1ccdfd3

File tree

4 files changed

+87
-103
lines changed

4 files changed

+87
-103
lines changed

readthedocs/core/filters.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Extended classes for django-filter."""
2+
3+
from django_filters import ModelChoiceFilter
4+
5+
6+
class FilteredModelChoiceFilter(ModelChoiceFilter):
7+
8+
"""
9+
A model choice field for customizing choice querysets at initialization.
10+
11+
This extends the model choice field queryset lookup to include executing a
12+
method from the parent filter set. This allows for use of ``self`` on the
13+
filterset, allowing better support for view time filtering.
14+
"""
15+
16+
def __init__(self, *args, **kwargs):
17+
self.queryset_method = kwargs.pop("queryset_method", None)
18+
super().__init__(*args, **kwargs)
19+
20+
def get_queryset(self, request):
21+
if self.queryset_method:
22+
fn = getattr(self.parent, self.queryset_method, None)
23+
assert callable(fn)
24+
return fn()
25+
return super().get_queryset(request)

readthedocs/organizations/filters.py

+48-94
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,45 @@
1414
from django_filters import (
1515
ChoiceFilter,
1616
FilterSet,
17-
ModelChoiceFilter,
1817
OrderingFilter,
1918
)
2019

20+
from readthedocs.core.filters import FilteredModelChoiceFilter
2121
from readthedocs.organizations.constants import ACCESS_LEVELS
2222
from readthedocs.organizations.models import Organization, Team
23-
from readthedocs.projects.models import Project
2423

2524
log = structlog.get_logger(__name__)
2625

2726

27+
class OrganizationFilterSet(FilterSet):
28+
29+
"""
30+
Organization base filter set.
31+
32+
Adds some object attributes that are used for orgaization related queries
33+
and common base querysets for filter fields.
34+
35+
Note, the querysets here are also found in the organization base views and
36+
mixin classes. These are redefined here instead of passing in the querysets
37+
from the view.
38+
39+
:param organization: Organization instance for current view
40+
"""
41+
42+
def __init__(self, *args, **kwargs):
43+
self.organization = kwargs.pop("organization", None)
44+
super().__init__(*args, **kwargs)
45+
46+
def get_organization_queryset(self):
47+
return Organization.objects.for_user(user=self.request.user)
48+
49+
def get_team_queryset(self):
50+
return Team.objects.member(
51+
self.request.user,
52+
organization=self.organization,
53+
).prefetch_related("organization")
54+
55+
2856
class OrganizationSortOrderingFilter(OrderingFilter):
2957

3058
"""Organization list sort ordering django_filters filter."""
@@ -69,16 +97,15 @@ def filter(self, qs, value):
6997
return qs.order_by(*order_bys)
7098

7199

72-
class OrganizationListFilterSet(FilterSet):
100+
class OrganizationListFilterSet(OrganizationFilterSet):
73101

74102
"""Filter and sorting for organization listing page."""
75103

76-
slug = ModelChoiceFilter(
104+
slug = FilteredModelChoiceFilter(
77105
label=_("Organization"),
78106
empty_label=_("All organizations"),
79107
to_field_name="slug",
80-
# Queryset is required, give an empty queryset from the correct model
81-
queryset=Organization.objects.none(),
108+
queryset_method="get_organization_queryset",
82109
method="get_organization",
83110
)
84111

@@ -87,26 +114,11 @@ class OrganizationListFilterSet(FilterSet):
87114
label=_("Sort by"),
88115
)
89116

90-
def __init__(
91-
self,
92-
data=None,
93-
queryset=None,
94-
*,
95-
request=None,
96-
prefix=None,
97-
):
98-
super().__init__(data, queryset, request=request, prefix=prefix)
99-
# Redefine the querysets used for the filter fields using the querysets
100-
# defined at view time. This populates the filter field with only the
101-
# correct related objects for the user. Otherwise, the default for model
102-
# choice filter fields is ``<Model>.objects.all()``.
103-
self.filters["slug"].field.queryset = self.queryset.all()
104-
105117
def get_organization(self, queryset, field_name, organization):
106118
return queryset.filter(slug=organization.slug)
107119

108120

109-
class OrganizationProjectListFilterSet(FilterSet):
121+
class OrganizationProjectListFilterSet(OrganizationFilterSet):
110122

111123
"""
112124
Filter and sorting set for organization project listing page.
@@ -130,46 +142,30 @@ class OrganizationProjectListFilterSet(FilterSet):
130142
:param team_queryset: Organization team list queryset
131143
"""
132144

133-
slug = ModelChoiceFilter(
145+
slug = FilteredModelChoiceFilter(
134146
label=_("Project"),
135147
empty_label=_("All projects"),
136148
to_field_name="slug",
137-
# Queryset is required, give an empty queryset from the correct model
138-
queryset=Project.objects.none(),
149+
queryset_method="get_project_queryset",
139150
method="get_project",
140151
)
141152

142-
teams__slug = ModelChoiceFilter(
153+
teams__slug = FilteredModelChoiceFilter(
143154
label=_("Team"),
144155
empty_label=_("All teams"),
145156
field_name="teams",
146157
to_field_name="slug",
147-
# Queryset is required, give an empty queryset from the correct model
148-
queryset=Team.objects.none(),
158+
queryset_method="get_team_queryset",
149159
)
150160

151-
def __init__(
152-
self,
153-
data=None,
154-
queryset=None,
155-
*,
156-
request=None,
157-
prefix=None,
158-
teams_queryset=None,
159-
):
160-
super().__init__(data, queryset, request=request, prefix=prefix)
161-
# Redefine the querysets used for the filter fields using the querysets
162-
# defined at view time. This populates the filter field with only the
163-
# correct related objects for the user. Otherwise, the default for model
164-
# choice filter fields is ``<Model>.objects.all()``.
165-
self.filters["slug"].field.queryset = self.queryset.all()
166-
self.filters["teams__slug"].field.queryset = teams_queryset.all()
161+
def get_project_queryset(self):
162+
return self.queryset
167163

168164
def get_project(self, queryset, field_name, project):
169165
return queryset.filter(slug=project.slug)
170166

171167

172-
class OrganizationTeamListFilterSet(FilterSet):
168+
class OrganizationTeamListFilterSet(OrganizationFilterSet):
173169

174170
"""
175171
Filter and sorting for organization team listing page.
@@ -184,37 +180,23 @@ class OrganizationTeamListFilterSet(FilterSet):
184180
with the dropdown too.
185181
"""
186182

187-
slug = ModelChoiceFilter(
183+
slug = FilteredModelChoiceFilter(
188184
label=_("Team"),
189185
empty_label=_("All teams"),
190186
field_name="teams",
191187
to_field_name="slug",
192-
# Queryset is required, give an empty queryset from the correct model
193-
queryset=Team.objects.none(),
188+
queryset_method="get_team_queryset",
194189
method="get_team",
195190
)
196191

197-
def __init__(
198-
self,
199-
data=None,
200-
queryset=None,
201-
*,
202-
request=None,
203-
prefix=None,
204-
teams_queryset=None,
205-
):
206-
super().__init__(data, queryset, request=request, prefix=prefix)
207-
# Redefine the querysets used for the filter fields using the querysets
208-
# defined at view time. This populates the filter field with only the
209-
# correct related objects for the user/organization. Otherwise, the
210-
# default for model choice filter fields is ``<Model>.objects.all()``.
211-
self.filters["slug"].field.queryset = queryset.all()
192+
def get_team_queryset(self):
193+
return self.queryset
212194

213195
def get_team(self, queryset, field_name, team):
214196
return queryset.filter(slug=team.slug)
215197

216198

217-
class OrganizationTeamMemberListFilterSet(FilterSet):
199+
class OrganizationTeamMemberListFilterSet(OrganizationFilterSet):
218200

219201
"""
220202
Filter and sorting set for organization member listing page.
@@ -245,12 +227,12 @@ class OrganizationTeamMemberListFilterSet(FilterSet):
245227

246228
ACCESS_OWNER = "owner"
247229

248-
teams__slug = ModelChoiceFilter(
230+
teams__slug = FilteredModelChoiceFilter(
249231
label=_("Team"),
250232
empty_label=_("All teams"),
251233
field_name="teams",
252234
to_field_name="slug",
253-
queryset=Team.objects.none(),
235+
queryset_method="get_team_queryset",
254236
)
255237

256238
access = ChoiceFilter(
@@ -260,34 +242,6 @@ class OrganizationTeamMemberListFilterSet(FilterSet):
260242
method="get_access",
261243
)
262244

263-
def __init__(
264-
self, data=None, queryset=None, *, request=None, prefix=None, organization=None
265-
):
266-
"""
267-
Organization members filter set.
268-
269-
This filter set requires the following additional parameters:
270-
271-
:param organization: Organization for field ``filter()`` and used to
272-
check for organization owner access.
273-
"""
274-
super().__init__(data, queryset, request=request, prefix=prefix)
275-
self.organization = organization
276-
# Redefine the querysets used for the filter fields using the querysets
277-
# defined at view time. This populates the filter field with only the
278-
# correct related objects for the user/organization. Otherwise, the
279-
# default for model choice filter fields is ``<Model>.objects.all()``.
280-
filter_with_user_relationship = True
281-
team_queryset = self.organization.teams
282-
if filter_with_user_relationship:
283-
# XXX remove this conditional and decide which one of these is most
284-
# correct There are reasons for both showing all the teams here and
285-
# only the team that the user has access to.
286-
team_queryset = Team.objects.member(request.user).filter(
287-
organization=self.organization,
288-
)
289-
self.filters["teams__slug"].field.queryset = team_queryset.all()
290-
291245
def get_access(self, queryset, field_name, value):
292246
# Note: the queryset here is effectively against the ``User`` model, and
293247
# is from Organization.members, a union of TeamMember.user and

readthedocs/organizations/views/private.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ def get_context_data(self, **kwargs):
7575
context = super().get_context_data(**kwargs)
7676
if settings.RTD_EXT_THEME_ENABLED:
7777
filter = OrganizationListFilterSet(
78-
self.request.GET, queryset=self.get_queryset()
78+
self.request.GET,
79+
queryset=self.get_queryset(),
80+
request=self.request,
7981
)
8082
context["filter"] = filter
8183
context["organization_list"] = filter.qs

readthedocs/organizations/views/public.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,24 @@ def get_context_data(self, **kwargs):
4848
.filter(organizations=org)
4949
.all()
5050
)
51-
teams = (
52-
Team.objects
53-
.member(self.request.user, organization=org)
54-
.prefetch_related('organization')
55-
.all()
56-
)
5751
if settings.RTD_EXT_THEME_ENABLED:
5852
filter = OrganizationProjectListFilterSet(
5953
self.request.GET,
6054
request=self.request,
6155
queryset=projects,
62-
teams_queryset=teams,
56+
organization=org,
6357
)
6458
context["filter"] = filter
6559
projects = filter.qs
60+
else:
61+
teams = (
62+
Team.objects.member(self.request.user, organization=org)
63+
.prefetch_related("organization")
64+
.all()
65+
)
66+
context["teams"] = teams
67+
6668
context["projects"] = projects
67-
context["teams"] = teams
6869
context["owners"] = org.owners.all()
6970
return context
7071

@@ -121,6 +122,8 @@ def get_context_data(self, **kwargs):
121122
filter = OrganizationTeamListFilterSet(
122123
self.request.GET,
123124
queryset=teams,
125+
request=self.request,
126+
organization=org,
124127
)
125128
context["filter"] = filter
126129
teams = filter.qs

0 commit comments

Comments
 (0)