From 1aa4721b008e40b6fbc138fce2a58a50c8b5562a Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 15 Feb 2024 16:26:36 +0100 Subject: [PATCH 1/6] Refactor parts of ReportDetailsView #10 Signed-off-by: tdruez --- .../includes/report_results_table.html | 2 +- reporting/templates/reporting/report_run.html | 2 +- reporting/views.py | 20 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/reporting/templates/reporting/includes/report_results_table.html b/reporting/templates/reporting/includes/report_results_table.html index 41d1da8f..2f4ce47c 100644 --- a/reporting/templates/reporting/includes/report_results_table.html +++ b/reporting/templates/reporting/includes/report_results_table.html @@ -1,4 +1,4 @@ - +
{% for header in headers %} diff --git a/reporting/templates/reporting/report_run.html b/reporting/templates/reporting/report_run.html index 11eb731b..b852f533 100644 --- a/reporting/templates/reporting/report_run.html +++ b/reporting/templates/reporting/report_run.html @@ -86,7 +86,7 @@

{% endif %}
-

+
diff --git a/reporting/views.py b/reporting/views.py index 11918020..c2ddbba2 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -26,6 +26,7 @@ from django.views.generic.list import MultipleObjectMixin import pyaml +import xlsxwriter from dje.utils import get_preserved_filters from dje.views import AdminLinksDropDownMixin @@ -192,6 +193,12 @@ def get_context_data(self, **kwargs): model_class = report.column_template.get_model_class() # Only available in the UI since the link is relative to the current URL include_view_link = hasattr(model_class, "get_absolute_url") and not self.format + interpolated_report_context = self.get_interpolated_report_context(request, report) + output = report.get_output( + queryset=context["object_list"], + user=request.user, + include_view_link=include_view_link, + ) context.update( { @@ -200,16 +207,9 @@ def get_context_data(self, **kwargs): "headers": report.column_template.as_headers(include_view_link=include_view_link), "runtime_filter_formset": self.runtime_filter_formset, "query_string_params": self.get_query_string_params(), - "interpolated_report_context": self.get_interpolated_report_context( - request, report - ), + "interpolated_report_context": interpolated_report_context, "errors": getattr(self, "errors", []), - "include_view_link": include_view_link, - "output": report.get_output( - queryset=context["object_list"], - user=request.user, - include_view_link=include_view_link, - ), + "output": output, } ) @@ -240,8 +240,6 @@ def get_yaml_response(self, **response_kwargs): def get_xlsx_response(self, **response_kwargs): """Return the results as `xlsx` format.""" - import xlsxwriter - context = self.get_context_data(**self.kwargs) report_data = [context["headers"]] + context["output"] From 27b96cee2a55f209a4ee934d4394c55fa90a6722 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 15 Feb 2024 16:33:56 +0100 Subject: [PATCH 2/6] Replace pyaml by saneyaml for report output #10 Signed-off-by: tdruez --- reporting/views.py | 18 +++++------------- setup.cfg | 1 - .../dist/pyaml-21.10.1-py2.py3-none-any.whl | Bin 24857 -> 0 bytes .../pyaml-21.10.1-py2.py3-none-any.whl.ABOUT | 16 ---------------- .../pyaml-21.10.1-py2.py3-none-any.whl.NOTICE | 1 - 5 files changed, 5 insertions(+), 31 deletions(-) delete mode 100644 thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl delete mode 100644 thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.ABOUT delete mode 100644 thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.NOTICE diff --git a/reporting/views.py b/reporting/views.py index c2ddbba2..291f1391 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -9,7 +9,6 @@ import datetime import io import json -from collections import OrderedDict from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import FieldDoesNotExist @@ -25,7 +24,7 @@ from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import MultipleObjectMixin -import pyaml +import saneyaml import xlsxwriter from dje.utils import get_preserved_filters @@ -216,13 +215,9 @@ def get_context_data(self, **kwargs): return context def get_dump(self, dumper, **dumper_kwargs): - """ - Return the data dump using provided kwargs. - The columns order form the ColumnTemplateAssignedField sequence is respected - using an OrderedDict. - """ + """Return the data dump using provided kwargs.""" context = self.get_context_data(**self.kwargs) - data = [OrderedDict(zip(context["headers"], values)) for values in context["output"]] + data = [dict(zip(context["headers"], values)) for values in context["output"]] return dumper(data, **dumper_kwargs) def get_json_response(self, **response_kwargs): @@ -231,11 +226,8 @@ def get_json_response(self, **response_kwargs): return HttpResponse(dump, **response_kwargs) def get_yaml_response(self, **response_kwargs): - """ - Return serialized results as yaml content response. - Using pretty-yaml (pyaml) on top of PyYAML for the OrderDict support with safe_dump. - """ - dump = self.get_dump(pyaml.dump, safe=True) + """Return serialized results as yaml content response.""" + dump = self.get_dump(saneyaml.dump) return HttpResponse(dump, **response_kwargs) def get_xlsx_response(self, **response_kwargs): diff --git a/setup.cfg b/setup.cfg index 5ab5c1ef..c51b5c24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -116,7 +116,6 @@ install_requires = charset-normalizer==3.1.0 PyYAML==6.0 Cython==0.29.30 - pyaml==21.10.1 importlib_metadata==4.11.4 zipp==3.8.0 XlsxWriter==3.1.9 diff --git a/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl b/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl deleted file mode 100644 index e301223b9087ea08c227b079c57eec29b6f773e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24857 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 diff --git a/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index d02b1e49..00000000 --- a/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,16 +0,0 @@ -about_resource: pyaml-21.10.1-py2.py3-none-any.whl -name: pyaml -version: 21.10.1 -download_url: https://files.pythonhosted.org/packages/35/fd/78a3a11c7b9b11878ebbf4461a09cbc758bdfc1b45168972727f7334b09a/pyaml-21.10.1-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/pyaml/21.10.1/ -package_url: pkg:pypi/pyaml@21.10.1 -license_expression: wtfpl-2.0 -copyright: Copyright (c) Mike Kazantsev -notice_file: pyaml-21.10.1-py2.py3-none-any.whl.NOTICE -owner: Mike Kazantsev -checksum_md5: a99385e1e3457a3c00867283f0ca9d7c -checksum_sha1: 7cb2074609c542fe4ea2dd3bbc84e28432640168 -licenses: - - key: wtfpl-2.0 - name: WTFPL 2.0 - file: wtfpl-2.0.LICENSE diff --git a/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index 009a76db..00000000 --- a/thirdparty/dist/pyaml-21.10.1-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1 +0,0 @@ -License: WTFPL \ No newline at end of file From dae90a6f1f42dc7b95b661a9f7dd9850badff91b Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 15 Feb 2024 16:45:50 +0100 Subject: [PATCH 3/6] Fix the rendering issue of the absolute url link #10 Signed-off-by: tdruez --- reporting/models.py | 16 +++++++++------- .../reporting/includes/report_results_table.html | 3 +-- reporting/views.py | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/reporting/models.py b/reporting/models.py index bef19d55..763e3ad2 100644 --- a/reporting/models.py +++ b/reporting/models.py @@ -754,16 +754,18 @@ def get_output(self, queryset=None, user=None, include_view_link=False): icon = format_html('') rows = [] + for instance in queryset: - cells = [ - field.get_value_for_instance(instance, user=user) - for field in self.column_template.fields.all() - ] + cells = [] if include_view_link: - cells.insert( - 0, instance.get_absolute_link(value=icon, title="View", target="_blank") - ) + view_link = instance.get_absolute_link(value=icon, title="View", target="_blank") + cells.append(view_link) + + for field in self.column_template.fields.all(): + cells.append(field.get_value_for_instance(instance, user=user)) + rows.append(cells) + return rows diff --git a/reporting/templates/reporting/includes/report_results_table.html b/reporting/templates/reporting/includes/report_results_table.html index 2f4ce47c..ef124dc6 100644 --- a/reporting/templates/reporting/includes/report_results_table.html +++ b/reporting/templates/reporting/includes/report_results_table.html @@ -12,9 +12,8 @@ {% for item in list %} {% if format == 'xls' %} - {% elif format == 'html' or not format %} - {% else %} + {# WARNING: Using |urlize breaks the "view_link" #} {% endif %} {% endfor %} diff --git a/reporting/views.py b/reporting/views.py index 291f1391..ea9f28ff 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -191,7 +191,7 @@ def get_context_data(self, **kwargs): report = self.object model_class = report.column_template.get_model_class() # Only available in the UI since the link is relative to the current URL - include_view_link = hasattr(model_class, "get_absolute_url") and not self.format + include_view_link = not self.format and hasattr(model_class, "get_absolute_url") interpolated_report_context = self.get_interpolated_report_context(request, report) output = report.get_output( queryset=context["object_list"], @@ -203,7 +203,7 @@ def get_context_data(self, **kwargs): { "opts": self.model._meta, "preserved_filters": get_preserved_filters(request, self.model), - "headers": report.column_template.as_headers(include_view_link=include_view_link), + "headers": report.column_template.as_headers(include_view_link), "runtime_filter_formset": self.runtime_filter_formset, "query_string_params": self.get_query_string_params(), "interpolated_report_context": interpolated_report_context, From 846b513619846d801368618e184df13918c1fa35 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 15 Feb 2024 16:51:07 +0100 Subject: [PATCH 4/6] Move the join logic in the get_output method #10 Signed-off-by: tdruez --- reporting/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reporting/models.py b/reporting/models.py index 763e3ad2..fb508cbc 100644 --- a/reporting/models.py +++ b/reporting/models.py @@ -674,8 +674,7 @@ def get_value_for_instance(self, instance, user=None): for field_name in self.field_name.split("__"): objects = self._get_objects_for_field_name(objects, field_name, user) - results = [str(val) for val in objects if not (len(objects) < 2 and val is None)] - return MULTIVALUE_SEPARATOR.join(results) + return [str(val) for val in objects if not (len(objects) < 2 and val is None)] class ReportQuerySet(DataspacedQuerySet): @@ -762,7 +761,8 @@ def get_output(self, queryset=None, user=None, include_view_link=False): cells.append(view_link) for field in self.column_template.fields.all(): - cells.append(field.get_value_for_instance(instance, user=user)) + value = field.get_value_for_instance(instance, user=user) + cells.append(MULTIVALUE_SEPARATOR.join(value)) rows.append(cells) From 88fcdf9e9697eb57ab5af15bdd05667ed85b2a9e Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 15 Feb 2024 17:50:51 +0100 Subject: [PATCH 5/6] Improve the presentation of "multi-values" in DejaCode Reports #10 Signed-off-by: tdruez --- reporting/models.py | 21 +++++++++++++++++---- reporting/views.py | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/reporting/models.py b/reporting/models.py index fb508cbc..4f8c1ea9 100644 --- a/reporting/models.py +++ b/reporting/models.py @@ -68,7 +68,7 @@ LICENSE_TAG_PREFIX = "tag: " -MULTIVALUE_SEPARATOR = ", " +MULTIVALUE_SEPARATOR = "\n" ERROR_STR = "Error" @@ -674,7 +674,12 @@ def get_value_for_instance(self, instance, user=None): for field_name in self.field_name.split("__"): objects = self._get_objects_for_field_name(objects, field_name, user) - return [str(val) for val in objects if not (len(objects) < 2 and val is None)] + return [ + # The .strip() ensure the SafeString types are casted to regular str + str(val).strip() + for val in objects + if not (len(objects) < 2 and val is None) + ] class ReportQuerySet(DataspacedQuerySet): @@ -746,7 +751,7 @@ def save(self, *args, **kwargs): def get_absolute_url(self): return reverse("reporting:report_details", args=[self.uuid]) - def get_output(self, queryset=None, user=None, include_view_link=False): + def get_output(self, queryset=None, user=None, include_view_link=False, multi_as_list=False): # Checking if the parameter is given rather than the boolean value of the QuerySet if queryset is None: queryset = self.query.get_qs(user=user) @@ -762,7 +767,15 @@ def get_output(self, queryset=None, user=None, include_view_link=False): for field in self.column_template.fields.all(): value = field.get_value_for_instance(instance, user=user) - cells.append(MULTIVALUE_SEPARATOR.join(value)) + if not multi_as_list: + cells.append(MULTIVALUE_SEPARATOR.join(value)) + else: + if value == []: + cells.append("") + elif len(value) > 1: + cells.append(value) + else: + cells.append(value[0]) rows.append(cells) diff --git a/reporting/views.py b/reporting/views.py index ea9f28ff..a64857e4 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -193,10 +193,12 @@ def get_context_data(self, **kwargs): # Only available in the UI since the link is relative to the current URL include_view_link = not self.format and hasattr(model_class, "get_absolute_url") interpolated_report_context = self.get_interpolated_report_context(request, report) + multi_as_list = True if self.format in ["json", "yaml"] else False output = report.get_output( queryset=context["object_list"], user=request.user, include_view_link=include_view_link, + multi_as_list=multi_as_list, ) context.update( From 8d8ee9f28c61bb2b39d844b07ac2229375a353b3 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 16 Feb 2024 17:15:41 +0100 Subject: [PATCH 6/6] Fix unit tests #10 Signed-off-by: tdruez --- reporting/models.py | 28 +++++++++++++++++++--------- reporting/tests/test_models.py | 22 +++++++++++----------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/reporting/models.py b/reporting/models.py index 4f8c1ea9..b2e3a600 100644 --- a/reporting/models.py +++ b/reporting/models.py @@ -662,10 +662,12 @@ def _get_objects_for_field_name(self, objects, field_name, user): result.extend(value if isinstance(value, list) else [value]) return result - def get_value_for_instance(self, instance, user=None): + def get_value_for_instance(self, instance, user=None, multi_as_list=False): """ Return the value to be display in the row, given an instance and the current self.field_name value of this assigned field. + When `multi_as_list` is enabled, return the results as a list instead of + joining on `MULTIVALUE_SEPARATOR`. """ if self.get_model_class() != instance.__class__: raise AssertionError("content types do not match") @@ -674,13 +676,18 @@ def get_value_for_instance(self, instance, user=None): for field_name in self.field_name.split("__"): objects = self._get_objects_for_field_name(objects, field_name, user) - return [ + results = [ # The .strip() ensure the SafeString types are casted to regular str str(val).strip() for val in objects if not (len(objects) < 2 and val is None) ] + if multi_as_list: + return results + else: + return MULTIVALUE_SEPARATOR.join(results) + class ReportQuerySet(DataspacedQuerySet): def user_availables(self): @@ -766,16 +773,19 @@ def get_output(self, queryset=None, user=None, include_view_link=False, multi_as cells.append(view_link) for field in self.column_template.fields.all(): - value = field.get_value_for_instance(instance, user=user) - if not multi_as_list: - cells.append(MULTIVALUE_SEPARATOR.join(value)) - else: + value = field.get_value_for_instance(instance, user, multi_as_list) + + if type(value) is list: if value == []: - cells.append("") + cell_value = "" elif len(value) > 1: - cells.append(value) + cell_value = value else: - cells.append(value[0]) + cell_value = value[0] + else: + cell_value = value + + cells.append(cell_value) rows.append(cells) diff --git a/reporting/tests/test_models.py b/reporting/tests/test_models.py index 138017ed..0afbd218 100644 --- a/reporting/tests/test_models.py +++ b/reporting/tests/test_models.py @@ -1196,12 +1196,12 @@ def test_get_value_for_instance_on_license_on_various_fields(self): ("owner__children__children", ""), ("owner__children__children__name", ""), # Many2Many - ("tags", "{}, {}".format(self.license_tag, license_tag2)), - ("tags__id", "{}, {}".format(self.license_tag.id, license_tag2.id)), - ("tags__show_in_license_list_view", "False, False"), - ("tags__default_value", "None, True"), - ("tags__label", "Network Redistribution, Tag2"), - ("tags__uuid", "{}, {}".format(self.license_tag.uuid, license_tag2.uuid)), + ("tags", "{}\n{}".format(self.license_tag, license_tag2)), + ("tags__id", "{}\n{}".format(self.license_tag.id, license_tag2.id)), + ("tags__show_in_license_list_view", "False\nFalse"), + ("tags__default_value", "None\nTrue"), + ("tags__label", "Network Redistribution\nTag2"), + ("tags__uuid", "{}\n{}".format(self.license_tag.uuid, license_tag2.uuid)), # Special tag property ("{}{}".format(LICENSE_TAG_PREFIX, self.license_tag.label), "True"), # Related @@ -1342,7 +1342,7 @@ def test_get_value_for_instance_on_component_on_various_fields(self): ("code_view_url", ""), ("bug_tracking_url", ""), ("primary_language", ""), - ("keywords", "Keyword1, Keyword2"), + ("keywords", "Keyword1\nKeyword2"), ("guidance", ""), ("admin_notes", ""), ("is_active", "True"), @@ -1694,9 +1694,9 @@ def test_get_value_for_instance_on_component_for_licenses_tags(self): component=self.component, license=license2, dataspace=self.dataspace ) - expected = "True, False" + expected = "True\nFalse" self.assertEqual(expected, self.field1.get_value_for_instance(self.component)) - expected = "License1, License2" + expected = "License1\nLicense2" self.assertEqual(expected, field2.get_value_for_instance(self.component)) def test_get_value_for_instance_multi_value_ordering(self): @@ -1735,9 +1735,9 @@ def test_get_value_for_instance_multi_value_ordering(self): expected = [license_a.key, license_b.name] self.assertEqual(expected, list(self.component.licenses.values_list("key", flat=True))) - expected = ", ".join([license_a.key, license_b.name]) + expected = "\n".join([license_a.key, license_b.name]) self.assertEqual(expected, field2.get_value_for_instance(self.component)) - expected = ", ".join([str(assigned_tag_a.value), str(assigned_tag_b.value)]) + expected = "\n".join([str(assigned_tag_a.value), str(assigned_tag_b.value)]) self.assertEqual(expected, self.field1.get_value_for_instance(self.component)) def test_column_template_with_fields_copy(self):
Field{{ item|linebreaksbr }}{{ item|default:' '|urlize|linebreaksbr }}{{ item|default:' '|linebreaksbr }}