Skip to content

Commit b4b8cab

Browse files
authored
Create missing Owner from the Product/Component form #239 (#264)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 91bed23 commit b4b8cab

File tree

10 files changed

+111
-12
lines changed

10 files changed

+111
-12
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ Release notes
105105
update_fields. As a result the usage_policy value was not included in the UPDATE.
106106
https://github.com/aboutcode-org/dejacode/issues/200
107107

108+
- Improve the Owner assignment process on a Product/Component form.
109+
Owner not found in the Dataspace are now automatically created.
110+
https://github.com/aboutcode-org/dejacode/issues/239
111+
108112
### Version 5.2.1
109113

110114
- Fix the models documentation navigation.

component_catalog/forms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ class Meta:
159159
"dependencies": forms.Textarea(attrs={"rows": 2}),
160160
}
161161

162+
def __init__(self, *args, **kwargs):
163+
super().__init__(*args, **kwargs)
164+
self.fields["owner"].user = self.user
165+
162166
def clean_packages_ids(self):
163167
packages_ids = self.cleaned_data.get("packages_ids")
164168
if packages_ids:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.1.6 on 2025-02-18 23:07
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('component_catalog', '0010_component_risk_score_package_risk_score'),
11+
('organization', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name='component',
17+
name='owner',
18+
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'),
19+
),
20+
]

component_catalog/models.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -709,13 +709,9 @@ class BaseComponentMixin(
709709
blank=True,
710710
on_delete=models.PROTECT,
711711
help_text=format_lazy(
712-
"Owner is an optional field selected by the user to identify the original "
713-
"creator (copyright holder) of the {verbose_name}. "
714-
"If this {verbose_name} is in its original, unmodified state, the {verbose_name}"
715-
" owner is associated with the original author/publisher. "
716-
"If this {verbose_name} has been copied and modified, "
717-
"the {verbose_name} owner should be the owner that has copied and "
718-
"modified it.",
712+
"Owner is the creator or maintainer of a {verbose_name}, typically the "
713+
"current copyright holder. "
714+
"This field is optional but recommended.",
719715
verbose_name=_(verbose_name),
720716
),
721717
)

dje/forms.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,11 +824,22 @@ def label_from_instance(obj):
824824

825825
class OwnerChoiceField(forms.ModelChoiceField):
826826
def to_python(self, value):
827-
try:
828-
return self.queryset.get(name=value)
829-
except ObjectDoesNotExist:
827+
if not value:
830828
return super().to_python(value)
831829

830+
# 1. Get from the current Dataspace.
831+
if obj := self.queryset.get_or_none(name=value):
832+
return obj
833+
834+
# 2. Attempt to copy from reference if already existing there,
835+
# or create in local Dataspace.
836+
# This requires the `user` object to be set to this field instance.
837+
if user := getattr(self, "user", None):
838+
if obj := self.queryset.copy_or_create(user, name=value):
839+
return obj
840+
841+
return super().to_python(value)
842+
832843
def prepare_value(self, value):
833844
try:
834845
return self.queryset.get(id=value)

dje/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,33 @@ def get_or_none(self, *args, **kwargs):
572572
with suppress(self.model.DoesNotExist, ValidationError):
573573
return self.get(*args, **kwargs)
574574

575+
def copy_or_create(self, user, *args, **kwargs):
576+
"""
577+
Look for the object in the reference Dataspace and copy it to the user
578+
Dataspace.
579+
If the object is not found in the reference Dataspace, the object is directly
580+
create in the user Dataspace.
581+
"""
582+
from dje.copier import copy_object
583+
584+
model_class = self.model
585+
dataspace = user.dataspace
586+
reference_dataspace = Dataspace.objects.get_reference()
587+
reference_object = None
588+
589+
if dataspace and reference_dataspace and dataspace != reference_dataspace:
590+
with suppress(ObjectDoesNotExist):
591+
reference_object = model_class.objects.get(
592+
*args,
593+
**kwargs,
594+
dataspace=reference_dataspace,
595+
)
596+
597+
if reference_object:
598+
return copy_object(reference_object, dataspace, user)
599+
600+
return self.create(*args, **kwargs, dataspace=dataspace)
601+
575602
def group_by(self, field_name):
576603
"""Return a dict of QS instances grouped by the given `field_name`."""
577604
# Not using a dict comprehension to support QS without `.order_by(field_name)`.

dje/tests/test_models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,16 @@ def test_dataspace_tab_permissions_enabled(self):
245245
def test_dataspaced_model_clean_extra_spaces_in_identifier_fields(self):
246246
owner = Owner.objects.create(name="contains extra spaces", dataspace=self.dataspace)
247247
self.assertEqual("contains extra spaces", owner.name)
248+
249+
def test_dataspaced_queryset_copy_or_create(self):
250+
owner_in_reference = Owner.objects.create(name="Owner1", dataspace=self.nexb_user.dataspace)
251+
self.assertTrue(self.nexb_user.dataspace.is_reference)
252+
copied_owner = Owner.objects.copy_or_create(
253+
self.alternate_user, name=owner_in_reference.name
254+
)
255+
self.assertEqual(self.alternate_dataspace, copied_owner.dataspace)
256+
self.assertEqual(owner_in_reference.name, copied_owner.name)
257+
258+
created_owner = Owner.objects.copy_or_create(self.alternate_user, name="Owner2")
259+
self.assertEqual(self.alternate_dataspace, copied_owner.dataspace)
260+
self.assertEqual("Owner2", created_owner.name)

product_portfolio/forms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ class Meta:
145145
),
146146
}
147147

148+
def __init__(self, *args, **kwargs):
149+
super().__init__(*args, **kwargs)
150+
self.fields["owner"].user = self.user
151+
148152
def assign_object_perms(self, user):
149153
assign_perm("view_product", user, self.instance)
150154

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.1.6 on 2025-02-18 23:07
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('organization', '0001_initial'),
11+
('product_portfolio', '0010_productcomponent_weighted_risk_score_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name='product',
17+
name='owner',
18+
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'),
19+
),
20+
]

product_portfolio/tests/test_views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,7 +1770,7 @@ def test_product_portfolio_product_add_view_create_proper(self):
17701770
data = {
17711771
"name": "Name",
17721772
"version": "1.0",
1773-
"owner": owner1.name,
1773+
"owner": "Unknown",
17741774
"license_expression": l1.key,
17751775
"copyright": "Copyright",
17761776
"notice_text": "Notice",
@@ -1787,7 +1787,7 @@ def test_product_portfolio_product_add_view_create_proper(self):
17871787

17881788
response = self.client.post(add_url, data, follow=True)
17891789
product = Product.objects.get_queryset(self.super_user).get(name="Name", version="1.0")
1790-
self.assertEqual(owner1, product.owner)
1790+
self.assertEqual("Unknown", product.owner.name)
17911791
self.assertEqual(configuration_status, product.configuration_status)
17921792
self.assertEqual(l1.key, product.license_expression)
17931793
expected = "Product &quot;Name 1.0&quot; was successfully created."

0 commit comments

Comments
 (0)