-
Notifications
You must be signed in to change notification settings - Fork 9
Django 5 and Django-unfold theme #227
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
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
677823b
django unfold
fpintoppb a32ba5d
Django unfold changes
fpintoppb 4f7ed96
django unfold index and sidebar
fpintoppb f82cff3
SCA - Django Unfold and Improvements
fpintoppb bac0f8b
fix small errors
fpintoppb 3042344
adds sbom_uid
fpintoppb 55d713c
improvements fro django unfold migration
fpintoppb 4377252
migrations
fpintoppb b8d15ad
fixes vulns finding
fpintoppb 587d97f
ui improvements and ReverseReadonlyMixin
fpintoppb 83b4d05
ui improvements
fpintoppb 3f6d672
removes comment
fpintoppb 115f03e
adds sidebar items
fpintoppb 05bb311
python 3.11
fpintoppb 02371d9
3.11-slim
fpintoppb 01142d4
3.11-slim-buster
fpintoppb 3e74060
3.11-slim-trixie
fpintoppb 3dbd277
fixes dockerfile
fpintoppb 90c92e8
fixes tests
fpintoppb a73c6bd
reformat with black
fpintoppb 584d8ce
adds hvac
fpintoppb ade88bc
fixes test
fpintoppb 769d122
fixes test
fpintoppb cd1e587
Log In button
fpintoppb 84fe29a
fixes login test
fpintoppb 534893d
dkron
fpintoppb e4eb254
dkron test fix
fpintoppb c1ed5cc
use filters
fpintoppb d33de89
fix: adds missing installed app, adds sca to sidebar and removes add …
fpintoppb 0154045
black
fpintoppb 9da933c
form submit
fpintoppb be06d88
rm import
fpintoppb 2ec0d24
indent
fpintoppb fb46f81
ui color changes
fpintoppb e864a87
adds missing files
fpintoppb 3f30729
adds result-list-wrapper
fpintoppb 9c4aff8
fixes import
fpintoppb 0dcd955
download sbom
fpintoppb 56276b0
fixes tests
fpintoppb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,13 @@ | ||
| import pytest | ||
| from selenium.webdriver.common.by import By | ||
| from selenium.webdriver.common.keys import Keys | ||
|
|
||
|
|
||
| @pytest.mark.webtest | ||
| def test_login(live_server, test_cfg): | ||
| test_cfg.driver.get(f"{live_server.url}/login/") | ||
| assert "Login" in test_cfg.driver.title | ||
| assert "Log in" in test_cfg.driver.title | ||
|
|
||
| test_cfg.driver.find_element(by=By.ID, value="id_username").send_keys( | ||
| test_cfg.username | ||
| ) | ||
| test_cfg.driver.find_element(by=By.ID, value="id_password").send_keys( | ||
| f"{test_cfg.password}" | ||
| ) | ||
| test_cfg.driver.find_element( | ||
| by=By.XPATH, value='//button[text()="Submit"]' | ||
| ).send_keys(Keys.ENTER) | ||
|
|
||
| assert "Home | Surface" == test_cfg.driver.title | ||
| test_cfg.driver.find_element(by=By.ID, value="id_username").send_keys(test_cfg.username) | ||
| test_cfg.driver.find_element(by=By.ID, value="id_password").send_keys(test_cfg.password) | ||
| test_cfg.driver.find_element(by=By.ID, value="login-form").submit() | ||
| assert "Dashboard | Surface Security" == test_cfg.driver.title |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,189 @@ | ||
| from django_restful_admin import site as rest | ||
| import logging | ||
| from urllib.parse import quote | ||
|
|
||
| from django.apps import apps | ||
| from django.db import models | ||
| from django.urls import reverse | ||
| from django.utils.html import format_html | ||
| from django.utils.safestring import mark_safe | ||
| from jsoneditor.forms import JSONEditor | ||
| from unfold.admin import ModelAdmin | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| from django_restful_admin import site as rest | ||
|
|
||
| # Register all models for REST API except the registered ones | ||
| for model in apps.get_models(): | ||
| if model in rest._registry: | ||
| continue | ||
| rest.register(model) | ||
|
|
||
|
|
||
| class DefaultModelAdmin(ModelAdmin): | ||
| list_filter_submit = True | ||
| list_filter_sheet = False | ||
| list_fullwidth = True | ||
| add_fieldsets = () | ||
| formfield_overrides = { | ||
| models.JSONField: {"widget": JSONEditor(attrs={"style": "background-color: white !important;"})} | ||
| } | ||
|
|
||
| def get_list_display(self, request): | ||
|
fpintoppb marked this conversation as resolved.
|
||
| """ | ||
| make sure model primary key is always present as first column for standard UX | ||
| """ | ||
| default_list_display = list(super(DefaultModelAdmin, self).get_list_display(request)) | ||
|
|
||
| pk = self.model._meta.pk.name | ||
| if pk in default_list_display: | ||
| default_list_display.remove(pk) | ||
| default_list_display.insert(0, self.model._meta.pk.name) | ||
|
|
||
| return default_list_display | ||
|
|
||
| def get_list_display_links(self, request, list_display): | ||
|
fpintoppb marked this conversation as resolved.
|
||
| default_list_display_links = super(DefaultModelAdmin, self).get_list_display_links(request, list_display) | ||
|
|
||
| if not default_list_display_links: | ||
| default_list_display_links = ("pk",) | ||
|
|
||
| return default_list_display_links | ||
|
|
||
|
|
||
| class ReverseReadonlyMixin: | ||
| """ | ||
| Mixin to add a readonly 'reverse' field showing related objects in Django admin. | ||
| Should be used with ModelAdmin or a compatible base class. | ||
| """ | ||
|
|
||
| def get_readonly_fields(self, request, obj=None): | ||
| parent_method = getattr(super(), "get_readonly_fields", None) | ||
| fields = list(parent_method(request, obj)) if parent_method else [] | ||
| if obj and "reverse" not in fields: | ||
| fields.append("reverse") | ||
| return fields | ||
|
|
||
| def get_fieldsets(self, request, obj=None): | ||
| parent_method = getattr(super(), "get_fieldsets", None) | ||
| fieldsets = list(parent_method(request, obj)) if parent_method else [] | ||
| if not fieldsets: | ||
| return fieldsets | ||
| label, opts = fieldsets[0] | ||
| opts = dict(opts) | ||
| opts.setdefault("classes", []).append("tab") | ||
| if "fields" in opts: | ||
| opts["fields"] = tuple(f for f in opts["fields"] if f != "reverse") | ||
| fieldsets[0] = ("General", opts) | ||
| if obj: | ||
| fieldsets.append(("Relationships", {"classes": ["tab"], "fields": ("reverse",)})) | ||
| return tuple(fieldsets) | ||
|
|
||
| def reverse(self, obj): | ||
| """Render all relationships for the current object.""" | ||
| if not obj or not obj.pk: | ||
| return format_html("<div>No relationships available for new objects.</div>") | ||
| try: | ||
| html = self._render_relationships(obj) | ||
| return format_html("<div class='relationships-container'>{}</div>", html) | ||
| except Exception as e: | ||
| return format_html("<div class='text-red-500'>Error loading relationships: {}</div>", str(e)) | ||
|
fpintoppb marked this conversation as resolved.
|
||
|
|
||
|
fpintoppb marked this conversation as resolved.
|
||
| def _render_relationships(self, obj): | ||
| html = [] | ||
| forward_html = self._get_forward_relationships(obj) | ||
| if forward_html: | ||
| html.append("<strong>Forward Relationships</strong>") | ||
| html.append(forward_html) | ||
| reverse_html = self._get_reverse_relationships(obj) | ||
| if reverse_html: | ||
| html.append("<br><strong>Reverse Relationships</strong>") | ||
| html.append(reverse_html) | ||
| if not html: | ||
| html.append("<div>No relationships found.</div>") | ||
| return mark_safe("\n".join(html)) | ||
|
|
||
| def _get_forward_relationships(self, obj): | ||
| html = [] | ||
| opts = obj._meta | ||
| for field in opts.get_fields(): | ||
| if field.is_relation: | ||
| try: | ||
| if not field.many_to_many and hasattr(field, "related_model"): | ||
| rel_obj = getattr(obj, field.name, None) | ||
| if rel_obj: | ||
| html.append( | ||
| self._format_relation(field, rel_obj, single_obj=True, relation_type="ForeignKey") | ||
| ) | ||
| elif field.many_to_many and not field.auto_created: | ||
| manager = getattr(obj, field.name) | ||
| rel_objs = manager.all()[:10] | ||
| total_count = manager.count() | ||
| if rel_objs: | ||
| html.append(self._format_relation(field, rel_objs, total_count, "ManyToMany")) | ||
| except AttributeError: | ||
| continue | ||
| return mark_safe("\n".join(html)) | ||
|
fpintoppb marked this conversation as resolved.
|
||
|
|
||
| def _get_reverse_relationships(self, obj): | ||
| html = [] | ||
| opts = obj._meta | ||
| for field in opts.get_fields(): | ||
| if field.auto_created and field.is_relation: | ||
| try: | ||
| manager = getattr(obj, field.get_accessor_name()) | ||
| if field.one_to_many: | ||
| rel_objs = manager.all()[:10] | ||
| total_count = manager.count() | ||
| if rel_objs: | ||
| html.append(self._format_relation(field, rel_objs, total_count, "ForeignKey")) | ||
| elif field.one_to_one: | ||
| rel_obj = getattr(manager, getattr(field.related_model._meta, "model_name", ""), None) | ||
| if rel_obj: | ||
| html.append( | ||
| self._format_relation(field, rel_obj, single_obj=True, relation_type="OneToOne") | ||
| ) | ||
| elif field.many_to_many: | ||
| rel_objs = manager.all()[:10] | ||
| total_count = manager.count() | ||
| if rel_objs: | ||
| html.append(self._format_relation(field, rel_objs, total_count, "ManyToMany")) | ||
| except AttributeError: | ||
| continue | ||
| return mark_safe("\n".join(html)) | ||
|
|
||
| def _format_relation(self, field, related_objs, total_count=None, relation_type=None, single_obj=False): | ||
| label = getattr(field, "verbose_name", None) | ||
| if not label and hasattr(field, "related_model") and field.related_model: | ||
| label = getattr(field.related_model._meta, "verbose_name_plural", str(field.related_model)) | ||
| label = label.title() if isinstance(label, str) else str(field) | ||
| rel_type = relation_type or "Relation" | ||
| header = f'<div class="py-1">{label}: <b>{rel_type}</b>' | ||
| if total_count is not None: | ||
| header += f" ({total_count} total)" | ||
| header += "</div>" | ||
|
|
||
| def obj_link(obj): | ||
| url = self._get_admin_url(obj) | ||
| return f'<a href="{url}" style="color: rgb(59, 130, 246);">{obj}</a>' if url else str(obj) | ||
|
|
||
| if single_obj: | ||
| body = f'<div class="pl-4" style="padding-left:2em">• {obj_link(related_objs)}</div>' | ||
| else: | ||
| body = "\n".join( | ||
| f'<div class="pl-4" style="padding-left:2em">• {obj_link(obj)}</div>' for obj in related_objs | ||
| ) | ||
| if total_count and total_count > 10: | ||
| body += ( | ||
| f'\n<div class="pl-4 text-gray-500" style="padding-left:2em">... and {total_count - 10} more</div>' | ||
| ) | ||
| return mark_safe(header + "\n" + body) | ||
|
|
||
| def _get_admin_url(self, obj): | ||
| if not obj or not obj.pk: | ||
| return None | ||
| try: | ||
| opts = obj._meta | ||
| return reverse(f"admin:{opts.app_label}_{opts.model_name}_change", args=[quote(str(obj.pk))]) | ||
| except Exception: | ||
| return None | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.