Skip to content

Commit 97a890b

Browse files
authored
Add administration site for main scanpipe models (#1323)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 585979d commit 97a890b

File tree

12 files changed

+382
-1
lines changed

12 files changed

+382
-1
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ v34.7.1 (unreleased)
1818
The `package_uid` is now included in each BOM Component as a property.
1919
https://github.com/nexB/scancode.io/issues/1316
2020

21+
- Add administration interface. Can be enabled with the SCANCODEIO_ENABLE_ADMIN_SITE
22+
setting.
23+
Add ``--admin`` and ``--super`` options to the ``create-user`` management command.
24+
https://github.com/nexB/scancode.io/pull/1323
25+
2126
- Add ``results_url`` and ``summary_url`` on the API ProjectSerializer.
2227
https://github.com/nexB/scancode.io/issues/1325
2328

docs/command-line-interface.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ API key.
351351
Optional arguments:
352352

353353
- ``--no-input`` Does not prompt the user for input of any kind.
354+
- ``--admin`` Specifies that the user should be created as an admin user.
355+
- ``--super`` Specifies that the user should be created as a superuser.
354356

355357
.. _cli_run:
356358

scancodeio/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
"SCANCODEIO_REQUIRE_AUTHENTICATION", default=False
5757
)
5858

59+
SCANCODEIO_ENABLE_ADMIN_SITE = env.bool("SCANCODEIO_ENABLE_ADMIN_SITE", default=False)
60+
5961
SECURE_CONTENT_TYPE_NOSNIFF = env.bool("SECURE_CONTENT_TYPE_NOSNIFF", default=True)
6062

6163
X_FRAME_OPTIONS = env.str("X_FRAME_OPTIONS", default="DENY")

scancodeio/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from rest_framework.routers import DefaultRouter
3030

31+
from scanpipe.admin import admin_site
3132
from scanpipe.api.views import ProjectViewSet
3233
from scanpipe.api.views import RunViewSet
3334
from scanpipe.views import AccountProfileView
@@ -53,5 +54,10 @@
5354
path("", RedirectView.as_view(url="project/")),
5455
]
5556

57+
58+
if settings.SCANCODEIO_ENABLE_ADMIN_SITE:
59+
urlpatterns.append(path("admin/", admin_site.urls))
60+
61+
5662
if settings.DEBUG and settings.DEBUG_TOOLBAR:
5763
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))

scanpipe/admin.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/nexB/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/scancode.io for support and download.
22+
23+
from django.contrib import admin
24+
from django.urls import reverse
25+
from django.utils.html import format_html
26+
27+
from scanpipe.models import CodebaseResource
28+
from scanpipe.models import DiscoveredDependency
29+
from scanpipe.models import DiscoveredPackage
30+
from scanpipe.models import Project
31+
32+
33+
class ScanPipeBaseAdmin(admin.ModelAdmin):
34+
"""Common ModelAdmin attributes."""
35+
36+
actions_on_top = False
37+
actions_on_bottom = True
38+
show_facets = admin.ShowFacets.ALWAYS
39+
40+
def has_add_permission(self, request):
41+
return False
42+
43+
44+
class ProjectAdmin(ScanPipeBaseAdmin):
45+
list_display = [
46+
"name",
47+
"label_list",
48+
"packages_link",
49+
"dependencies_link",
50+
"resources_link",
51+
"is_archived",
52+
]
53+
search_fields = ["uuid", "name"]
54+
list_filter = ["is_archived", "labels"]
55+
ordering = ["-created_date"]
56+
fieldsets = [
57+
("", {"fields": ("name", "slug", "notes", "extra_data", "settings", "uuid")}),
58+
("Links", {"fields": ("packages_link", "dependencies_link", "resources_link")}),
59+
]
60+
readonly_fields = ["packages_link", "dependencies_link", "resources_link", "uuid"]
61+
62+
def get_queryset(self, request):
63+
return (
64+
super()
65+
.get_queryset(request)
66+
.prefetch_related("labels")
67+
.with_counts(
68+
"codebaseresources",
69+
"discoveredpackages",
70+
"discovereddependencies",
71+
)
72+
)
73+
74+
@admin.display(description="Labels")
75+
def label_list(self, obj):
76+
return ", ".join(label.name for label in obj.labels.all())
77+
78+
@staticmethod
79+
def make_filtered_link(obj, value, url_name):
80+
"""Return a link to the provided ``url_name`` filtered by this project."""
81+
url = reverse(f"admin:scanpipe_{url_name}_changelist")
82+
return format_html(
83+
'<a href="{}?project__uuid__exact={}">{}</a>', url, obj.uuid, value
84+
)
85+
86+
@admin.display(description="Packages", ordering="discoveredpackages_count")
87+
def packages_link(self, obj):
88+
count = obj.discoveredpackages_count
89+
return self.make_filtered_link(obj, count, "discoveredpackage")
90+
91+
@admin.display(description="Dependencies", ordering="discovereddependencies_count")
92+
def dependencies_link(self, obj):
93+
count = obj.discovereddependencies_count
94+
return self.make_filtered_link(obj, count, "discovereddependency")
95+
96+
@admin.display(description="Resources", ordering="codebaseresources_count")
97+
def resources_link(self, obj):
98+
count = obj.codebaseresources_count
99+
return self.make_filtered_link(obj, count, "codebaseresource")
100+
101+
102+
class CodebaseResourceAdmin(ScanPipeBaseAdmin):
103+
list_display = [
104+
"path",
105+
"status",
106+
"type",
107+
"name",
108+
"extension",
109+
"programming_language",
110+
"mime_type",
111+
"tag",
112+
"detected_license_expression",
113+
"compliance_alert",
114+
"project",
115+
]
116+
search_fields = [
117+
"path",
118+
]
119+
list_filter = ["project", "type", "programming_language", "compliance_alert"]
120+
ordering = ["project", "path"]
121+
122+
123+
class DiscoveredPackageAdmin(ScanPipeBaseAdmin):
124+
list_display = [
125+
"__str__",
126+
"declared_license_expression",
127+
"primary_language",
128+
"project",
129+
]
130+
search_fields = [
131+
"uuid",
132+
"package_uid",
133+
"type",
134+
"namespace",
135+
"name",
136+
"version",
137+
"filename",
138+
"declared_license_expression",
139+
"other_license_expression",
140+
"tag",
141+
"keywords",
142+
]
143+
list_filter = ["project", "type", "primary_language", "compliance_alert"]
144+
exclude = ["codebase_resources"]
145+
ordering = ["project", "type", "namespace", "name", "version"]
146+
147+
148+
class DiscoveredDependencyAdmin(ScanPipeBaseAdmin):
149+
list_display = [
150+
"dependency_uid",
151+
"type",
152+
"scope",
153+
"is_runtime",
154+
"is_optional",
155+
"is_resolved",
156+
"is_direct",
157+
"project",
158+
]
159+
search_fields = [
160+
"uuid",
161+
"dependency_uid",
162+
"namespace",
163+
"name",
164+
"version",
165+
"datasource_id",
166+
"extracted_requirement",
167+
]
168+
list_filter = [
169+
"project",
170+
"type",
171+
"scope",
172+
"is_runtime",
173+
"is_optional",
174+
"is_resolved",
175+
"is_direct",
176+
]
177+
ordering = ["project", "dependency_uid"]
178+
179+
180+
class ScanCodeIOAdminSite(admin.AdminSite):
181+
site_header = "ScanCode.io administration"
182+
site_title = "ScanCode.io administration"
183+
184+
185+
admin_site = ScanCodeIOAdminSite(name="scancodeio_admin")
186+
admin_site.register(Project, ProjectAdmin)
187+
admin_site.register(CodebaseResource, CodebaseResourceAdmin)
188+
admin_site.register(DiscoveredPackage, DiscoveredPackageAdmin)
189+
admin_site.register(DiscoveredDependency, DiscoveredDependencyAdmin)

scanpipe/management/commands/create-user.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,21 @@ def add_arguments(self, parser):
5050
dest="interactive",
5151
help="Do not prompt the user for input of any kind.",
5252
)
53+
parser.add_argument(
54+
"--admin",
55+
action="store_true",
56+
help="Specifies that the user should be created as an admin user.",
57+
)
58+
parser.add_argument(
59+
"--super",
60+
action="store_true",
61+
help="Specifies that the user should be created as a superuser.",
62+
)
5363

5464
def handle(self, *args, **options):
5565
username = options["username"]
66+
is_admin = options["admin"]
67+
is_superuser = options["super"]
5668

5769
error_msg = self._validate_username(username)
5870
if error_msg:
@@ -62,7 +74,14 @@ def handle(self, *args, **options):
6274
if options["interactive"]:
6375
password = self.get_password_from_stdin(username)
6476

65-
user = self.UserModel._default_manager.create_user(username, password=password)
77+
user_kwargs = {
78+
"username": username,
79+
"password": password,
80+
"is_staff": is_admin or is_superuser,
81+
"is_superuser": is_superuser,
82+
}
83+
84+
user = self.UserModel._default_manager.create_user(**user_kwargs)
6685
token, _ = Token._default_manager.get_or_create(user=user)
6786

6887
if options["verbosity"] > 0:

scanpipe/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from django.db.models.functions import Lower
5656
from django.dispatch import receiver
5757
from django.forms import model_to_dict
58+
from django.urls import NoReverseMatch
5859
from django.urls import reverse
5960
from django.utils import timezone
6061
from django.utils.functional import cached_property
@@ -448,6 +449,33 @@ def update(self, **kwargs):
448449
self.save(update_fields=list(kwargs.keys()))
449450

450451

452+
class AdminURLMixin:
453+
"""
454+
A mixin to provide an admin URL for a model instance.
455+
456+
This mixin adds a method to generate the admin URL for a model instance,
457+
which can be useful for linking to the admin interface directly from
458+
the model instances.
459+
"""
460+
461+
def get_admin_url(self):
462+
"""
463+
Return the URL for the admin change view of the instance.
464+
The admin URL is only constructed and returned if the
465+
SCANCODEIO_ENABLE_ADMIN_SITE setting is enabled.
466+
"""
467+
if not settings.SCANCODEIO_ENABLE_ADMIN_SITE:
468+
return
469+
470+
opts = self._meta
471+
viewname = f"admin:{opts.app_label}_{opts.model_name}_change"
472+
try:
473+
url = reverse(viewname, args=[self.pk])
474+
except NoReverseMatch:
475+
return
476+
return url
477+
478+
451479
def get_project_slug(project):
452480
"""
453481
Return a "slug" value for the provided ``project`` based on the slugify name
@@ -2412,6 +2440,7 @@ class CodebaseResource(
24122440
UpdateFromDataMixin,
24132441
HashFieldsMixin,
24142442
ComplianceAlertMixin,
2443+
AdminURLMixin,
24152444
models.Model,
24162445
):
24172446
"""
@@ -3146,6 +3175,7 @@ class DiscoveredPackage(
31463175
PackageURLMixin,
31473176
VulnerabilityMixin,
31483177
ComplianceAlertMixin,
3178+
AdminURLMixin,
31493179
AbstractPackage,
31503180
):
31513181
"""
@@ -3495,6 +3525,7 @@ class DiscoveredDependency(
34953525
SaveProjectMessageMixin,
34963526
UpdateFromDataMixin,
34973527
VulnerabilityMixin,
3528+
AdminURLMixin,
34983529
PackageURLMixin,
34993530
):
35003531
"""
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% if user.is_superuser %}
2+
{% with object.get_admin_url as admin_url %}
3+
{% if admin_url %}
4+
<span class="icon is-size-6">
5+
<a href="{{ object.get_admin_url }}" target="_blank" title="Edit in Admin">
6+
<i class="fa-regular fa-pen-to-square"></i>
7+
</a>
8+
</span>
9+
{% endif %}
10+
{% endwith %}
11+
{% endif %}

scanpipe/templates/scanpipe/includes/breadcrumb_detail_view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{% if object_title %}
1212
<h1 class="is-size-5 break-all has-text-weight-semibold has-text-grey-darker">
1313
{{ object_title }}
14+
{% include 'scanpipe/includes/admin_edit_link.html' %}
1415
</h1>
1516
{% elif template_title %}
1617
{% include template_title %}

scanpipe/templates/scanpipe/includes/resource_path_links.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ <h1 class="is-size-5 break-word">
1313
<span class="has-text-weight-semibold">{{ segment }}{% if is_extract %}-extract{% endif %}</span>
1414
{% endif %}
1515
{% endfor %}
16+
{% include 'scanpipe/includes/admin_edit_link.html' %}
1617
</h1>
1718
{% endspaceless %}

0 commit comments

Comments
 (0)