Skip to content

Commit b144b89

Browse files
authored
Add is_locked field on the Product model #310 (#311)
* Add is_locked field on the Product model #310 Signed-off-by: tdruez <[email protected]> * Add is_locked indication in the UI #310 Also, remove edit permissions when Product is_locked Signed-off-by: tdruez <[email protected]> * Refine the warning message UI and disable actions buttons #310 Signed-off-by: tdruez <[email protected]> * Move the is_locked field on the ProductStatus model #310 Signed-off-by: tdruez <[email protected]> * Add unit tests and changelog entry #310 Signed-off-by: tdruez <[email protected]> --------- Signed-off-by: tdruez <[email protected]>
1 parent 5014ff5 commit b144b89

File tree

18 files changed

+247
-87
lines changed

18 files changed

+247
-87
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Release notes
22
=============
33

4+
### Version 5.3.1-dev (unreleased)
5+
6+
- Add new `is_locked` "Locked inventory" field to the ProductStatus model.
7+
When a Product is locked through his status, its inventory cannot be modified.
8+
https://github.com/aboutcode-org/dejacode/issues/189
9+
410
### Version 5.3.0
511

612
- Rename ProductDependency is_resolved to is_pinned.

component_catalog/forms.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,11 @@ def __init__(self, user, *args, **kwargs):
624624
super().__init__(user, *args, **kwargs)
625625

626626
product_field = self.fields["product"]
627-
perms = ["view_product", "change_product"]
628-
product_field.queryset = Product.objects.get_queryset(user, perms=perms)
627+
product_field.queryset = Product.objects.get_queryset(
628+
user=user,
629+
perms=["view_product", "change_product"],
630+
exclude_locked=True,
631+
)
629632

630633
if relation_instance:
631634
help_text = f'"{relation_instance}" will be assigned to the selected product.'
@@ -709,7 +712,9 @@ def __init__(self, request, model, relation_model, *args, **kwargs):
709712
self.relation_model = relation_model
710713
self.dataspace = request.user.dataspace
711714
self.fields["product"].queryset = Product.objects.get_queryset(
712-
request.user, perms=["view_product", "change_product"]
715+
user=request.user,
716+
perms=["view_product", "change_product"],
717+
exclude_locked=True,
713718
)
714719

715720
def get_selected_objects(self):

dejacode/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import git
1616

17-
VERSION = "5.3.0"
17+
VERSION = "5.3.1-dev"
1818

1919
PROJECT_DIR = Path(__file__).resolve().parent
2020
ROOT_DIR = PROJECT_DIR.parent

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ table.packages-table .column-primary_language {
368368
width: 130px;
369369
}
370370
.list-inline-margin-sm .list-inline-item:not(:last-child) {
371-
margin-right: 0.3rem;
371+
margin-right: 0.125rem;
372372
}
373373

374374
/* -- Vulnerability List -- */

dje/templates/object_details_base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<h1 class="header-title text-break">
2323
{% block header_title %}
2424
{{ object|truncatechars:100 }}
25-
<small class="ms-1" style="font-size: 70%">
25+
<small class="ms-1" style="font-size: 65%">
2626
{% if is_user_dataspace %}
2727
{% if has_change_permission %}
2828
{% with object.get_change_url as object_change_url %}

dje/tests/testfiles/test_dataset_pp_only.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"default_on_addition": false,
6262
"label": "Approved",
6363
"text": "Approved",
64-
"request_to_generate": null
64+
"request_to_generate": null,
65+
"is_locked": false
6566
}
6667
},
6768
{

product_portfolio/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class ProductStatusAdmin(BaseStatusAdmin):
8888
"label",
8989
"text",
9090
"default_on_addition",
91+
"is_locked",
9192
"request_to_generate",
9293
"get_dataspace",
9394
)
@@ -99,6 +100,7 @@ class ProductStatusAdmin(BaseStatusAdmin):
99100
"label",
100101
"text",
101102
"default_on_addition",
103+
"is_locked",
102104
"request_to_generate",
103105
"dataspace",
104106
"uuid",

product_portfolio/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ class Meta:
249249
"homepage_url",
250250
"primary_language",
251251
"admin_notes",
252+
"is_active",
252253
"configuration_status",
253254
"contact",
254255
"vcs_url",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.1.9 on 2025-05-30 14:30
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', '0011_alter_component_owner'),
11+
('product_portfolio', '0012_alter_scancodeproject_status_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='productstatus',
17+
name='is_locked',
18+
field=models.BooleanField(db_index=True, default=False, help_text='Marks this product version as read-only, preventing any modifications to its inventory.', verbose_name='Locked inventory'),
19+
),
20+
migrations.AlterField(
21+
model_name='productdependency',
22+
name='for_package',
23+
field=models.ForeignKey(blank=True, help_text='The package that declares this dependency.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='declared_dependencies', to='component_catalog.package'),
24+
),
25+
migrations.AlterField(
26+
model_name='productdependency',
27+
name='resolved_to_package',
28+
field=models.ForeignKey(blank=True, help_text='The resolved package for this dependency. If empty, it indicates the dependency is unresolved.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_from_dependencies', to='component_catalog.package'),
29+
),
30+
]

product_portfolio/models.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ class ProductStatus(BaseStatusMixin, DataspacedModel):
115115
"fields, since DejaCode will be creating the Request automatically."
116116
),
117117
)
118+
is_locked = models.BooleanField(
119+
verbose_name=_("Locked inventory"),
120+
default=False,
121+
db_index=True,
122+
help_text=_(
123+
"Marks this product version as read-only, preventing any modifications to "
124+
"its inventory."
125+
),
126+
)
118127

119128
class Meta(BaseStatusMixin.Meta):
120129
verbose_name_plural = _("product status")
@@ -141,7 +150,9 @@ def get_by_natural_key(self, dataspace_name, uuid):
141150
"""
142151
return self.model.unsecured_objects.get(dataspace__name=dataspace_name, uuid=uuid)
143152

144-
def get_queryset(self, user=None, perms="view_product", include_inactive=False):
153+
def get_queryset(
154+
self, user=None, perms="view_product", include_inactive=False, exclude_locked=False
155+
):
145156
"""
146157
Force the object level protection at the QuerySet level.
147158
Always Return an empty QuerySet unless a `user` is provided.
@@ -162,6 +173,9 @@ def get_queryset(self, user=None, perms="view_product", include_inactive=False):
162173
user, perms, klass=queryset_class, accept_global_perms=False
163174
).scope(user.dataspace)
164175

176+
if exclude_locked:
177+
queryset = queryset.exclude(configuration_status__is_locked=True)
178+
165179
if include_inactive:
166180
return queryset
167181

@@ -350,6 +364,12 @@ def actions_on_status_change(self):
350364
object_id=self.id,
351365
)
352366

367+
@cached_property
368+
def is_locked(self):
369+
if self.configuration_status_id:
370+
return self.configuration_status.is_locked
371+
return False
372+
353373
@cached_property
354374
def all_packages(self):
355375
return Package.objects.filter(

0 commit comments

Comments
 (0)