Skip to content

Commit 5f344f6

Browse files
committed
locations: everything else
1 parent f844d2c commit 5f344f6

345 files changed

Lines changed: 108363 additions & 3944 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/integration-tests.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,16 @@ jobs:
4141
"tests/notifications_test.py",
4242
"tests/tool_config.py",
4343
"openapi-validatator",
44-
4544
]
4645
os: [alpine, debian]
46+
v3_feature_locations: [true, false]
47+
exclude:
48+
# standalone create endpoint page is gone in v3
49+
- v3_feature_locations: true
50+
test-case: "tests/endpoint_test.py"
4751
fail-fast: false
52+
env:
53+
DD_V3_FEATURE_LOCATIONS: ${{ matrix.v3_feature_locations }}
4854

4955
steps:
5056
- name: Checkout

.github/workflows/rest-framework-tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ on:
66
platform:
77
type: string
88
default: "linux/amd64"
9+
v3_feature_locations:
10+
type: boolean
11+
default: false
912

1013
jobs:
1114
unit_tests:
1215
name: Rest Framework Unit Tests
1316
runs-on: ${{ inputs.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
14-
1517
strategy:
1618
matrix:
1719
os: [alpine, debian]
@@ -53,10 +55,11 @@ jobs:
5355

5456
# no celery or initializer needed for unit tests
5557
- name: Unit tests
56-
timeout-minutes: 20
58+
timeout-minutes: 25
5759
run: docker compose up --no-deps --exit-code-from uwsgi uwsgi
5860
env:
5961
DJANGO_VERSION: ${{ matrix.os }}
62+
DD_V3_FEATURE_LOCATIONS: ${{ inputs.v3_feature_locations }}
6063

6164
- name: Logs
6265
if: failure()

.github/workflows/unit-tests.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ jobs:
2323

2424
test-rest-framework:
2525
strategy:
26-
matrix:
27-
platform: ['linux/amd64', 'linux/arm64']
28-
fail-fast: false
26+
matrix:
27+
platform: ['linux/amd64', 'linux/arm64']
28+
v3_feature_locations: [ false, true ]
29+
fail-fast: false
2930
needs: build-docker-containers
3031
uses: ./.github/workflows/rest-framework-tests.yml
3132
secrets: inherit
3233
with:
33-
platform: ${{ matrix.platform}}
34+
platform: ${{ matrix.platform }}
35+
v3_feature_locations: ${{ matrix.v3_feature_locations }}
3436

3537
# only run integration tests for linux/amd64 (default)
3638
test-user-interface:

docker-compose.override.integration_tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ services:
3636
DD_SECURE_CROSS_ORIGIN_OPENER_POLICY: 'None'
3737
DD_SECRET_KEY: "${DD_SECRET_KEY:-.}"
3838
DD_EMAIL_URL: "smtp://mailhog:1025"
39+
DD_V3_FEATURE_LOCATIONS: ${DD_V3_FEATURE_LOCATIONS:-False}
3940
celerybeat:
4041
environment:
4142
DD_DATABASE_URL: ${DD_TEST_DATABASE_URL:-postgresql://defectdojo:defectdojo@postgres:5432/test_defectdojo}
4243
celeryworker:
4344
entrypoint: ['/wait-for-it.sh', '${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432}', '-t', '30', '--', '/entrypoint-celery-worker-dev.sh']
4445
environment:
4546
DD_DATABASE_URL: ${DD_TEST_DATABASE_URL:-postgresql://defectdojo:defectdojo@postgres:5432/test_defectdojo}
47+
DD_V3_FEATURE_LOCATIONS: ${DD_V3_FEATURE_LOCATIONS:-False}
4648
initializer:
4749
environment:
4850
PYTHONWARNINGS: error # We are strict about Warnings during testing

docker-compose.override.unit_tests_cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ services:
2828
DD_CELERY_BROKER_PATH: '/dojo.celerydb.sqlite'
2929
DD_CELERY_BROKER_PARAMS: ''
3030
DD_JIRA_EXTRA_ISSUE_TYPES: 'Vulnerability' # Shouldn't trigger a migration error
31+
DD_V3_FEATURE_LOCATIONS: ${DD_V3_FEATURE_LOCATIONS:-False}
3132
celerybeat: !reset
3233
celeryworker: !reset
3334
initializer: !reset

dojo/api_helpers/__init__.py

Whitespace-only changes.

dojo/api_helpers/filters.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timedelta
4+
from typing import TYPE_CHECKING
5+
6+
from django.utils import timezone
7+
from django_filters import (
8+
BaseInFilter,
9+
BooleanFilter,
10+
CharFilter,
11+
DateTimeFromToRangeFilter,
12+
FilterSet,
13+
MultipleChoiceFilter,
14+
NumberFilter,
15+
OrderingFilter,
16+
)
17+
18+
if TYPE_CHECKING:
19+
from collections.abc import Iterable
20+
21+
22+
# https://django-filter.readthedocs.io/en/stable/ref/filters.html#baseinfilter
23+
class NumberInFilter(BaseInFilter, NumberFilter):
24+
25+
"""Support for searches like `id__in`."""
26+
27+
28+
# https://django-filter.readthedocs.io/en/stable/ref/filters.html#baseinfilter
29+
class CharFieldInFilter(BaseInFilter, CharFilter):
30+
31+
"""Support for searches like `id__in`."""
32+
33+
def filter(self, qs, value):
34+
if not value:
35+
return qs
36+
if isinstance(value, str):
37+
value = [v.strip() for v in value.split(",") if v.strip()]
38+
return super().filter(qs, value)
39+
40+
41+
class StaticMethodFilters(FilterSet):
42+
43+
"""Static methods to make setting new filters easier."""
44+
45+
@staticmethod
46+
def set_class_variables(context: dict, class_vars: dict) -> None:
47+
"""Set the contents of `class_vars` into the supplied context."""
48+
context.update(class_vars)
49+
50+
@staticmethod
51+
def create_char_filters(
52+
field_name: str,
53+
help_text_header: str,
54+
context: dict,
55+
) -> None:
56+
"""
57+
Create all the filters needed for a CharFilter.
58+
59+
- Exact Match
60+
- Not Exact Match
61+
- Contains
62+
- Not Contains
63+
- Starts with
64+
- Ends with
65+
"""
66+
return StaticMethodFilters.set_class_variables(
67+
context,
68+
{
69+
f"{field_name}_exact": CharFilter(
70+
field_name=field_name,
71+
lookup_expr="iexact",
72+
help_text=f"{help_text_header}: Exact Match",
73+
),
74+
f"{field_name}_not_exact": CharFilter(
75+
field_name=field_name,
76+
lookup_expr="iexact",
77+
help_text=f"{help_text_header}: Not Exact Match",
78+
exclude=True,
79+
),
80+
f"{field_name}_contains": CharFilter(
81+
field_name=field_name,
82+
lookup_expr="icontains",
83+
help_text=f"{help_text_header}: Contains",
84+
),
85+
f"{field_name}_not_contains": CharFilter(
86+
field_name=field_name,
87+
lookup_expr="icontains",
88+
help_text=f"{help_text_header}: Not Contains",
89+
exclude=True,
90+
),
91+
f"{field_name}_starts_with": CharFilter(
92+
field_name=field_name,
93+
lookup_expr="istartswith",
94+
help_text=f"{help_text_header}: Starts With",
95+
),
96+
f"{field_name}_ends_with": CharFilter(
97+
field_name=field_name,
98+
lookup_expr="iendswith",
99+
help_text=f"{help_text_header}: Ends With",
100+
),
101+
f"{field_name}_includes": CharFieldInFilter(
102+
field_name=field_name,
103+
lookup_expr="in",
104+
help_text=f"{help_text_header}: Included in List",
105+
),
106+
f"{field_name}_not_includes": CharFieldInFilter(
107+
field_name=field_name,
108+
lookup_expr="in",
109+
help_text=f"{help_text_header}: Not Included in List",
110+
exclude=True,
111+
),
112+
},
113+
)
114+
115+
@staticmethod
116+
def create_integer_filters(
117+
field_name: str,
118+
help_text_header: str,
119+
context: dict,
120+
) -> None:
121+
"""
122+
Create all the filters needed for an IntegerFilter.
123+
124+
- Exact Match
125+
- Not Exact Match
126+
- Greater Than or Equal to
127+
- Less Than or Equal to
128+
- ID included in the list
129+
- ID Not included in the list
130+
"""
131+
return StaticMethodFilters.set_class_variables(
132+
context,
133+
{
134+
f"{field_name}_equals": NumberFilter(
135+
field_name=field_name,
136+
lookup_expr="exact",
137+
help_text=f"{help_text_header}: Equals",
138+
),
139+
f"{field_name}_not_equals": NumberFilter(
140+
field_name=field_name,
141+
lookup_expr="exact",
142+
help_text=f"{help_text_header}: Not Equals",
143+
exclude=True,
144+
),
145+
f"{field_name}_greater_than_or_equal_to": NumberFilter(
146+
field_name=field_name,
147+
lookup_expr="gte",
148+
help_text=f"{help_text_header}: Greater Than or Equal To",
149+
),
150+
f"{field_name}_less_than_or_equal_to": NumberFilter(
151+
field_name=field_name,
152+
lookup_expr="lte",
153+
help_text=f"{help_text_header}: Less Than or Equal To",
154+
),
155+
f"{field_name}_includes": NumberInFilter(
156+
field_name=field_name,
157+
lookup_expr="in",
158+
help_text=f"{help_text_header}: Included in List",
159+
),
160+
f"{field_name}_not_includes": NumberInFilter(
161+
field_name=field_name,
162+
lookup_expr="in",
163+
help_text=f"{help_text_header}: Not Included in List",
164+
exclude=True,
165+
),
166+
},
167+
)
168+
169+
@staticmethod
170+
def create_choice_filters(
171+
field_name: str,
172+
help_text_header: str,
173+
choices: list[tuple[str]],
174+
context: dict,
175+
) -> None:
176+
"""Create a filter for requiring a single choice."""
177+
return StaticMethodFilters.set_class_variables(
178+
context,
179+
{
180+
f"{field_name}_equals": MultipleChoiceFilter(
181+
field_name=field_name,
182+
choices=choices,
183+
help_text=f"{help_text_header}: Choice Filter",
184+
),
185+
},
186+
)
187+
188+
@staticmethod
189+
def create_datetime_filters(
190+
field_name: str,
191+
help_text_header: str,
192+
context: dict,
193+
) -> None:
194+
"""Create a filter for setting datetime filters."""
195+
return StaticMethodFilters.set_class_variables(
196+
context,
197+
{
198+
field_name: DateTimeFromToRangeFilter(
199+
field_name=field_name,
200+
help_text=f"{help_text_header}: DateTime Range Filter",
201+
),
202+
},
203+
)
204+
205+
@staticmethod
206+
def create_boolean_filters(
207+
field_name: str,
208+
help_text_header: str,
209+
context: dict,
210+
) -> None:
211+
"""Create a filter for boolean filters."""
212+
return StaticMethodFilters.set_class_variables(
213+
context,
214+
{
215+
field_name: BooleanFilter(
216+
field_name=field_name,
217+
help_text=f"{help_text_header}: True/False",
218+
),
219+
},
220+
)
221+
222+
@staticmethod
223+
def create_ordering_filters(
224+
context: dict,
225+
field_names: Iterable[str],
226+
) -> None:
227+
"""Create an ordering filter for all fields in the dict."""
228+
return StaticMethodFilters.set_class_variables(
229+
context,
230+
{"ordering": OrderingFilter(fields=[(field_name, field_name) for field_name in field_names])},
231+
)
232+
233+
234+
class CommonFilters(StaticMethodFilters):
235+
236+
"""Helpers for FilterSets to reduce copy/past code."""
237+
238+
StaticMethodFilters.create_integer_filters("id", "ID", locals())
239+
StaticMethodFilters.create_datetime_filters("created_at", "Created At", locals())
240+
StaticMethodFilters.create_datetime_filters("updated_at", "Updated At", locals())
241+
242+
243+
def filter_timestamp(queryset, name, value):
244+
try:
245+
date = datetime.strptime(value, "%Y-%m-%d")
246+
except ValueError:
247+
return queryset
248+
249+
start_datetime = timezone.make_aware(datetime.combine(date, datetime.min.time()))
250+
end_datetime = timezone.make_aware(datetime.combine(date + timedelta(days=1), datetime.min.time()))
251+
252+
return queryset.filter(**{f"{name}__gte": start_datetime, f"{name}__lt": end_datetime})
253+
254+
255+
def csv_filter(queryset, name, value):
256+
return queryset.filter(**{f"{name}__in": value.split(",")})
257+
258+
259+
class CustomOrderingFilter(OrderingFilter):
260+
def __init__(self, *args, **kwargs):
261+
self.reverse_fields = kwargs.pop("reverse_fields", [])
262+
super().__init__(*args, **kwargs)
263+
264+
def filter(self, qs, value):
265+
if value in {None, ""}:
266+
return qs
267+
268+
ordering = []
269+
270+
for param in value:
271+
stripped_param = param.strip()
272+
raw_field = stripped_param.lstrip("-")
273+
reverse = raw_field in self.reverse_fields
274+
275+
if reverse:
276+
if stripped_param.startswith("-"):
277+
ordering.append(raw_field)
278+
else:
279+
ordering.append(f"-{raw_field}")
280+
else:
281+
ordering.append(stripped_param)
282+
283+
return qs.order_by(*ordering)

0 commit comments

Comments
 (0)