Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create public purl-validate UI #535

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Welcome to PurlDB documentation!
=========================================
Welcome to the PurlDB documentation!
====================================


PurlDB aka. ``Package URL Database`` is a database of software package metadata keyed by Package-URL
or purl that offers information and indentication services about software packages.
`PurlDB <https://github.com/aboutcode-org/purldb>`__ aka. ``Package URL Database`` is a database of software package metadata keyed by a `Package-URL
or purl <https://github.com/package-url>`__ that offers information and indentication services about software packages.

A purl or Package-URL is an attempt to standardize existing approaches to reliably identify and
locate software packages in general and Free and Open Source Software (FOSS) packages in
Expand All @@ -30,8 +30,8 @@ This what PurlDB is all about and it offers:
software supply chain.


What can you do PurlDB?
------------------------
What can you do with PurlDB?
----------------------------

- Build a comprehensive open source software packages knowledge base. This includes the extensive
scan of package code for origin, dependencies, embedded packages and licenses.
Expand Down
21 changes: 21 additions & 0 deletions packagedb/forms.py
Original file line number Diff line number Diff line change
@@ -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 import forms


class PackageSearchForm(forms.Form):
search = forms.CharField(
required=True,
widget=forms.TextInput(
attrs={
"placeholder": "pkg:maven/org.elasticsearch/[email protected]?classifier=sources"
},
),
)
105 changes: 105 additions & 0 deletions packagedb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
import uuid
from collections import OrderedDict
from urllib.parse import urlencode

from django.conf import settings
from django.contrib.auth.models import UserManager
Expand All @@ -34,6 +35,9 @@
from packageurl.contrib.django.models import PackageURLMixin
from packageurl.contrib.django.models import PackageURLQuerySetMixin
from rest_framework.authtoken.models import Token
from rest_framework.serializers import BooleanField
from rest_framework.serializers import CharField
from rest_framework.serializers import Serializer

from packagedb import schedules

Expand All @@ -43,6 +47,8 @@
logging.basicConfig(stream=sys.stdout)
logger.setLevel(logging.INFO)

print_to_console = False


def sort_version(packages):
"""Return the packages sorted by version."""
Expand Down Expand Up @@ -81,6 +87,105 @@ def paginated(self, per_page=5000):
page = paginator.page(page_number)
yield from page.object_list

# Based on class PurlValidateResponseSerializer(Serializer). Added here because when added to serializers.py and imported raises a circular import error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that the circular import happens when you add this code to serializers.py and then try to import it from somewhere like views.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonoYang Glad you asked about the custom serializer I created inside models.py.

This means that when I initially defined that new serializer in packagedb/serializers.py and tried to import it into models.py, where I am using the local one now, I got the circular import error. Maybe I don't need a serializer here?

I modeled my approach on what we do in the api.py PurlValidateViewSet() class with the PurlValidateResponseSerializer we import from packagedb.serializers. Essentially I'm trying to mimic in my purl-validate UI the dictionary that our validate endpoint returns -- 4 fields, and I added one or two more for my UI validation messaging (preventing an exception from being raised and interrupting the process). packagedb/serializers.py has numerous imports from packagedb/models, so that's the source of the circular import issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonoYang You can see the similar code structure I used and the validate endpoint used to handle an exception:

validate endpoint in api.py:
image

purl-validate UI in models.py:
image

class PurlValidateErrorSerializer(Serializer):
valid = BooleanField()
exists = BooleanField(required=False)
message = CharField()
purl = CharField()
error_details = CharField()

def search(self, query: str = None):
"""
Return a Package queryset searching for the ``query``. A version is
required. Returns an exact match if the record(s) exist(s), otherwise
no match.
"""
query = query and query.strip()
if not query:
return self.none()
qs = self

message_not_valid = "The provided PackageURL is not valid."
response = {}
response["exists"] = None
response["purl"] = query
response["valid"] = False
response["message"] = message_not_valid
response["error_details"] = None

try:
package_url = PackageURL.from_string(query)
except ValueError as e:
if print_to_console:
print(f"\nInput: {query}")
print(f"\nValueError: {e}")
response["error_details"] = e
serializer = self.PurlValidateErrorSerializer(response)
return serializer.data

qs = qs.filter(
models.Q(namespace=package_url.namespace)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move these if-else statements into their own variables for clarity.

if package_url.namespace
else (models.Q(namespace="")),
models.Q(subpath=package_url.subpath)
if package_url.subpath
else (models.Q(subpath="")),
type=package_url.type,
name=package_url.name,
version=package_url.version,
qualifiers=urlencode(package_url.qualifiers),
)

if print_to_console:
print(f"\nmodels.py PackageQuerySet search() qs.query = {qs.query}")
print(f"\nlist(qs): {list(qs)}")
for abc in list(qs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a variable name that describes what's in qs.

print(f"- abc = {abc}")
print(f"- abc.type = {abc.type}")
print(f"- abc.namespace = {abc.namespace}")
print(f"- abc.name = {abc.name}")
print(f"- abc.version = {abc.version}")
print(f"- abc.qualifiers = {abc.qualifiers}")
print(f"- abc.subpath = {abc.subpath}")

return qs

def get_packageurl_from_string(self, query: str = None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method being used anywhere?

"""Vet with packageurl-python __init__.py from_string()."""
query = query and query.strip()
if not query:
return self.none()

try:
packageurl_from_string = PackageURL.from_string(query)
if print_to_console:
print(
f"\n- models.py PackageQuerySet get_packageurl_from_string() query = {packageurl_from_string}"
)
print(f"\npackageurl_from_string.type = {packageurl_from_string.type}")
print(
f"- packageurl_from_string.namespace = {packageurl_from_string.namespace}"
)
print(f"- packageurl_from_string.name = {packageurl_from_string.name}")
print(
f"- packageurl_from_string.version = {packageurl_from_string.version}"
)
print(
f"- packageurl_from_string.qualifiers = {packageurl_from_string.qualifiers}"
)
print(
f"- packageurl_from_string.subpath = {packageurl_from_string.subpath}"
)

return packageurl_from_string

except ValueError as e:
if print_to_console:
print(f"\nInput: {query}")
print(f"\nValueError: {e}")
return None


VCS_CHOICES = [
("git", "git"),
Expand Down
132 changes: 132 additions & 0 deletions packagedb/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}PurlDB.io{% endblock %}</title>
<link rel="icon" href="{% static 'images/favicon.ico' %}" />

<link rel="stylesheet" href="{% static 'css/bulma.css' %}" />
<link rel="stylesheet" href="{% static 'css/custom.css' %}" />
<link rel="stylesheet" href="{% static 'css/font-awesome.css' %}" />
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.css' %}" />

<script src="{% static 'js/clipboard-2.0.0.min.js' %}" integrity="sha384-5tfO0soa+FisnuBhaHP2VmPXQG/JZ8dLcRL43IkJFzbsXTXT6zIX8q8sIT0VSe2G" crossorigin="anonymous"></script>

{% block extrahead %}{% endblock %}
</head>
<body class="Site">
<div class="container is-max-widescreen">
{% include "navbar.html" %}
{% block content %}{% endblock %}
{% include "footer.html" %}
</div>

{% block scripts %}
{% endblock %}

{% block javascript %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Set initial data-tooltip value
document.querySelectorAll('.btn-clipboard').forEach(button => {
console.info('span:', button.querySelector('span'));
const span = button.querySelector('span');
if (span && !span.getAttribute('data-tooltip')) {
span.setAttribute('data-tooltip', 'Copy to clipboard');
span.setAttribute('class', 'tooltip-narrow');
}
});

// clipboard.js
let clip = new ClipboardJS(".btn-clipboard", {
target: function (trigger) {
return trigger.nextElementSibling
}
});

clip.on("success", function (clip) {
const button = clip.trigger;
const span = button.querySelector('span');
span.setAttribute('data-tooltip', 'Copied!');
span.setAttribute('class', 'tooltip-narrow-success');
span.classList.add('has-tooltip-active');
button.classList.add('tooltip-visible');
clip.clearSelection()
});

clip.on("error", function (e) {
console.error('Failed to copy: ', e);
const span = e.trigger.querySelector('span');
span.setAttribute('data-tooltip', 'Failed to copy');
});

// Display button when mouse enters the copyable area.
document.querySelectorAll('.btn-clipboard').forEach(button => {
const span = button.querySelector('span');
if (span) {
span.setAttribute('data-original-tooltip', span.getAttribute('data-tooltip'));
button.nextElementSibling.addEventListener('mouseenter', () => {
button.style.display = '';
});
}
});

//Adjust tooltip position when close to page left/right border.
document.querySelectorAll('.has-tooltip-multiline').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.left < 100) {
el.classList.add('has-tooltip-right');
} else if (rect.right > window.innerWidth - 100) {
el.classList.add('has-tooltip-left');
}
});


//<!-- Tooltips that can contain clickable links. -->
const tooltipTriggers = document.querySelectorAll('.tooltip-trigger');

function hideTooltipWithDelay(tooltipContent) {
setTimeout(() => {
if (!tooltipContent.matches(':hover') && !tooltipContent.previousElementSibling.matches(':hover')) {
tooltipContent.style.display = 'none';
}
}, 200); // Delay to allow the user to hover over the tooltip
}

tooltipTriggers.forEach(trigger => {
trigger.addEventListener('mouseenter', function() {
const tooltipContent = this.nextElementSibling;

if (tooltipContent) {
tooltipContent.style.display = 'block';
}
});

trigger.addEventListener('mouseleave', function() {
const tooltipContent = this.nextElementSibling;

if (tooltipContent) {
hideTooltipWithDelay(tooltipContent);
}
});
});

// Keep the tooltip visible when the user hovers over the tooltip itself
document.querySelectorAll('.tooltip-content').forEach(content => {
content.addEventListener('mouseenter', function() {
this.style.display = 'block';
});

content.addEventListener('mouseleave', function() {
hideTooltipWithDelay(this);
});
});

});
</script>

{% endblock %}
</body>
</html>
9 changes: 9 additions & 0 deletions packagedb/templates/footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>PurlDB</strong> is free software by <a href="https://www.aboutcode.org/">https://www.aboutcode.org/</a> and others</a> |
Source code license: <a href="https://github.com/aboutcode-org/purldb/blob/main/apache-2.0.LICENSE">Apache-2.0</a> |
Data license: <a href="https://github.com/aboutcode-org/purldb/blob/main/cc-by-sa-4.0.LICENSE">CC-BY-SA-4.0</a> | <span style="color: #ff0000;">Terms of Service</span>
</p>
</div>
</footer>
39 changes: 39 additions & 0 deletions packagedb/templates/includes/pagination.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<nav class="pagination is-centered is-small" aria-label="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&search={{ search|urlencode }}" class="pagination-previous">Previous</a>
{% else %}
<a class="pagination-previous" disabled>Previous</a>
{% endif %}

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search={{ search|urlencode }}" class="pagination-next">Next</a>
{% else %}
<a class="pagination-next" disabled>Next</a>
{% endif %}

<ul class="pagination-list">
{% if page_obj.number != 1%}
<li>
<a href="?page=1&search={{ search|urlencode }}" class="pagination-link" aria-label="Goto page 1">1</a>
</li>
{% if page_obj.number > 2 %}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{% endif %}
{% endif %}
<li>
<a class="pagination-link is-current" aria-label="Page {{ page_obj.number }}" aria-current="page">{{ page_obj.number }}</a>
</li>
{% if page_obj.number != page_obj.paginator.num_pages %}
{% if page_obj.next_page_number != page_obj.paginator.num_pages %}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{% endif %}
<li>
<a href="?page={{ page_obj.paginator.num_pages }}&search={{ search|urlencode }}" class="pagination-link" aria-label="Goto page {{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
</li>
{% endif %}
</ul>
</nav>
14 changes: 14 additions & 0 deletions packagedb/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load widget_tweaks %}

{% block title %}
PurlDB Home
{% endblock %}

{% block content %}
<section class="section pt-0">
{% include "validate_purl_input.html" %}
</section>

<div></div>
{% endblock %}
Loading