Skip to content

Commit

Permalink
Refactor the implementation of Keywords on forms #48 (#54)
Browse files Browse the repository at this point in the history
* Add KeywordViewSet to the API #48

Signed-off-by: tdruez <tdruez@nexb.com>

* Refactor the implementation of Keywords on forms #48

Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez committed May 14, 2024
1 parent 86fcc7f commit 8827373
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 103 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ Release notes
Packages (excluding the version).
https://github.com/nexB/dejacode/issues/113

- Refactor the implementation of Keywords on forms to allow more flexibilty.
Existing Keywords are suggested for consistency but any values is now allowed.
https://github.com/nexB/dejacode/issues/48

### Version 5.0.1

- Improve the stability of the "Check for new Package versions" feature.
Expand Down
34 changes: 34 additions & 0 deletions component_catalog/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,3 +1029,37 @@ def get_queryset(self):
"child",
)
)


class KeywordSerializer(DataspacedSerializer):
class Meta:
model = ComponentKeyword
fields = (
"api_url",
"uuid",
"label",
"description",
)
extra_kwargs = {
"api_url": {
"view_name": "api_v2:componentkeyword-detail",
"lookup_field": "uuid",
},
}


class KeywordViewSet(CreateRetrieveUpdateListViewSet):
queryset = ComponentKeyword.objects.all()
serializer_class = KeywordSerializer
lookup_field = "uuid"
search_fields = (
"label",
"description",
)
search_fields_autocomplete = ("label",)
ordering_fields = (
"label",
"created_date",
"last_modified_date",
)
allow_reference_access = True
47 changes: 23 additions & 24 deletions component_catalog/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from dje.forms import DataspacedModelForm
from dje.forms import DefaultOnAdditionLabelMixin
from dje.forms import Group
from dje.forms import JSONListChoiceField
from dje.forms import JSONListField
from dje.forms import OwnerChoiceField
from dje.forms import autocomplete_placeholder
from dje.mass_update import DejacodeMassUpdateForm
Expand All @@ -56,23 +56,29 @@
class SetKeywordsChoicesFormMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

keywords_field = self.fields.get("keywords")
if keywords_field:
if keywords_field := self.fields.get("keywords"):
keywords_qs = ComponentKeyword.objects.scope(self.dataspace)
labels = keywords_qs.values_list("label", flat=True)
keywords_field.choices = [(label, label) for label in labels]
keywords_field.widget.attrs.update(
{
"data-list": ", ".join(labels),
}
)
keywords_field.widget.attrs.update({"data-list": ", ".join(labels)})


class KeywordsField(JSONListField):
def __init__(self, **kwargs):
widget = AutocompleteInput(
attrs={
"data-api_url": reverse_lazy("api_v2:componentkeyword-list"),
},
display_link=False,
display_attribute="label",
)
kwargs.setdefault("widget", widget)
kwargs.setdefault("required", False)
super().__init__(**kwargs)


class ComponentForm(
LicenseExpressionFormMixin,
DefaultOnAdditionLabelMixin,
SetKeywordsChoicesFormMixin,
DataspacedModelForm,
):
default_on_addition_fields = ["configuration_status"]
Expand All @@ -83,10 +89,7 @@ class ComponentForm(
]
color_initial = True

keywords = JSONListChoiceField(
required=False,
widget=AwesompleteInputWidget(attrs=autocomplete_placeholder),
)
keywords = KeywordsField()

packages_ids = forms.CharField(
widget=forms.HiddenInput,
Expand Down Expand Up @@ -262,16 +265,12 @@ def clean(self):
class PackageForm(
LicenseExpressionFormMixin,
PackageFieldsValidationMixin,
SetKeywordsChoicesFormMixin,
DataspacedModelForm,
):
save_as = True
color_initial = True

keywords = JSONListChoiceField(
required=False,
widget=AwesompleteInputWidget(attrs=autocomplete_placeholder),
)
keywords = KeywordsField()

collect_data = forms.BooleanField(
required=False,
Expand Down Expand Up @@ -910,7 +909,7 @@ class ComponentAdminForm(
SetKeywordsChoicesFormMixin,
DataspacedAdminForm,
):
keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
widget=AdminAwesompleteInputWidget(attrs=autocomplete_placeholder),
)
Expand Down Expand Up @@ -944,7 +943,7 @@ class PackageAdminForm(
SetKeywordsChoicesFormMixin,
DataspacedAdminForm,
):
keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
widget=AdminAwesompleteInputWidget(attrs=autocomplete_placeholder),
)
Expand Down Expand Up @@ -990,7 +989,7 @@ class ComponentMassUpdateForm(
DejacodeMassUpdateForm,
):
raw_id_fields = ["owner"]
keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
widget=AwesompleteInputWidget(attrs=autocomplete_placeholder),
)
Expand Down Expand Up @@ -1063,7 +1062,7 @@ class Meta:
class PackageMassUpdateForm(
LicenseExpressionFormMixin, SetKeywordsChoicesFormMixin, DejacodeMassUpdateForm
):
keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
widget=AwesompleteInputWidget(attrs=autocomplete_placeholder),
)
Expand Down
17 changes: 14 additions & 3 deletions component_catalog/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# See https://github.com/nexB/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

import json
import os
from urllib.parse import urlparse
Expand All @@ -29,7 +30,7 @@
from component_catalog.models import Package
from component_catalog.models import Subcomponent
from component_catalog.programming_languages import PROGRAMMING_LANGUAGES
from dje.forms import JSONListChoiceField
from dje.forms import JSONListField
from dje.importers import BaseImporter
from dje.importers import BaseImportModelForm
from dje.importers import ComponentRelatedFieldImportMixin
Expand All @@ -40,6 +41,12 @@
from product_portfolio.models import ProductComponent
from product_portfolio.models import ProductPackage

keywords_help = (
get_help_text(Component, "keywords")
+ " You can add multiple keywords by separating them with commas, like this: "
"keyword1, keyword2."
)


class OwnerChoiceField(ModelChoiceFieldForImport):
def get_suggestions(self, value, limit=5):
Expand Down Expand Up @@ -157,8 +164,9 @@ class ComponentImportForm(
identifier_field="label",
)

keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
help_text=keywords_help,
)

acceptable_linkages = ImportMultipleChoiceField(
Expand All @@ -172,6 +180,7 @@ class Meta:
"children",
"packages",
"completion_level",
"request_count",
# JSONField not supported
"dependencies",
)
Expand Down Expand Up @@ -237,8 +246,9 @@ class PackageImportForm(
identifier_field="label",
)

keywords = JSONListChoiceField(
keywords = JSONListField(
required=False,
help_text=keywords_help,
)

class Meta:
Expand All @@ -248,6 +258,7 @@ class Meta:
# JSONField not supported
"dependencies",
"file_references",
"request_count",
"parties",
]

Expand Down
67 changes: 9 additions & 58 deletions component_catalog/tests/test_importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import NON_FIELD_ERRORS
from django.forms import widgets
from django.test import TestCase
from django.urls import reverse

Expand All @@ -24,7 +23,6 @@
from component_catalog.importers import SubcomponentImporter
from component_catalog.models import AcceptableLinkage
from component_catalog.models import Component
from component_catalog.models import ComponentKeyword
from component_catalog.models import ComponentStatus
from component_catalog.models import ComponentType
from component_catalog.models import Subcomponent
Expand Down Expand Up @@ -639,49 +637,21 @@ def test_component_import_keywords(self):
formset_data["form-0-keywords"] = {"key": "value"}
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [{"keywords": ["Enter a list of values."]}]
expected = [{"keywords": ['Value must be valid JSON list: ["item1", "item2"].']}]
self.assertEqual(expected, importer.formset.errors)

formset_data["form-0-keywords"] = "keyword1"
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [
{"keywords": ["Select a valid choice. keyword1 is not one of the available choices."]}
]
self.assertEqual(expected, importer.formset.errors)

formset_data["form-0-keywords"] = "keyword1,keyword2"
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [
{"keywords": ["Select a valid choice. keyword1 is not one of the available choices."]}
]
self.assertEqual(expected, importer.formset.errors)

k1 = ComponentKeyword.objects.create(label="Keyword1", dataspace=self.dataspace)
k2 = ComponentKeyword.objects.create(label="Keyword2", dataspace=self.dataspace)

formset_data["form-0-keywords"] = f"{k1.label}"
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

formset_data["form-0-keywords"] = f" , {k1.label}, ,"
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

formset_data["form-0-keywords"] = f"{k1.label}, {k2.label}"
formset_data["form-0-keywords"] = ", keyword1,keyword2 ,"
importer = ComponentImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

importer.save_all()
self.assertEqual(1, len(importer.results["added"]))
added_component = importer.results["added"][0]
self.assertEqual(["Keyword1", "Keyword2"], added_component.keywords)

# Making sure the TextInput widget is used in place of the default Select
# as loading multiple initial values is problematic on the Select
keywords_field = importer.formset.forms[0].fields["keywords"]
self.assertIsInstance(keywords_field.widget, widgets.TextInput)
self.assertEqual(["keyword1", "keyword2"], added_component.keywords)

def test_component_import_license_expression(self):
formset_data = {
Expand Down Expand Up @@ -1171,49 +1141,29 @@ def test_package_import_keywords(self):
formset_data["form-0-keywords"] = {"key": "value"}
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [{"keywords": ["Enter a list of values."]}]
expected = [{"keywords": ['Value must be valid JSON list: ["item1", "item2"].']}]
self.assertEqual(expected, importer.formset.errors)

formset_data["form-0-keywords"] = "keyword1"
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [
{"keywords": ["Select a valid choice. keyword1 is not one of the available choices."]}
]
self.assertEqual(expected, importer.formset.errors)
self.assertTrue(importer.formset.is_valid())

formset_data["form-0-keywords"] = "keyword1,keyword2"
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertFalse(importer.formset.is_valid())
expected = [
{"keywords": ["Select a valid choice. keyword1 is not one of the available choices."]}
]
self.assertEqual(expected, importer.formset.errors)

k1 = ComponentKeyword.objects.create(label="Keyword1", dataspace=self.dataspace)
k2 = ComponentKeyword.objects.create(label="Keyword2", dataspace=self.dataspace)

formset_data["form-0-keywords"] = f"{k1.label}"
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

formset_data["form-0-keywords"] = f" , {k1.label}, ,"
formset_data["form-0-keywords"] = " , keyword1, ,"
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

formset_data["form-0-keywords"] = f"{k1.label}, {k2.label}"
formset_data["form-0-keywords"] = "keyword1, keyword2"
importer = PackageImporter(self.super_user, formset_data=formset_data)
self.assertTrue(importer.formset.is_valid())

importer.save_all()
self.assertEqual(1, len(importer.results["added"]))
added_package = importer.results["added"][0]
self.assertEqual(["Keyword1", "Keyword2"], added_package.keywords)

# Making sure the TextInput widget is used in place of the default Select
# as loading multiple initial values is problematic on the Select
keywords_field = importer.formset.forms[0].fields["keywords"]
self.assertIsInstance(keywords_field.widget, widgets.TextInput)
self.assertEqual(["keyword1", "keyword2"], added_package.keywords)

def test_package_import_scancode_json_input_issues(self):
file = os.path.join(TESTFILES_LOCATION, "package_empty.json")
Expand Down Expand Up @@ -1257,6 +1207,7 @@ def test_package_import_scancode_json_proper_input(self):
def test_package_import_scancode_json_proper_no_summary(self):
file = os.path.join(TESTFILES_LOCATION, "package_from_scancode_no_summary.json")
importer = PackageImporter(self.super_user, file)
self.assertTrue(importer.is_valid())
self.assertEqual([], importer.fatal_errors)
self.assertTrue(importer.is_from_scancode)
importer.save_all()
Expand Down
5 changes: 2 additions & 3 deletions component_catalog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4641,8 +4641,6 @@ def test_component_catalog_component_form_add(self):
self.assertIn(status, form.fields["configuration_status"].queryset)
self.assertNotIn(alternate_status, form.fields["configuration_status"].queryset)

self.assertEqual([(keyword.label, keyword.label)], form.fields["keywords"].choices)

# NameVersionValidationFormMixin
data = {
"name": self.component1.name,
Expand Down Expand Up @@ -4688,7 +4686,7 @@ def test_component_catalog_component_form_add(self):
"copyright": "Copyright",
"notice_text": "Notice",
"description": "Description",
"keywords": [keyword.label],
"keywords": [keyword.label, "Another keyword"],
"primary_language": "Python",
"homepage_url": "https://nexb.com",
"configuration_status": status.pk,
Expand All @@ -4701,6 +4699,7 @@ def test_component_catalog_component_form_add(self):
self.assertEqual(owner, component.owner)
self.assertEqual(status, component.configuration_status)
self.assertEqual(license1.key, component.license_expression)
self.assertEqual(["Key1", "Another keyword"], component.keywords)

def test_component_catalog_component_form_assigned_packages(self):
data = {
Expand Down

0 comments on commit 8827373

Please sign in to comment.