From 63af97b88800ce341bf2c4553b7e8e9aa3b833ff Mon Sep 17 00:00:00 2001 From: SamehOssama Date: Tue, 29 Apr 2025 03:57:57 +0300 Subject: [PATCH 1/4] Add base package list view and template with pagination Signed-off-by: SamehOssama --- packagedb/templates/package/package_list.html | 69 ++++++++++ packagedb/urls.py | 21 +++ packagedb/views.py | 120 ++++++++++++++++++ purldb_project/urls.py | 2 +- 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 packagedb/templates/package/package_list.html create mode 100644 packagedb/urls.py create mode 100644 packagedb/views.py diff --git a/packagedb/templates/package/package_list.html b/packagedb/templates/package/package_list.html new file mode 100644 index 00000000..1b4c66e8 --- /dev/null +++ b/packagedb/templates/package/package_list.html @@ -0,0 +1,69 @@ + + + + + + PurlDB + + + + + + + {% for column in columns_data %} + + {% endfor %} + + + + {% for package in object_list %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{{ column.label }}
+ {{ package.package_url }} + + {{ package.name }} + + {{ package.type }} + + {{ package.other_license_expression_spdx }} + + Download + + {{ package.created_date }} +
No packages found.
+ + + + + diff --git a/packagedb/urls.py b/packagedb/urls.py new file mode 100644 index 00000000..ca3d57b6 --- /dev/null +++ b/packagedb/urls.py @@ -0,0 +1,21 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# purldb is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/purldb for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from django.urls import path +from rest_framework.routers import DefaultRouter +from django.views.generic import RedirectView +from packagedb.views import PackageListView + +router = DefaultRouter() +router.register(r"packages", PackageListView, basename="package") + +urlpatterns = [ + path("", RedirectView.as_view(url="/packages")), + path("packages/", PackageListView.as_view(), name='package_list'), +] diff --git a/packagedb/views.py b/packagedb/views.py new file mode 100644 index 00000000..e4151569 --- /dev/null +++ b/packagedb/views.py @@ -0,0 +1,120 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# purldb is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/purldb for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from packagedb.models import Package +from django.views.generic import ListView + +PAGE_SIZE = 20 + +class TableColumnsMixin: + """ + table_columns = [ + "", + "", + { + "field_name": "", + "label": None, + "condition": None, + "sort_name": None, + "css_class": None, + }, + ] + """ + + table_columns = [] + + def get_columns_data(self): + """Return the columns data structure used in template rendering.""" + columns_data = [] + + sortable_fields = [] + active_sort = "" + filterset = getattr(self, "filterset", None) + if filterset and "sort" in filterset.filters: + sortable_fields = list(filterset.filters["sort"].param_map.keys()) + active_sort = filterset.data.get("sort", "") + + for column_definition in self.table_columns: + # Support for single "field_name" entry in columns list. + if not isinstance(column_definition, dict): + field_name = column_definition + column_data = {"field_name": field_name} + else: + field_name = column_definition.get("field_name") + column_data = column_definition.copy() + + condition = column_data.get("condition", None) + if condition is not None and not bool(condition): + continue + + if "label" not in column_data: + column_data["label"] = self.get_field_label(field_name) + + sort_name = column_data.get("sort_name") or field_name + if sort_name in sortable_fields: + is_sorted = sort_name == active_sort.lstrip("-") + + sort_direction = "" + if is_sorted and not active_sort.startswith("-"): + sort_direction = "-" + + column_data["is_sorted"] = is_sorted + column_data["sort_direction"] = sort_direction + query_dict = self.request.GET.copy() + query_dict["sort"] = f"{sort_direction}{sort_name}" + column_data["sort_query"] = query_dict.urlencode() + + if filter_fieldname := column_data.get("filter_fieldname"): + column_data["filter"] = filterset.form[filter_fieldname] + + columns_data.append(column_data) + + return columns_data + + @staticmethod + def get_field_label(field_name): + """Return a formatted label for display based on the `field_name`.""" + return field_name.replace("_", " ").capitalize().replace("url", "URL") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["columns_data"] = self.get_columns_data() + context["request_query_string"] = self.request.GET.urlencode() + return context + +class PackageListView( + TableColumnsMixin, + ListView +): + model = Package + paginate_by = PAGE_SIZE + template_name = "package/package_list.html" + + table_columns = [ + { + "field_name": "package_url", + "label": "Purl", + "sort_name": "projectmessages_count", + }, + "name", + "type", + { + "field_name": "other_license_expression_spdx", + "label": "License", + }, + { + "field_name": "download_url", + "label": "Download Url", + }, + { + "field_name": "created_date", + "label": "Created Date", + "sort_name": "created_date", + } + ] diff --git a/purldb_project/urls.py b/purldb_project/urls.py index 73bce815..76244551 100644 --- a/purldb_project/urls.py +++ b/purldb_project/urls.py @@ -57,7 +57,7 @@ path("api/", include((api_router.urls, "api"))), path("api/to_purl/", include((api_to_purl_router.urls, "api_to"))), path("api/from_purl/", include((api_from_purl_router.urls, "api_from"))), - path("", RedirectView.as_view(url="api/")), + path("", include("packagedb.urls")), path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path( "api/docs/", From 4bf9990c377a28e3797dc7e3e19cf6272d86012e Mon Sep 17 00:00:00 2001 From: SamehOssama Date: Tue, 29 Apr 2025 15:52:51 +0300 Subject: [PATCH 2/4] Add base html file for templates Signed-off-by: SamehOssama --- packagedb/templates/base.html | 15 +++ packagedb/templates/package/package_list.html | 120 ++++++++---------- 2 files changed, 71 insertions(+), 64 deletions(-) create mode 100644 packagedb/templates/base.html diff --git a/packagedb/templates/base.html b/packagedb/templates/base.html new file mode 100644 index 00000000..637aaae0 --- /dev/null +++ b/packagedb/templates/base.html @@ -0,0 +1,15 @@ + + + + + + {% block title %}PurlDB{% endblock %} + + {% block extrahead %}{% endblock %} + + + {% block content %}{% endblock %} + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/packagedb/templates/package/package_list.html b/packagedb/templates/package/package_list.html index 1b4c66e8..9d8e6f9b 100644 --- a/packagedb/templates/package/package_list.html +++ b/packagedb/templates/package/package_list.html @@ -1,69 +1,61 @@ - - - - - - PurlDB - - - - - +{% extends "base.html" %} +{% block content %} + +
+ + + {% for column in columns_data %} + + {% endfor %} + + + + {% for package in object_list %} - {% for column in columns_data %} - - {% endfor %} + + + + + + + + {% empty %} + + - - - {% for package in object_list %} - - - - - - - - - {% empty %} - - - - {% endfor %} - -
{{ column.label }}
{{ column.label }} + {{ package.package_url }} + + {{ package.name }} + + {{ package.type }} + + {{ package.other_license_expression_spdx }} + + Download + + {{ package.created_date }} +
No packages found.
- {{ package.package_url }} - - {{ package.name }} - - {{ package.type }} - - {{ package.other_license_expression_spdx }} - - Download - - {{ package.created_date }} -
No packages found.
- - +{% endblock %} From a44cfd1b11782d3e372ce4b1fd9daaad848f0778 Mon Sep 17 00:00:00 2001 From: SamehOssama Date: Tue, 29 Apr 2025 16:40:49 +0300 Subject: [PATCH 3/4] Add base package detail view and template Signed-off-by: SamehOssama --- .../templates/package/package_detail.html | 67 +++++++++++++++++++ packagedb/templates/package/package_list.html | 2 +- packagedb/urls.py | 2 + packagedb/views.py | 16 ++++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 packagedb/templates/package/package_detail.html diff --git a/packagedb/templates/package/package_detail.html b/packagedb/templates/package/package_detail.html new file mode 100644 index 00000000..782af6ee --- /dev/null +++ b/packagedb/templates/package/package_detail.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block content %} +

Package Details

+ +
    +
  • Package URL: {{ package.package_url }}
  • +
  • Filename: {{ package.filename }}
  • +
  • Download URL: {{ package.download_url }}
  • +
  • Size: {{ package.size }}
  • +
  • Release Date: {{ package.release_date }}
  • +
  • Primary language: {{ package.primary_language }}
  • +
  • Description: {{ package.description }}
  • +
  • Type: {{ package.type }}
  • +
  • Namespace: {{ package.namespace }}
  • +
  • Name: {{ package.name }}
  • +
  • Version: {{ package.version }}
  • +
  • Homepage URL: {{ package.homepage_url }}
  • +
  • SHA1: {{ package.sha1 }}
  • +
  • Declared license: {{ package.declared_license_expression_spdx }}
  • +
+ +

Dependencies

+{% if package.dependencies.all %} +
    + {% for dep in package.dependencies.all %} +
  • {{ dep }}
  • + {% endfor %} +
+{% else %} +

No dependencies listed.

+{% endif %} + +

Source Packages

+{% if package.source_packages.all %} +
    + {% for sp in package.source_packages.all %} +
  • + {{ sp.name }} + {% if sp.version %} — v{{ sp.version }}{% endif %} + {% if sp.package_url %}
    PURL: {{ sp.package_url }}{% endif %} +
  • + {% endfor %} +
+{% else %} +

No source packages linked.

+{% endif %} + +

Parties

+{% if package.parties.all %} +
    + {% for party in package.parties.all %} +
  • + Type: {{ party.type }}
    + Role: {{ party.role }}
    + Name: {{ party.name }}
    + Email: {{ party.email }}
    + {% if party.url %} + URL: {{ party.url }} + {% endif %} +
  • + {% endfor %} +
+{% else %} +

No parties listed.

+{% endif %} +{% endblock %} diff --git a/packagedb/templates/package/package_list.html b/packagedb/templates/package/package_list.html index 9d8e6f9b..9de2faec 100644 --- a/packagedb/templates/package/package_list.html +++ b/packagedb/templates/package/package_list.html @@ -13,7 +13,7 @@ {% for package in object_list %} - {{ package.package_url }} + {{ package.package_url }} {{ package.name }} diff --git a/packagedb/urls.py b/packagedb/urls.py index ca3d57b6..5a84bbeb 100644 --- a/packagedb/urls.py +++ b/packagedb/urls.py @@ -11,6 +11,7 @@ from rest_framework.routers import DefaultRouter from django.views.generic import RedirectView from packagedb.views import PackageListView +from packagedb.views import PackageDetailView router = DefaultRouter() router.register(r"packages", PackageListView, basename="package") @@ -18,4 +19,5 @@ urlpatterns = [ path("", RedirectView.as_view(url="/packages")), path("packages/", PackageListView.as_view(), name='package_list'), + path("packages//", PackageDetailView.as_view(), name='package_detail'), ] diff --git a/packagedb/views.py b/packagedb/views.py index e4151569..42565e65 100644 --- a/packagedb/views.py +++ b/packagedb/views.py @@ -8,7 +8,9 @@ # from packagedb.models import Package + from django.views.generic import ListView +from django.views.generic.detail import DetailView PAGE_SIZE = 20 @@ -95,7 +97,6 @@ class PackageListView( model = Package paginate_by = PAGE_SIZE template_name = "package/package_list.html" - table_columns = [ { "field_name": "package_url", @@ -118,3 +119,16 @@ class PackageListView( "sort_name": "created_date", } ] + +class PackageDetailView(DetailView): + model = Package + slug_field = "uuid" + slug_url_kwarg = "uuid" + template_name = "package/package_detail.html" + + def get_queryset(self): + return ( + super() + .get_queryset() + .prefetch_related("parties", "dependencies") + ) From fd7f109fd6f7d52ab01d4d9561090a1d7a0fabea Mon Sep 17 00:00:00 2001 From: SamehOssama Date: Tue, 29 Apr 2025 17:00:23 +0300 Subject: [PATCH 4/4] Add filters and sorting using the api's filter set Signed-off-by: SamehOssama --- packagedb/templates/package/package_list.html | 10 ++++++++++ packagedb/views.py | 6 ++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packagedb/templates/package/package_list.html b/packagedb/templates/package/package_list.html index 9de2faec..ab0ca158 100644 --- a/packagedb/templates/package/package_list.html +++ b/packagedb/templates/package/package_list.html @@ -1,6 +1,16 @@ {% extends "base.html" %} {% block content %} +

Package List

+ + +

Package Filters

+
+ {{ filter.form.as_p }} + +
+ +

Packages

diff --git a/packagedb/views.py b/packagedb/views.py index 42565e65..86d0527c 100644 --- a/packagedb/views.py +++ b/packagedb/views.py @@ -7,10 +7,11 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +from packagedb.api import PackageFilterSet from packagedb.models import Package -from django.views.generic import ListView from django.views.generic.detail import DetailView +from django_filters.views import FilterView PAGE_SIZE = 20 @@ -92,9 +93,10 @@ def get_context_data(self, **kwargs): class PackageListView( TableColumnsMixin, - ListView + FilterView ): model = Package + filterset_class = PackageFilterSet paginate_by = PAGE_SIZE template_name = "package/package_list.html" table_columns = [