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

feat(admin): django admin tweaks #4842

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8e18830
feat(admin): add date_created to ProcessingQueueAdmin and PacerFetchQ…
elisa-a-v Dec 18, 2024
d76d64e
feat(admin): add autocomplete field for 'court' in DocketAdmin
elisa-a-v Dec 18, 2024
2a3a40a
feat(admin): add autocomplete field for 'user' in SCOTUSMapAdmin
elisa-a-v Dec 18, 2024
e85e13b
feat(admin): add date_created and date_modified to EmailProcessingQue…
elisa-a-v Dec 18, 2024
94c4640
feat(admin): add jurisdiction to CourtAdmin list_display
elisa-a-v Dec 19, 2024
83132b1
feat(admin): optimize queryset for user_permissions in User form
elisa-a-v Dec 19, 2024
5e77e85
feat(admin): include admin for UserProxyEvent and UserProfileEvent mo…
elisa-a-v Dec 19, 2024
85c8a3e
feat(admin): add links to related Events admins in User instance detail
elisa-a-v Dec 19, 2024
36d7002
refactor(admin): move get_email_confirmed and get_stub_account to Use…
elisa-a-v Dec 20, 2024
e210304
Merge branch 'main' into 2988-django-admin-tweaks
elisa-a-v Dec 20, 2024
3cca3c5
refactor(admin): use decorator to register UserAdmin
elisa-a-v Dec 20, 2024
10e6d08
feat(admin): make all events fields readonly
elisa-a-v Dec 20, 2024
265058e
feat(admin): add list_filter fields to UserAdmin
elisa-a-v Dec 20, 2024
50aa258
feat(admin): enhance DocketEntryAdmin list view
elisa-a-v Dec 20, 2024
60b05f7
feat(admin): replace DocketEntryInline in DocketAdmin
elisa-a-v Dec 20, 2024
1ec8c0f
feat(admin): enhance DocketAdmin list view
elisa-a-v Dec 20, 2024
58f99e5
feat(admin): add search_help_text to admins with search fields
elisa-a-v Dec 20, 2024
e114241
fix(admin): remove non-indexed fields from DocketEntryAdmin search fi…
elisa-a-v Dec 20, 2024
e779c3c
feat(admin): replace DocketAlertInline in DocketAdmin
elisa-a-v Dec 21, 2024
13832c0
feat(admin): add appeal_from to autocomplete fields in DocketAdmin
elisa-a-v Dec 23, 2024
89e6d58
fix(admin): update UserAdmin change_form_template path
elisa-a-v Dec 23, 2024
a1e9d4a
refactor(admin): introduce method to build admin URLs for a given model
elisa-a-v Dec 23, 2024
d64ecf7
Merge branch 'main' into 2988-django-admin-tweaks
elisa-a-v Dec 23, 2024
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
19 changes: 19 additions & 0 deletions cl/assets/templates/admin/docket_change_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "admin/change_form.html" %}

{% block object-tools-items %}
{% if docket_entries_url %}
<li>
<a class="historylink" href="{{ docket_entries_url }}">
mlissner marked this conversation as resolved.
Show resolved Hide resolved
View Docket Entries
</a>
</li>
{% endif %}
{% if docket_alerts_url %}
<li>
<a class="historylink" href="{{ docket_alerts_url }}">
mlissner marked this conversation as resolved.
Show resolved Hide resolved
View Docket Alerts
</a>
</li>
{% endif %}
{{ block.super }}
{% endblock object-tools-items %}
19 changes: 19 additions & 0 deletions cl/assets/templates/admin/user_change_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "admin/change_form.html" %}

{% block object-tools-items %}
{% if proxy_events_url %}
<li>
<a class="historylink" href="{{ proxy_events_url }}">
Copy link

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:

Detected a template variable used in an anchor tag with the 'href' attribute. This allows a malicious actor to input the 'javascript:' URI and is subject to cross- site scripting (XSS) attacks. If using Flask, use 'url_for()' to safely generate a URL. If using Django, use the 'url' filter to safely generate a URL. If using Mustache, use a URL encoding library, or prepend a slash '/' to the variable for relative links (href="/{{link}}"). You may also consider setting the Content Security Policy (CSP) header.

To resolve this comment:

No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Leave a nosemgrep comment directly above or at the end of line 6 like so // nosemgrep: generic.html-templates.security.var-in-href.var-in-href

Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.

You can view more details about this finding in the Semgrep AppSec Platform.

View UserProxy Events
</a>
</li>
{% endif %}
{% if profile_events_url %}
<li>
<a class="historylink" href="{{ profile_events_url }}">
Copy link

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:

Detected a template variable used in an anchor tag with the 'href' attribute. This allows a malicious actor to input the 'javascript:' URI and is subject to cross- site scripting (XSS) attacks. If using Flask, use 'url_for()' to safely generate a URL. If using Django, use the 'url' filter to safely generate a URL. If using Mustache, use a URL encoding library, or prepend a slash '/' to the variable for relative links (href="/{{link}}"). You may also consider setting the Content Security Policy (CSP) header.

To resolve this comment:

No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Leave a nosemgrep comment directly above or at the end of line 13 like so // nosemgrep: generic.html-templates.security.var-in-href.var-in-href

Take care to validate that this is not a true positive finding before ignoring it.
Learn more about ignoring code, files and folders here.

You can view more details about this finding in the Semgrep AppSec Platform.

View UserProfile Events
</a>
</li>
{% endif %}
{{ block.super }}
{% endblock object-tools-items %}
26 changes: 11 additions & 15 deletions cl/recap/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ class ProcessingQueueAdmin(CursorPaginatorAdmin):
"pacer_case_id",
"document_number",
"attachment_number",
"date_created",
)
list_filter = ("status",)
list_filter = ("status", "date_created")
search_help_text = "Search ProcessingQueues by pacer_case_id or court__pk."
search_fields = (
"pacer_case_id",
"court__pk",
Expand All @@ -41,15 +43,8 @@ class ProcessingQueueAdmin(CursorPaginatorAdmin):

@admin.register(PacerFetchQueue)
class PacerFetchQueueAdmin(CursorPaginatorAdmin):
list_display = (
"__str__",
"court",
"request_type",
)
list_filter = (
"status",
"request_type",
)
list_display = ("__str__", "court", "request_type", "date_created")
list_filter = ("status", "request_type", "date_created")
readonly_fields = (
"date_created",
"date_modified",
Expand Down Expand Up @@ -94,14 +89,15 @@ def reprocess_failed_epq(modeladmin, request, queryset):

@admin.register(EmailProcessingQueue)
class EmailProcessingQueueAdmin(CursorPaginatorAdmin):
list_display = (
"__str__",
"status",
)
list_filter = ("status",)
list_display = ("__str__", "status", "date_created")
list_filter = ("status", "date_created")
actions = [reprocess_failed_epq]
raw_id_fields = ["uploader", "court"]
exclude = ["recap_documents", "filepath"]
readonly_fields = (
"date_created",
"date_modified",
)


admin.site.register(FjcIntegratedDatabase)
78 changes: 69 additions & 9 deletions cl/search/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from django.http import HttpRequest

from cl.alerts.admin import DocketAlertInline
from cl.alerts.models import DocketAlert
from cl.lib.cloud_front import invalidate_cloudfront
from cl.lib.models import THUMBNAIL_STATUSES
from cl.lib.string_utils import trunc
from cl.recap.management.commands.delete_document_from_ia import delete_from_ia
from cl.search.models import (
BankruptcyInformation,
Expand Down Expand Up @@ -88,7 +90,14 @@ class OpinionClusterAdmin(CursorPaginatorAdmin):

@admin.register(Court)
class CourtAdmin(admin.ModelAdmin):
list_display = ("full_name", "short_name", "position", "in_use", "pk")
list_display = (
"full_name",
"short_name",
"position",
"in_use",
"pk",
"jurisdiction",
)
list_filter = (
"jurisdiction",
"in_use",
Expand Down Expand Up @@ -215,17 +224,36 @@ class RECAPDocumentInline(admin.StackedInline):
@admin.register(DocketEntry)
class DocketEntryAdmin(CursorPaginatorAdmin):
inlines = (RECAPDocumentInline,)
search_help_text = (
"Search DocketEntries by Docket ID or RECAP sequence number."
)
search_fields = (
"docket__id",
"recap_sequence_number",
)
list_display = (
"get_pk",
"get_trunc_description",
"date_filed",
"time_filed",
"entry_number",
"recap_sequence_number",
"pacer_sequence_number",
)
raw_id_fields = ("docket", "tags")
readonly_fields = (
"date_created",
"date_modified",
)
list_filter = ("date_filed", "date_created", "date_modified")

@admin.display(description="Docket entry")
def get_pk(self, obj):
return obj.pk

class DocketEntryInline(admin.TabularInline):
model = DocketEntry
extra = 1
raw_id_fields = ("tags",)
@admin.display(description="Description")
def get_trunc_description(self, obj):
return trunc(obj.description, 35, ellipsis="...")


@admin.register(OriginatingCourtInformation)
Expand All @@ -238,17 +266,26 @@ class OriginatingCourtInformationAdmin(admin.ModelAdmin):

@admin.register(Docket)
class DocketAdmin(CursorPaginatorAdmin):
change_form_template = "admin/docket_change_form.html"
prepopulated_fields = {"slug": ["case_name"]}
inlines = (
DocketEntryInline,
BankruptcyInformationInline,
DocketAlertInline,
list_display = (
"__str__",
"pacer_case_id",
"docket_number",
"pacer_case_id",
)
search_help_text = "Search dockets by PK, PACER case ID, or Docket number."
search_fields = ("pk", "pacer_case_id", "docket_number")
inlines = (BankruptcyInformationInline,)
readonly_fields = (
"date_created",
"date_modified",
"view_count",
)
autocomplete_fields = (
"court",
"appeal_from",
)
raw_id_fields = (
"panel",
"tags",
Expand All @@ -259,6 +296,29 @@ class DocketAdmin(CursorPaginatorAdmin):
"parent_docket",
)

def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add links to pre-filtered related admin pages."""
extra_context = extra_context or {}
docket = self.get_object(request, object_id)

if docket and hasattr(docket, "docket_entries"):
docket_entries_url = (
f"/admin/{DocketEntry._meta.app_label}/"
f"{DocketEntry._meta.model_name}/?docket={object_id}"
)
extra_context["docket_entries_url"] = docket_entries_url

if docket and hasattr(docket, "alerts"):
docket_alerts_url = (
f"/admin/{DocketAlert._meta.app_label}/"
f"{DocketAlert._meta.model_name}/?docket={object_id}"
)
extra_context["docket_alerts_url"] = docket_alerts_url

return super().change_view(
request, object_id, form_url, extra_context=extra_context
)


@admin.register(OpinionsCited)
class OpinionsCitedAdmin(CursorPaginatorAdmin):
Expand Down
135 changes: 118 additions & 17 deletions cl/users/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.apps import apps
from django.contrib import admin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Permission, User
from rest_framework.authtoken.models import Token

Expand All @@ -19,30 +21,42 @@
UserProfile,
)

UserProxyEvent = apps.get_model("users", "UserProxyEvent")
UserProfileEvent = apps.get_model("users", "UserProfileEvent")

def get_email_confirmed(obj):
return obj.profile.email_confirmed


get_email_confirmed.short_description = "Email Confirmed?"

class TokenInline(admin.StackedInline):
model = Token

def get_stub_account(obj):
return obj.profile.stub_account

class UserProfileInline(admin.StackedInline):
model = UserProfile

get_stub_account.short_description = "Stub Account?"

class CustomUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = User
fields = "__all__"

class TokenInline(admin.StackedInline):
model = Token
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Ensure user_permissions field uses an optimized queryset
if "user_permissions" in self.fields:
self.fields["user_permissions"].queryset = (
Permission.objects.select_related("content_type").order_by(
"content_type__app_label", "codename"
)
)


class UserProfileInline(admin.StackedInline):
model = UserProfile
# Replace the normal User admin with our better one.
admin.site.unregister(User)


@admin.register(User)
class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
form = CustomUserChangeForm # optimize queryset for user_permissions field
change_form_template = "admin/user_change_form.html"
inlines = (
UserProfileInline,
DonationInline,
Expand All @@ -57,8 +71,16 @@ class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
)
list_display = (
"username",
get_email_confirmed,
get_stub_account,
"get_email_confirmed",
"get_stub_account",
)
list_filter = (
"is_superuser",
"profile__email_confirmed",
"profile__stub_account",
)
search_help_text = (
"Search Users by username, first name, last name, or email."
)
search_fields = (
"username",
Expand All @@ -67,6 +89,36 @@ class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
"email",
)

def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add links to related event admin pages filtered by user/profile."""
extra_context = extra_context or {}
user = self.get_object(request, object_id)
proxy_events_url = (
f"/admin/{UserProxyEvent._meta.app_label}/"
f"{UserProxyEvent._meta.model_name}/?pgh_obj={object_id}"
)
extra_context["proxy_events_url"] = proxy_events_url

if user and hasattr(user, "profile"):
profile_id = user.profile.pk
profile_events_url = (
f"/admin/{UserProfileEvent._meta.app_label}/"
f"{UserProfileEvent._meta.model_name}/?pgh_obj={profile_id}"
)
extra_context["profile_events_url"] = profile_events_url

return super().change_view(
request, object_id, form_url, extra_context=extra_context
)

@admin.display(description="Email Confirmed?")
def get_email_confirmed(self, obj):
return obj.profile.email_confirmed

@admin.display(description="Stub Account?")
def get_stub_account(self, obj):
return obj.profile.stub_account


@admin.register(EmailFlag)
class EmailFlagAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -117,8 +169,57 @@ class FailedEmailAdmin(admin.ModelAdmin):
raw_id_fields = ("stored_email",)


# Replace the normal User admin with our better one.
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
class BaseUserEventAdmin(admin.ModelAdmin):
ordering = ("-pgh_created_at",)
# Define common attributes to be extended:
common_list_display = ("get_pgh_created", "get_pgh_label")
common_list_filters = ("pgh_created_at",)
common_search_fields = ("pgh_obj",)
# Default to common attributes:
list_display = common_list_display
list_filter = common_list_filters
search_fields = common_search_fields

@admin.display(ordering="pgh_created_at", description="Event triggered")
def get_pgh_created(self, obj):
return obj.pgh_created_at

@admin.display(ordering="pgh_label", description="Event label")
def get_pgh_label(self, obj):
return obj.pgh_label


@admin.register(UserProxyEvent)
class UserProxyEventAdmin(BaseUserEventAdmin):
search_help_text = "Search UserProxyEvents by pgh_obj, email, or username."
search_fields = BaseUserEventAdmin.common_search_fields + (
"email",
"username",
)
list_display = BaseUserEventAdmin.list_display + (
"email",
"username",
)
readonly_fields = [
field.name for field in UserProxyEvent._meta.get_fields()
]


@admin.register(UserProfileEvent)
class UserProfileEventAdmin(BaseUserEventAdmin):
search_help_text = "Search UserProxyEvents by pgh_obj or username."
search_fields = BaseUserEventAdmin.common_search_fields + (
"user__username",
)
list_display = BaseUserEventAdmin.common_list_display + (
"user",
"email_confirmed",
)
list_filter = BaseUserEventAdmin.common_list_filters + ("email_confirmed",)
readonly_fields = [
field.name for field in UserProfileEvent._meta.get_fields()
]


admin.site.register(BarMembership)
admin.site.register(Permission)
Loading
Loading