-
Notifications
You must be signed in to change notification settings - Fork 23
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
base: main
Are you sure you want to change the base?
Changes from all commits
e738b0e
fc20a4f
e680f5e
7cbcc75
a4d18ca
f5451a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/elasticsearch@7.17.9?classifier=sources" | ||
}, | ||
), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
@@ -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.""" | ||
|
@@ -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. | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would use a variable name that describes what's in |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"), | ||
|
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> |
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> |
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">…</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">…</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> |
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 %} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 intomodels.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 thePurlValidateResponseSerializer
we import frompackagedb.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 frompackagedb/models
, so that's the source of the circular import issue.There was a problem hiding this comment.
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:
purl-validate UI in models.py: