@@ -276,7 +276,68 @@ def prepare_value(self, value):
276276 return value
277277
278278
279- ignored_patterns_help_markdown = """
279+ class KeyValueListField (forms .CharField ):
280+ """
281+ A Django form field that displays as a textarea and converts each line of
282+ "key:value" input into a list of dictionaries with customizable keys.
283+
284+ Each line of the textarea input is split into key-value pairs,
285+ removing leading/trailing whitespace and empty lines. The resulting list of
286+ dictionaries is then stored as the field value.
287+ """
288+
289+ widget = forms .Textarea
290+
291+ def __init__ (self , * args , key_name = "key" , value_name = "value" , ** kwargs ):
292+ """Initialize the KeyValueListField with custom key and value names."""
293+ self .key_name = key_name
294+ self .value_name = value_name
295+ super ().__init__ (* args , ** kwargs )
296+
297+ def to_python (self , value ):
298+ """
299+ Split the textarea input into lines, convert each line to a dictionary,
300+ and remove empty lines.
301+ """
302+ if not value :
303+ return None
304+
305+ items = []
306+ for line in value .splitlines ():
307+ line = line .strip ()
308+ if not line :
309+ continue
310+ parts = line .split (":" , 1 )
311+ if len (parts ) != 2 :
312+ raise ValidationError (
313+ f"Invalid input line: '{ line } '. "
314+ f"Each line must contain exactly one ':' character."
315+ )
316+ key , value = parts
317+ key = key .strip ()
318+ value = value .strip ()
319+ if not key or not value :
320+ raise ValidationError (
321+ f"Invalid input line: '{ line } '. "
322+ f"Both key and value must be non-empty."
323+ )
324+ items .append ({self .key_name : key , self .value_name : value })
325+
326+ return items
327+
328+ def prepare_value (self , value ):
329+ """
330+ Join the list of dictionaries into a string with newlines,
331+ using the "key:value" format.
332+ """
333+ if value is not None and isinstance (value , list ):
334+ value = "\n " .join (
335+ f"{ item [self .key_name ]} :{ item [self .value_name ]} " for item in value
336+ )
337+ return value
338+
339+
340+ ignored_patterns_help = """
280341Provide one or more path patterns to be ignored, one per line.
281342
282343Each pattern should follow the syntax of Unix shell-style wildcards:
@@ -295,18 +356,27 @@ def prepare_value(self, value):
295356Be cautious when specifying patterns to avoid unintended exclusions.
296357"""
297358
359+ ignored_dependency_scopes_help = """
360+ Specify certain dependency scopes to be ignored for a given package type.
361+
362+ This allows you to exclude dependencies from being created or resolved based on their
363+ scope using the `package_type:scope` syntax, **one per line**.
364+ For example: `npm:devDependencies`
365+ """
366+
298367
299368class ProjectSettingsForm (forms .ModelForm ):
300369 settings_fields = [
301370 "ignored_patterns" ,
371+ "ignored_dependency_scopes" ,
302372 "attribution_template" ,
303373 "product_name" ,
304374 "product_version" ,
305375 ]
306376 ignored_patterns = ListTextarea (
307377 label = "Ignored patterns" ,
308378 required = False ,
309- help_text = convert_markdown_to_html (ignored_patterns_help_markdown .strip ()),
379+ help_text = convert_markdown_to_html (ignored_patterns_help .strip ()),
310380 widget = forms .Textarea (
311381 attrs = {
312382 "class" : "textarea is-dynamic" ,
@@ -315,6 +385,20 @@ class ProjectSettingsForm(forms.ModelForm):
315385 },
316386 ),
317387 )
388+ ignored_dependency_scopes = KeyValueListField (
389+ label = "Ignored dependency scopes" ,
390+ required = False ,
391+ help_text = convert_markdown_to_html (ignored_dependency_scopes_help .strip ()),
392+ widget = forms .Textarea (
393+ attrs = {
394+ "class" : "textarea is-dynamic" ,
395+ "rows" : 2 ,
396+ "placeholder" : "npm:devDependencies\n pypi:tests" ,
397+ },
398+ ),
399+ key_name = "package_type" ,
400+ value_name = "scope" ,
401+ )
318402 attribution_template = forms .CharField (
319403 label = "Attribution template" ,
320404 required = False ,
0 commit comments