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,#3492): big overhaul of the SNAPI admin panel #3495

Merged
merged 9 commits into from
Mar 20, 2024
151 changes: 133 additions & 18 deletions api/admin.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,159 @@
"""Custom admin views for the Spaceflight News API."""

from django import forms
from django.contrib import admin
from django.db.models import Count, QuerySet
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from django.utils.html import format_html
from django.utils.safestring import SafeString

# ignore the type error as it seems there's no package for it
from jet.filters import RelatedFieldAjaxListFilter # type: ignore

from api.models import Article, Blog, Event, Launch, NewsSite, Provider, Report
from api.models.abc import NewsItem


class ArticleForm(forms.ModelForm[NewsItem]):
title = forms.CharField(widget=forms.TextInput(attrs={"size": 70}), required=True)


# Register your models here.
# Models that need customization
@admin.register(Article)
@admin.register(Blog)
class ArticleAdmin(admin.ModelAdmin[NewsItem]):
"""Admin view for articles and blogs."""

list_per_page = 30
form = ArticleForm
list_display = (
"title",
"news_site",
"published_at",
"featured",
"published_at_formatted",
"news_site_formatted",
"assigned_launches",
"assigned_events",
"featured_formatted",
"is_deleted_formatted",
)
list_filter = (
("news_site", RelatedFieldAjaxListFilter),
("launches", RelatedFieldAjaxListFilter),
("events", RelatedFieldAjaxListFilter),
"published_at",
"featured",
"is_deleted",
)
list_filter = ("published_at", "featured", "news_site", "is_deleted")
filter_horizontal = ["launches", "events"]
search_fields = ["title"]
ordering = ("-published_at",)
readonly_fields = [
"image_tag",
]
fields = [
"title",
"url",
"news_site",
"summary",
"published_at",
"featured",
"launches",
"events",
"is_deleted",
"image_tag",
]

@staticmethod
def assigned_launches(obj: NewsItem) -> int:
@admin.display(
ordering="-published_at",
description="Published at",
)
def published_at_formatted(obj: NewsItem) -> str:
"""Returns the publication datetime as a formatted string."""
return obj.published_at.strftime("%B %d, %Y – %H:%M")

@staticmethod
@admin.display(
ordering="news_site",
description="News site",
)
def news_site_formatted(obj: NewsItem) -> SafeString:
"""Returns the news site as a hyperlink to the article page."""
return format_html('<a href="{}">{}</a>', obj.url, obj.news_site)

@staticmethod
@admin.display(
description=format_html(
'<div title="{}">{}</div >',
"LL2 Launches",
"L",
),
)
def assigned_launches(obj: NewsItem) -> SafeString:
"""Returns the number of launches assigned to the article."""
return obj.launches.count()
count = obj.launches.count()
if count == 0:
return format_html("")
return format_html(
'<div title="{}" style="text-align:center;background:LawnGreen;color:black">{}</div >',
"\n".join(obj.launches.values_list("name", flat=True)),
obj.launches.count(),
)

@staticmethod
def assigned_events(obj: NewsItem) -> int:
@admin.display(
description=format_html(
'<div title="{}">{}</div >',
"LL2 Events",
"E",
)
)
def assigned_events(obj: NewsItem) -> SafeString:
"""Returns the number of events assigned to the article."""
return obj.events.count()
count = obj.events.count()
if count == 0:
return format_html("")
return format_html(
'<div title="{}" style="text-align:center;background:PaleTurquoise;color:black">{}</div >',
"\n".join(obj.events.values_list("name", flat=True)),
obj.events.count(),
)

def get_queryset(self, request: HttpRequest) -> QuerySet[NewsItem]:
queryset = super().get_queryset(request)
queryset = queryset.annotate(launch_count=Count("launches"), event_count=Count("events"))
return queryset
@staticmethod
@admin.display(
boolean=True,
ordering="featured",
description=format_html(
'<div title="{}">{}</div >',
"Featured",
"F",
),
)
def featured_formatted(obj: NewsItem) -> bool:
"""Returns whether the article is featured."""
return obj.featured

@staticmethod
@admin.display(
boolean=True,
ordering="is_deleted",
description=format_html(
'<div title="{}">{}</div >',
"Deleted",
"D",
),
)
def is_deleted_formatted(obj: NewsItem) -> bool:
"""Returns whether the article is hidden from the API response."""
return obj.is_deleted

@staticmethod
@admin.display(description="Image")
def image_tag(obj: NewsItem) -> SafeString:
"""Returns the image of the article."""
return format_html('<img src="{}" width=50%/>', obj.image_url)

def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | None = None) -> HttpResponse:
extra_context = {"title": "News"}
return super().changelist_view(request, extra_context)


@admin.register(Report)
Expand All @@ -58,6 +169,10 @@ class ReportAdmin(admin.ModelAdmin[Report]):
class NewsSiteAdmin(admin.ModelAdmin[NewsSite]):
list_display = ("name", "id")

def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | None = None) -> HttpResponse:
extra_context = {"title": "News Sites"}
return super().changelist_view(request, extra_context)


# Models that can be registered as is
@admin.register(Event)
Expand All @@ -75,6 +190,6 @@ class LaunchAdmin(admin.ModelAdmin[Launch]):
admin.site.register(Provider)

# Other customizations
admin.site.site_title = "Spaceflight News API Admin"
admin.site.site_header = "Spaceflight News API Admin"
admin.site.index_title = "Spaceflight News API Admin"
admin.site.site_title = "SNAPI Admin"
admin.site.site_header = "SNAPI Admin"
admin.site.index_title = "Home"
6 changes: 6 additions & 0 deletions api/models/news_site.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Literal

from django.db import models


Expand All @@ -6,3 +8,7 @@ class NewsSite(models.Model):

def __str__(self) -> str:
return self.name

@staticmethod
def autocomplete_search_fields() -> tuple[Literal["name"],]:
return ("name",)