Skip to content

Commit e900168

Browse files
authored
Enhance Package Import to support modifications #84 (#120)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 1c4fb5e commit e900168

5 files changed

Lines changed: 99 additions & 30 deletions

File tree

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ Release notes
7575
- Add a new AboutCode tab in Package details view.
7676
https://github.com/nexB/dejacode/issues/42
7777

78+
- Enhance Package Import to support modifications.
79+
https://github.com/nexB/dejacode/issues/84
80+
7881
### Version 5.0.1
7982

8083
- Improve the stability of the "Check for new Package versions" feature.

component_catalog/importers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from component_catalog.models import Package
3131
from component_catalog.models import Subcomponent
3232
from component_catalog.programming_languages import PROGRAMMING_LANGUAGES
33+
from dje.fields import SmartFileField
3334
from dje.forms import JSONListField
3435
from dje.importers import BaseImporter
3536
from dje.importers import BaseImportModelForm
@@ -305,10 +306,20 @@ def save(self, commit=True):
305306
return package
306307

307308

309+
class PackageImportableUploadFileForm(forms.Form):
310+
file = SmartFileField(extensions=["csv", "json"])
311+
312+
@property
313+
def header(self):
314+
return "Select a <strong>CSV (.csv) or JSON (.json)</strong> file"
315+
316+
308317
class PackageImporter(BaseImporter):
309318
model_form = PackageImportForm
319+
upload_form_class = PackageImportableUploadFileForm
310320
add_to_product_perm = "product_portfolio.add_productpackage"
311321
relation_model = ProductPackage
322+
update_existing = True
312323

313324
def prepare_data_json(self, data):
314325
"""

component_catalog/tests/test_importers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,40 @@ def test_package_import_prepare_package(self):
13181318
prepared_package = PackageImporter.prepare_package(package_data)
13191319
self.assertEqual(expected, prepared_package)
13201320

1321+
def test_package_import_update_existing(self):
1322+
formset_data = {
1323+
"form-TOTAL_FORMS": "1",
1324+
"form-INITIAL_FORMS": "0",
1325+
"form-0-filename": "filename.zip",
1326+
}
1327+
1328+
importer = PackageImporter(self.super_user, formset_data=formset_data)
1329+
self.assertTrue(importer.formset.is_valid())
1330+
importer.save_all()
1331+
self.assertEqual(1, len(importer.results["added"]))
1332+
1333+
importer = PackageImporter(self.super_user, formset_data=formset_data)
1334+
self.assertTrue(importer.formset.is_valid())
1335+
importer.save_all()
1336+
self.assertEqual(1, len(importer.results["unmodified"]))
1337+
1338+
formset_data = {
1339+
"form-TOTAL_FORMS": "1",
1340+
"form-INITIAL_FORMS": "0",
1341+
"form-0-filename": "filename.zip",
1342+
"form-0-notes": "Notes",
1343+
}
1344+
importer = PackageImporter(self.super_user, formset_data=formset_data)
1345+
self.assertTrue(importer.formset.is_valid())
1346+
importer.save_all()
1347+
self.assertEqual(1, len(importer.results["modified"]))
1348+
modified_package = importer.results["modified"][0]
1349+
self.assertEqual("Notes", modified_package.notes)
1350+
1351+
history_entry = History.objects.get_for_object(modified_package).get()
1352+
expected_messages = "Updated notes from import"
1353+
self.assertEqual(expected_messages, history_entry.change_message)
1354+
13211355
def test_package_import_add_to_product(self):
13221356
admin_user = create_admin("admin_user", self.dataspace)
13231357
self.client.login(username=admin_user.username, password="secret")

dje/importers.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def pre_process_form(self, data, **kwargs):
154154
identifier_fields = model_class.get_identifier_fields()
155155
# Crafting the list of unique filters to match the instance
156156
for field_name in identifier_fields:
157-
value = data.get(prefix + field_name, None)
157+
value = data.get(prefix + field_name, "")
158158
if value:
159159
value = value.strip()
160160
filters.update({field_name: value})
@@ -288,6 +288,10 @@ def clean(self):
288288
raise forms.ValidationError("One of the row is a duplicate.")
289289

290290

291+
class ImportableUploadFileForm(forms.Form):
292+
file = SmartFileField(extensions=["csv"])
293+
294+
291295
class BaseImporter:
292296
"""
293297
Import in 3 steps:
@@ -309,7 +313,9 @@ class BaseImporter:
309313

310314
model_form = None
311315
formset_class = BaseImportModelFormSet
316+
upload_form_class = ImportableUploadFileForm
312317
add_to_product = False
318+
update_existing = False
313319

314320
def __init__(self, user, file_location=None, formset_data=None):
315321
if not self.model_form and not isinstance(self.model_form, BaseImportModelForm):
@@ -564,7 +570,7 @@ def get_key_sorted_fields(self):
564570
"""
565571
model_form_instance = self.model_form(dataspace=self.dataspace, user=self.user)
566572

567-
for _, field in model_form_instance.fields.items():
573+
for field in model_form_instance.fields.values():
568574
field.supported_values = self.get_supported_values(field)
569575

570576
return sorted(model_form_instance.fields.items())
@@ -582,14 +588,31 @@ def save_all(self):
582588
if not self.formset.is_valid(): # Just in case...
583589
return
584590

585-
self.results = {"added": [], "unmodified": []}
591+
self.results = {"added": [], "modified": [], "unmodified": []}
586592
for form in self.formset:
587-
if not form.instance.pk: # Save only addition for now
588-
saved_instance = form.save()
589-
self.results["added"].append(saved_instance)
590-
History.log_addition(self.user, saved_instance)
591-
else:
592-
self.results["unmodified"].append(form.instance)
593+
self.save_form(form)
594+
595+
def save_form(self, form):
596+
instance = form.instance
597+
598+
if not instance.pk:
599+
saved_instance = form.save()
600+
self.results["added"].append(saved_instance)
601+
History.log_addition(self.user, saved_instance)
602+
return
603+
604+
elif self.update_existing:
605+
# We need to refresh the instance from the db because form.instance has
606+
# the unsaved form.cleaned_data modification at that stage.
607+
instance.refresh_from_db()
608+
updated_fields = instance.update_from_data(self.user, form.cleaned_data, override=False)
609+
if updated_fields:
610+
self.results["modified"].append(instance)
611+
msg = f'Updated {", ".join(updated_fields)} from import'
612+
History.log_change(self.user, instance, message=msg)
613+
return
614+
615+
self.results["unmodified"].append(instance)
593616

594617
def get_added_instance_ids(self):
595618
"""Return the list of added instance ids."""
@@ -625,25 +648,11 @@ def get_add_to_product_form(self, request):
625648
return form
626649

627650

628-
class ImportableUploadFileForm(forms.Form):
629-
file = SmartFileField(extensions=["csv"])
630-
631-
632-
class PackageImportableUploadFileForm(forms.Form):
633-
file = SmartFileField(extensions=["csv", "json"])
634-
635-
@property
636-
def header(self):
637-
return "Select a <strong>CSV (.csv) or JSON (.json)</strong> file"
638-
639-
640651
@login_required()
641652
def import_view(request, importer_class):
642653
user = request.user
643654
importer = importer_class(user)
644-
upload_form_class = ImportableUploadFileForm
645-
if importer_class.__name__ == "PackageImporter":
646-
upload_form_class = PackageImportableUploadFileForm
655+
upload_form_class = importer.upload_form_class
647656

648657
opts = importer.model_form._meta.model._meta
649658
perm_codename = get_permission_codename("add", opts)

dje/templates/admin/object_import.html

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ <h1 class="header-title">
3030
<h5>Help for {{ importer.verbose_name|title }} Import:</h5>
3131
<ul class="mb-0">
3232
{% if importer.formset and not importer.fatal_errors %}
33-
<li><span class="badge text-bg-secondary">M</span> The {{ importer.verbose_name|title }} matches an already existing {{ importer.verbose_name|title }}. Nothing will be done for the row.</li>
33+
<li>
34+
<span class="badge text-bg-secondary">M</span>
35+
The {{ importer.verbose_name|title }} matches an already existing {{ importer.verbose_name|title }}.
36+
{% if importer.update_existing %}
37+
Empty fields will be updated with import data.
38+
{% else %}
39+
Nothing will be done for the row.
40+
{% endif %}
41+
</li>
3442
<li><span class="badge text-bg-success">A</span> The {{ importer.verbose_name|title }} will be added based on the input data</li>
3543
<li><span class="badge text-bg-danger">E</span> The row contains at least one error</li>
3644
<li><span class="badge text-bg-warning">W</span> The row is valid but should be manually reviewed</li>
@@ -41,11 +49,15 @@ <h5>Help for {{ importer.verbose_name|title }} Import:</h5>
4149
<li>The character encoding of the input file needs to be "UTF-8"</li>
4250
<li>The column order does not matter.</li>
4351
<li>The column names are case sensitive.</li>
44-
<li>Required column{{ importer.required_fields|pluralize }}: <strong>{{ importer.required_fields|join:", " }}</strong></li>
45-
<li>Supported values for boolean fields:<br>
46-
<strong>Yes:</strong> True, T, Yes, Y<br>
47-
<strong>No:</strong> False, F, No, N
52+
{% if importer.required_fields %}
53+
<li>Required column{{ importer.required_fields|pluralize }}: <strong>{{ importer.required_fields|join:", " }}</strong></li>
54+
{% endif %}
55+
<li>Supported values for boolean fields:
56+
<strong>Yes:</strong> True, T, Yes, Y; <strong>No:</strong> False, F, No, N
4857
</li>
58+
{% if importer.verbose_name == "package" %}
59+
<li>Note that a <strong>Package is uniquely defined in DejaCode by a combination of filename, download_url, and the six Package URL fields</strong> type, namespace, name, version, qualifiers, and subpath. You are not required to provide values in all of these fields (qualifiers and subpath are less commonly used) but if any of them are different from an existing similar package already in DejaCode, then the importer will perform an addition rather than a modification.</li>
60+
{% endif %}
4961
{% endif %}
5062
</ul>
5163
<div class="text-center">
@@ -265,7 +277,7 @@ <h3>Step 1: Select your file</h3>
265277
</div>
266278
{% endif %}
267279
<form enctype="multipart/form-data" method="post">{% csrf_token %}
268-
<p><input type="file" id="id_file" name="file"></p>
280+
<p>{{ file_form.file }}</p>
269281
<input class="btn btn-primary" type="submit" value="Upload" />
270282
</form>
271283
</div>

0 commit comments

Comments
 (0)