Skip to content

Commit

Permalink
Add ignored_dependency_scopes field on the settings form #1197
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez committed May 22, 2024
1 parent 705ee3f commit bdc7998
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 2 deletions.
88 changes: 86 additions & 2 deletions scanpipe/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,68 @@ def prepare_value(self, value):
return value


ignored_patterns_help_markdown = """
class KeyValueListField(forms.CharField):
"""
A Django form field that displays as a textarea and converts each line of
"key:value" input into a list of dictionaries with customizable keys.
Each line of the textarea input is split into key-value pairs,
removing leading/trailing whitespace and empty lines. The resulting list of
dictionaries is then stored as the field value.
"""

widget = forms.Textarea

def __init__(self, *args, key_name="key", value_name="value", **kwargs):
"""Initialize the KeyValueListField with custom key and value names."""
self.key_name = key_name
self.value_name = value_name
super().__init__(*args, **kwargs)

def to_python(self, value):
"""
Split the textarea input into lines, convert each line to a dictionary,
and remove empty lines.
"""
if not value:
return None

items = []
for line in value.splitlines():
line = line.strip()
if not line:
continue
parts = line.split(":", 1)
if len(parts) != 2:
raise ValidationError(
f"Invalid input line: '{line}'. "
f"Each line must contain exactly one ':' character."
)
key, value = parts
key = key.strip()
value = value.strip()
if not key or not value:
raise ValidationError(
f"Invalid input line: '{line}'. "
f"Both key and value must be non-empty."
)
items.append({self.key_name: key, self.value_name: value})

return items

def prepare_value(self, value):
"""
Join the list of dictionaries into a string with newlines,
using the "key:value" format.
"""
if value is not None and isinstance(value, list):
value = "\n".join(
f"{item[self.key_name]}:{item[self.value_name]}" for item in value
)
return value


ignored_patterns_help = """
Provide one or more path patterns to be ignored, one per line.
Each pattern should follow the syntax of Unix shell-style wildcards:
Expand All @@ -295,18 +356,27 @@ def prepare_value(self, value):
Be cautious when specifying patterns to avoid unintended exclusions.
"""

ignored_dependency_scopes_help = """
Specify certain dependency scopes to be ignored for a given package type.
This allows you to exclude dependencies from being created or resolved based on their
scope using the `package_type:scope` syntax, **one per line**.
For example: `npm:devDependencies`
"""


class ProjectSettingsForm(forms.ModelForm):
settings_fields = [
"ignored_patterns",
"ignored_dependency_scopes",
"attribution_template",
"product_name",
"product_version",
]
ignored_patterns = ListTextarea(
label="Ignored patterns",
required=False,
help_text=convert_markdown_to_html(ignored_patterns_help_markdown.strip()),
help_text=convert_markdown_to_html(ignored_patterns_help.strip()),
widget=forms.Textarea(
attrs={
"class": "textarea is-dynamic",
Expand All @@ -315,6 +385,20 @@ class ProjectSettingsForm(forms.ModelForm):
},
),
)
ignored_dependency_scopes = KeyValueListField(
label="Ignored dependency scopes",
required=False,
help_text=convert_markdown_to_html(ignored_dependency_scopes_help.strip()),
widget=forms.Textarea(
attrs={
"class": "textarea is-dynamic",
"rows": 2,
"placeholder": "npm:devDependencies\npypi:tests",
},
),
key_name="package_type",
value_name="scope",
)
attribution_template = forms.CharField(
label="Attribution template",
required=False,
Expand Down
12 changes: 12 additions & 0 deletions scanpipe/templates/scanpipe/project_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@
{{ form.ignored_patterns.help_text|safe|linebreaksbr }}
</div>
</div>

<div class="field">
<label class="label" for="{{ form.ignored_dependency_scopes.id_for_label }}">
{{ form.ignored_dependency_scopes.label }}
</label>
<div class="control">
{{ form.ignored_dependency_scopes }}
</div>
<div class="help">
{{ form.ignored_dependency_scopes.help_text|safe|linebreaksbr }}
</div>
</div>
</div>
</div>

Expand Down
54 changes: 54 additions & 0 deletions scanpipe/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,60 @@ def test_scanpipe_forms_project_settings_form_update_project_settings(self):
}
self.assertEqual(expected, project.get_env())

def test_scanpipe_forms_project_settings_form_ignored_dependency_scopes(self):
data = {
"name": self.project1.name,
"ignored_dependency_scopes": "",
}
form = ProjectSettingsForm(data=data, instance=self.project1)
self.assertTrue(form.is_valid())

data["ignored_dependency_scopes"] = "bad"
form = ProjectSettingsForm(data=data, instance=self.project1)
self.assertFalse(form.is_valid())
expected = {
"ignored_dependency_scopes": [
"Invalid input line: 'bad'. Each line must contain exactly one ':' "
"character."
]
}
self.assertEqual(expected, form.errors)

data["ignored_dependency_scopes"] = "npm:"
form = ProjectSettingsForm(data=data, instance=self.project1)
self.assertFalse(form.is_valid())

expected = {
"ignored_dependency_scopes": [
"Invalid input line: 'npm:'. Both key and value must be non-empty."
]
}
self.assertEqual(expected, form.errors)

data["ignored_dependency_scopes"] = "npm:devDependencies\npypi:tests"
form = ProjectSettingsForm(data=data, instance=self.project1)
self.assertTrue(form.is_valid())

project = form.save()
expected = {
"ignored_patterns": None,
"ignored_dependency_scopes": [
{"package_type": "npm", "scope": "devDependencies"},
{"package_type": "pypi", "scope": "tests"},
],
"attribution_template": "",
"product_name": "",
"product_version": "",
}
self.assertEqual(expected, project.settings)
expected = {
"ignored_dependency_scopes": [
{"package_type": "npm", "scope": "devDependencies"},
{"package_type": "pypi", "scope": "tests"},
]
}
self.assertEqual(expected, project.get_env())

def test_scanpipe_forms_edit_input_source_tag_form(self):
data = {}
form = EditInputSourceTagForm(data=data)
Expand Down

0 comments on commit bdc7998

Please sign in to comment.