Skip to content

Commit

Permalink
Add replace_existing_version field on the AddToProduct form #12 (#124)
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez authored May 27, 2024
1 parent 68964a6 commit cfb5726
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 62 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ Release notes
- Enhance Package Import to support modifications.
https://github.com/nexB/dejacode/issues/84

- Add an option on the "Add to Product" form to to replace any existing relationships
with a different version of the same object by the selected object.
https://github.com/nexB/dejacode/issues/12

### Version 5.0.1

- Improve the stability of the "Check for new Package versions" feature.
Expand Down
19 changes: 16 additions & 3 deletions component_catalog/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,17 @@ class AddToProductAdminForm(forms.Form):
queryset=Product.objects.none(),
)
ids = forms.CharField(widget=forms.widgets.HiddenInput)
replace_existing_version = forms.BooleanField(
required=False,
initial=False,
label="Replace existing relationships by newer version.",
help_text=(
"Select this option to replace any existing relationships with a different version "
"of the same object. "
"If more than one version of the object is already assigned, no replacements will be "
"made, and the new version will be added instead."
),
)

def __init__(self, request, model, relation_model, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -663,9 +674,11 @@ def get_selected_objects(self):

def save(self):
product = self.cleaned_data["product"]

return product.assign_objects(
related_objects=self.get_selected_objects(),
user=self.request.user,
replace_version=self.cleaned_data["replace_existing_version"],
)


Expand Down Expand Up @@ -719,15 +732,15 @@ def new_component_from_package_link(self):
href = f"{component_add_url}?package_ids={package.id}"

return HTML(
f"<hr>"
f'<div class="text-center">'
f' <a href="{href}" '
f' id="new-component-link" '
f' class="btn btn-success" '
f' class="btn btn-outline-success" '
f' data-add-url="{component_add_url}">'
f" Add Component from Package data"
f" </a>"
f"</div>"
f"<hr>"
)

def clean_component(self):
Expand All @@ -749,9 +762,9 @@ def helper(self):
helper.layout = Layout(
Fieldset(
None,
self.new_component_from_package_link(),
"object_id",
"component",
self.new_component_from_package_link(),
),
)
return helper
Expand Down
6 changes: 5 additions & 1 deletion component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1915,12 +1915,16 @@ def get_export_cyclonedx_url(self):
return self.get_url("export_cyclonedx")

@classmethod
def get_identifier_fields(cls):
def get_identifier_fields(cls, *args, purl_fields_only=False, **kwargs):
"""
Explicit list of identifier fields as we do not enforce a unique together
on this model.
This is used in the Importer, to catch duplicate entries.
The purl_fields_only option can be use to limit the results.
"""
if purl_fields_only:
return PACKAGE_URL_FIELDS

return ["filename", "download_url", *PACKAGE_URL_FIELDS]

@property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ <h1>Add {% trans opts.verbose_name %} to a product</h1>
The Product provides you with a group of {% trans opts.verbose_name_plural|title %} that are used together, so that
you can generate Attribution documentation for all of the {% trans opts.verbose_name_plural|title %} in that Product.
</p>
<p style="margin-bottom: 10px;">
<div style="margin-bottom: 10px;">
{{ form.non_field_errors }}
<label for="id_product">Product:</label>
{{ form.product.errors }}
{{ form.product }}
<div style="margin-top:15px;margin-bottom:15px;">
{{ form.replace_existing_version }}
{{ form.replace_existing_version.label }}
<p class="grp-help">{{ form.replace_existing_version.help_text }}</p>
</div>
{% if perms.product_portfolio.add_product %}
<a href="{% url 'admin:product_portfolio_product_add' %}" class="add-another"></a>
{% endif %}
{{ form.ct }}
{{ form.ids }}
</p>
</div>
<p id="description" style="margin-bottom: 10px;"></p>
<div class="grp-content-container g-d-c">
<div class="grp-module">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
<script>
$(document).ready(function () {
let add_to_btn = $('#add-to-btn');
let add_to_btn_wrapper = add_to_btn.parent();
document.addEventListener('DOMContentLoaded', function () {
let add_to_btn = document.getElementById('add-to-btn');
let add_to_btn_wrapper = add_to_btn.parentElement;

let handle_button_display = function () {
let count = $('main input[type="checkbox"]:checked').length;
if (count >= 1) {
add_to_btn.removeClass('disabled');
add_to_btn_wrapper.attr('data-bs-original-title', '');
}
else {
add_to_btn.addClass('disabled');
add_to_btn_wrapper.attr('data-bs-original-title', 'Select objects first');
}
let checkedCheckboxes = document.querySelectorAll('main input[type="checkbox"]:checked');
if (checkedCheckboxes.length >= 1) {
add_to_btn.classList.remove('disabled');
add_to_btn_wrapper.setAttribute('data-bs-original-title', '');
} else {
add_to_btn.classList.add('disabled');
add_to_btn_wrapper.setAttribute('data-bs-original-title', 'Select objects first');
}
};

$('main input[type="checkbox"]').on('change', function() {
handle_button_display();
// Adding change event listener to all checkboxes
document.querySelectorAll('main input[type="checkbox"]').forEach(function(checkbox) {
checkbox.addEventListener('change', handle_button_display);
});

// Runs on load to support the "back" button of the browser
// Initial call to handle_button_display to set the correct state on page load
handle_button_display();

// Call handle_button_display when the page is shown (e.g., when navigating back)
window.addEventListener('pageshow', function () {
handle_button_display();
});

$('#add-to-product-modal, #add-to-component-modal').on('show.bs.modal', function (event) {
let checked = $('main input[type="checkbox"]:checked');
// Do not include the select-all as its value is not an id we want to keep
let checked = $('main input[type="checkbox"]:checked').not('#checkbox-select-all');

if (checked.length < 1) {
event.preventDefault();
Expand Down Expand Up @@ -52,5 +58,13 @@
}
});

// Select all forms with id starting with "add-to-"
const forms = document.querySelectorAll('form[id^="add-to-"]');
forms.forEach(function (form) {
form.addEventListener('submit', function () {
NEXB.displayOverlay("Adding objects...");
});
});

});
</script>
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ <h5 class="modal-title">{{ form.helper.modal_title }}</h5>
</div>
<form autocomplete="off" method="{{ form.helper.form_method }}" action="{{ form.helper.form_action }}" id="{{ form.helper.form_id }}" class="{{ form.helper.form_class }}">
<div class="modal-body bg-body-tertiary">
{% crispy form %}
{# Only displayed on list views #}
{% if request.resolver_match.url_name|default:""|slice:"-4:" == "list" %}
<hr>
<h6>Selected objects:</h6>
<ul id="object-repe-list" style="word-break: break-word;"></ul>
<hr>
{% endif %}
{% crispy form %}
</div>
<div class="modal-footer">
<input type="button" name="close" value="Close" class="btn btn-secondary" data-bs-dismiss="modal">
Expand Down
3 changes: 3 additions & 0 deletions component_catalog/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from component_catalog.importers import ComponentImporter
from component_catalog.importers import PackageImporter
from component_catalog.models import PACKAGE_URL_FIELDS
from component_catalog.models import Component
from component_catalog.models import ComponentAssignedLicense
from component_catalog.models import ComponentAssignedPackage
Expand Down Expand Up @@ -369,6 +370,8 @@ def test_component_catalog_models_get_identifier_fields(self):
for model_class, expected in inputs:
self.assertEqual(expected, model_class.get_identifier_fields())

self.assertEqual(PACKAGE_URL_FIELDS, Package.get_identifier_fields(purl_fields_only=True))

def test_component_model_get_absolute_url(self):
c = Component(name="c1", version="1.0", dataspace=self.dataspace)
self.assertEqual("/components/nexB/c1/1.0/", c.get_absolute_url())
Expand Down
15 changes: 12 additions & 3 deletions component_catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,17 @@ def get_form_kwargs(self):
return kwargs

def form_valid(self, form):
created_count, unchanged_count = form.save()
created_count, updated_count, unchanged_count = form.save()
product = form.cleaned_data["product"]
opts = self.model._meta

msg = ""
if created_count:
msg = f'{created_count} {opts.model_name}(s) added to "{product}".'
msg = f'{created_count} {opts.model_name}(s) added to "{product}". '
if updated_count:
msg += f'{updated_count} {opts.model_name}(s) updated on "{product}".'

if msg:
messages.success(self.request, msg)
else:
msg = f"No new {opts.model_name}(s) were assigned to this product."
Expand Down Expand Up @@ -937,12 +942,16 @@ def get_form_kwargs(self):
return kwargs

def form_valid(self, form):
created_count, unchanged_count = form.save()
created_count, updated_count, unchanged_count = form.save()
model_name = self.model._meta.model_name
product_name = form.cleaned_data["product"].name

msg = f"{created_count} {model_name}(s) added to {product_name}."
if updated_count:
msg += f" {updated_count} {model_name}(s) updated on {product_name}."
if unchanged_count:
msg += f" {unchanged_count} {model_name}(s) were already assigned."

messages.success(self.request, msg)
return super().form_valid(form)

Expand Down
13 changes: 12 additions & 1 deletion dje/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,17 @@ def update_from_data(self, user, data, override=False):

return updated_fields

def update(self, **kwargs):
"""
Update this instance with the provided ``kwargs`` values.
The full ``save()`` process will be triggered, including signals, and the
``update_fields`` is automatically set.
"""
for field_name, value in kwargs.items():
setattr(self, field_name, value)

self.save(update_fields=list(kwargs.keys()))

def as_json(self):
try:
serialized_data = serialize(
Expand Down Expand Up @@ -881,7 +892,7 @@ def _get_local_foreign_fields(self):
local_foreign_fields = property(_get_local_foreign_fields)

@classmethod
def get_identifier_fields(cls):
def get_identifier_fields(cls, *args, **kwargs):
"""
Return a list of the fields, based on the Meta unique_together, to be
used to match a unique instance within a Dataspace.
Expand Down
2 changes: 1 addition & 1 deletion policy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def str_with_content_type(self):
return f"{self.label} ({self.content_type.model})"

@classmethod
def get_identifier_fields(cls):
def get_identifier_fields(cls, *args, **kwargs):
"""Hack required by the Component import."""
return ["label"]

Expand Down
Loading

0 comments on commit cfb5726

Please sign in to comment.