Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ Release notes
update_fields. As a result the usage_policy value was not included in the UPDATE.
https://github.com/aboutcode-org/dejacode/issues/200

- Improve the Owner assignment process on a Product/Component form.
Owner not found in the Dataspace are now automatically created.
https://github.com/aboutcode-org/dejacode/issues/239

### Version 5.2.1

- Fix the models documentation navigation.
Expand Down
4 changes: 4 additions & 0 deletions component_catalog/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ class Meta:
"dependencies": forms.Textarea(attrs={"rows": 2}),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["owner"].user = self.user

def clean_packages_ids(self):
packages_ids = self.cleaned_data.get("packages_ids")
if packages_ids:
Expand Down
20 changes: 20 additions & 0 deletions component_catalog/migrations/0011_alter_component_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.6 on 2025-02-18 23:07

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('component_catalog', '0010_component_risk_score_package_risk_score'),
('organization', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='component',
name='owner',
field=models.ForeignKey(blank=True, help_text='Owner is the creator or maintainer of a component, typically the current copyright holder. This field is optional but recommended.', null=True, on_delete=django.db.models.deletion.PROTECT, to='organization.owner'),
),
]
10 changes: 3 additions & 7 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,13 +709,9 @@ class BaseComponentMixin(
blank=True,
on_delete=models.PROTECT,
help_text=format_lazy(
"Owner is an optional field selected by the user to identify the original "
"creator (copyright holder) of the {verbose_name}. "
"If this {verbose_name} is in its original, unmodified state, the {verbose_name}"
" owner is associated with the original author/publisher. "
"If this {verbose_name} has been copied and modified, "
"the {verbose_name} owner should be the owner that has copied and "
"modified it.",
"Owner is the creator or maintainer of a {verbose_name}, typically the "
"current copyright holder. "
"This field is optional but recommended.",
verbose_name=_(verbose_name),
),
)
Expand Down
17 changes: 14 additions & 3 deletions dje/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,11 +824,22 @@ def label_from_instance(obj):

class OwnerChoiceField(forms.ModelChoiceField):
def to_python(self, value):
try:
return self.queryset.get(name=value)
except ObjectDoesNotExist:
if not value:
return super().to_python(value)

# 1. Get from the current Dataspace.
if obj := self.queryset.get_or_none(name=value):
return obj

# 2. Attempt to copy from reference if already existing there,
# or create in local Dataspace.
# This requires the `user` object to be set to this field instance.
if user := getattr(self, "user", None):
if obj := self.queryset.copy_or_create(user, name=value):
return obj

return super().to_python(value)

def prepare_value(self, value):
try:
return self.queryset.get(id=value)
Expand Down
27 changes: 27 additions & 0 deletions dje/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,33 @@ def get_or_none(self, *args, **kwargs):
with suppress(self.model.DoesNotExist, ValidationError):
return self.get(*args, **kwargs)

def copy_or_create(self, user, *args, **kwargs):
"""
Look for the object in the reference Dataspace and copy it to the user
Dataspace.
If the object is not found in the reference Dataspace, the object is directly
create in the user Dataspace.
"""
from dje.copier import copy_object

model_class = self.model
dataspace = user.dataspace
reference_dataspace = Dataspace.objects.get_reference()
reference_object = None

if dataspace and reference_dataspace and dataspace != reference_dataspace:
with suppress(ObjectDoesNotExist):
reference_object = model_class.objects.get(
*args,
**kwargs,
dataspace=reference_dataspace,
)

if reference_object:
return copy_object(reference_object, dataspace, user)

return self.create(*args, **kwargs, dataspace=dataspace)

def group_by(self, field_name):
"""Return a dict of QS instances grouped by the given `field_name`."""
# Not using a dict comprehension to support QS without `.order_by(field_name)`.
Expand Down
13 changes: 13 additions & 0 deletions dje/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,16 @@ def test_dataspace_tab_permissions_enabled(self):
def test_dataspaced_model_clean_extra_spaces_in_identifier_fields(self):
owner = Owner.objects.create(name="contains extra spaces", dataspace=self.dataspace)
self.assertEqual("contains extra spaces", owner.name)

def test_dataspaced_queryset_copy_or_create(self):
owner_in_reference = Owner.objects.create(name="Owner1", dataspace=self.nexb_user.dataspace)
self.assertTrue(self.nexb_user.dataspace.is_reference)
copied_owner = Owner.objects.copy_or_create(
self.alternate_user, name=owner_in_reference.name
)
self.assertEqual(self.alternate_dataspace, copied_owner.dataspace)
self.assertEqual(owner_in_reference.name, copied_owner.name)

created_owner = Owner.objects.copy_or_create(self.alternate_user, name="Owner2")
self.assertEqual(self.alternate_dataspace, copied_owner.dataspace)
self.assertEqual("Owner2", created_owner.name)
4 changes: 4 additions & 0 deletions product_portfolio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ class Meta:
),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["owner"].user = self.user

def assign_object_perms(self, user):
assign_perm("view_product", user, self.instance)

Expand Down
20 changes: 20 additions & 0 deletions product_portfolio/migrations/0011_alter_product_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.6 on 2025-02-18 23:07

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('organization', '0001_initial'),
('product_portfolio', '0010_productcomponent_weighted_risk_score_and_more'),
]

operations = [
migrations.AlterField(
model_name='product',
name='owner',
field=models.ForeignKey(blank=True, help_text='Owner is the creator or maintainer of a product, typically the current copyright holder. This field is optional but recommended.', null=True, on_delete=django.db.models.deletion.PROTECT, to='organization.owner'),
),
]
4 changes: 2 additions & 2 deletions product_portfolio/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1770,7 +1770,7 @@ def test_product_portfolio_product_add_view_create_proper(self):
data = {
"name": "Name",
"version": "1.0",
"owner": owner1.name,
"owner": "Unknown",
"license_expression": l1.key,
"copyright": "Copyright",
"notice_text": "Notice",
Expand All @@ -1787,7 +1787,7 @@ def test_product_portfolio_product_add_view_create_proper(self):

response = self.client.post(add_url, data, follow=True)
product = Product.objects.get_queryset(self.super_user).get(name="Name", version="1.0")
self.assertEqual(owner1, product.owner)
self.assertEqual("Unknown", product.owner.name)
self.assertEqual(configuration_status, product.configuration_status)
self.assertEqual(l1.key, product.license_expression)
expected = "Product "Name 1.0" was successfully created."
Expand Down