Skip to content

Commit

Permalink
Added links & icons to wagtail form view Used By columns (#4106)
Browse files Browse the repository at this point in the history
Adds icons & edit links to the Wagtail forms views (Application Forms,
Determination Forms, Review Forms, etc.) "Used by" column, making it
easier to differentiate what's used where at a glance . Sometimes rounds
& funds take the same name so it's not possible to know which is which.

Also added an enum class to utils to allow for consistency across the
interface with icon usage rather than hardcoding the same values over
and over. I would've used `StrEnum` but it felt strange to have repeat
values for different enums. Open to better solutions, though.
  • Loading branch information
wes-otf authored Sep 12, 2024
1 parent f0ca4ae commit fccca82
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 26 deletions.
6 changes: 4 additions & 2 deletions hypha/apply/categories/admin.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
from django.urls import re_path
from wagtail.contrib.modeladmin.options import ModelAdmin

from hypha.apply.utils.admin import AdminIcon

from .admin_helpers import MetaTermButtonHelper
from .admin_views import AddChildMetaTermViewClass
from .models import Category, MetaTerm


class CategoryAdmin(ModelAdmin):
menu_label = "Category Questions"
menu_icon = "list-ul"
menu_icon = str(AdminIcon.CATEGORY)
model = Category


class MetaTermAdmin(ModelAdmin):
model = MetaTerm

menu_icon = "tag"
menu_icon = str(AdminIcon.META_TERM)

list_per_page = 50
list_display = ("get_as_listing_header", "get_parent")
Expand Down
4 changes: 2 additions & 2 deletions hypha/apply/determinations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
DeterminationMessageSettings,
)
from hypha.apply.review.admin_helpers import ButtonsWithClone
from hypha.apply.utils.admin import ListRelatedMixin
from hypha.apply.utils.admin import AdminIcon, ListRelatedMixin
from hypha.core.wagtail.admin.options import SettingModelAdmin

from .admin_views import CreateDeterminationFormView, EditDeterminationFormView
Expand All @@ -22,7 +22,7 @@ def __init__(self, *args, **kwargs):

class DeterminationFormAdmin(ListRelatedMixin, ModelAdmin):
model = DeterminationForm
menu_icon = "form"
menu_icon = str(AdminIcon.DETERMINATION_FORM)
list_display = ("name", "used_by")
button_helper_class = ButtonsWithClone
clone_view_class = CloneView
Expand Down
20 changes: 10 additions & 10 deletions hypha/apply/funds/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from hypha.apply.funds.models import ReviewerRole, ReviewerSettings, ScreeningStatus
from hypha.apply.review.admin import ReviewFormAdmin
from hypha.apply.utils.admin import ListRelatedMixin, RelatedFormsMixin
from hypha.apply.utils.admin import AdminIcon, ListRelatedMixin, RelatedFormsMixin
from hypha.core.wagtail.admin.options import SettingModelAdmin

from .admin_helpers import (
Expand Down Expand Up @@ -49,7 +49,7 @@ def fund(self, obj):

class RoundAdmin(BaseRoundAdmin, RelatedFormsMixin):
model = Round
menu_icon = "repeat"
menu_icon = str(AdminIcon.ROUND)
list_display = (
"title",
"fund",
Expand Down Expand Up @@ -85,42 +85,42 @@ def user_can_delete_obj(self, user, obj):

class ScreeningStatusAdmin(ModelAdmin):
model = ScreeningStatus
menu_icon = "tag"
menu_icon = str(AdminIcon.SCREENING_STATUS)
list_display = ("title", "yes", "default")
permission_helper_class = ScreeningStatusPermissionHelper
list_display = ("title", "yes", "default")


class SealedRoundAdmin(BaseRoundAdmin):
model = SealedRound
menu_icon = "lock"
menu_icon = str(AdminIcon.SEALED_ROUND)
menu_label = "Sealed Rounds"
list_display = ("title", "fund", "start_date", "end_date")


class FundAdmin(ModelAdmin, RelatedFormsMixin):
model = FundType
menu_icon = "doc-empty"
menu_icon = str(AdminIcon.FUND)
menu_label = "Funds"
list_display = ("title", "application_forms", "review_forms", "determination_forms")


class RFPAdmin(ModelAdmin):
model = RequestForPartners
menu_icon = "group"
menu_icon = str(AdminIcon.REQUEST_FOR_PARTNERS)
menu_label = "Request For Partners"


class LabAdmin(ModelAdmin, RelatedFormsMixin):
model = LabType
menu_icon = "doc-empty"
menu_icon = str(AdminIcon.LAB)
menu_label = "Labs"
list_display = ("title", "application_forms", "review_forms", "determination_forms")


class ReviewerRoleAdmin(ModelAdmin):
model = ReviewerRole
menu_icon = "group"
menu_icon = str(AdminIcon.REVIEWER_ROLE)
menu_label = "Reviewer Roles"


Expand All @@ -139,7 +139,7 @@ def user_can_delete_obj(self, user, obj):

class ApplicationFormAdmin(ListRelatedMixin, ModelAdmin):
model = ApplicationForm
menu_icon = "form"
menu_icon = str(AdminIcon.APPLICATION_FORM)
list_display = ("name", "used_by")
list_filter = (FormsFundRoundListFilter,)
permission_helper_class = DeletePermission
Expand Down Expand Up @@ -179,7 +179,7 @@ class ReviewerSettingAdmin(SettingModelAdmin):

class ApplyAdminGroup(ModelAdminGroup):
menu_label = "Apply"
menu_icon = "folder-inverse"
menu_icon = str(AdminIcon.APPLY)
items = (
RoundAdmin,
SealedRoundAdmin,
Expand Down
14 changes: 7 additions & 7 deletions hypha/apply/projects/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup

from hypha.apply.utils.admin import ListRelatedMixin
from hypha.apply.utils.admin import AdminIcon, ListRelatedMixin
from hypha.core.wagtail.admin import SettingModelAdmin

from .admin_views import (
Expand All @@ -24,7 +24,7 @@

class DocumentCategoryAdmin(ModelAdmin):
model = DocumentCategory
menu_icon = "doc-full"
menu_icon = str(AdminIcon.DOCUMENT_CATEGORY)
list_display = (
"name",
"required",
Expand All @@ -33,7 +33,7 @@ class DocumentCategoryAdmin(ModelAdmin):

class ContractDocumentCategoryAdmin(ModelAdmin):
model = ContractDocumentCategory
menu_icon = "doc-full"
menu_icon = str(AdminIcon.CONTRACT_DOCUMENT_CATEGORY)
list_display = (
"name",
"required",
Expand All @@ -43,7 +43,7 @@ class ContractDocumentCategoryAdmin(ModelAdmin):
class ProjectFormAdmin(ListRelatedMixin, ModelAdmin):
model = ProjectForm
menu_label = "Project Forms"
menu_icon = "form"
menu_icon = str(AdminIcon.PROJECT_FORM)
list_display = (
"name",
"used_by",
Expand All @@ -60,7 +60,7 @@ class ProjectFormAdmin(ListRelatedMixin, ModelAdmin):
class ProjectSOWFormAdmin(ListRelatedMixin, ModelAdmin):
model = ProjectSOWForm
menu_label = "SOW Forms"
menu_icon = "form"
menu_icon = str(AdminIcon.PROJECT_SOW_FORM)
list_display = (
"name",
"used_by",
Expand All @@ -77,7 +77,7 @@ class ProjectSOWFormAdmin(ListRelatedMixin, ModelAdmin):
class ProjectReportFormAdmin(ListRelatedMixin, ModelAdmin):
model = ProjectReportForm
menu_label = "Report Forms"
menu_icon = "form"
menu_icon = str(AdminIcon.PROJECT_REPORT_FORM)
list_display = (
"name",
"used_by",
Expand All @@ -101,7 +101,7 @@ class VendorFormSettingsAdmin(SettingModelAdmin):

class ProjectAdminGroup(ModelAdminGroup):
menu_label = "Projects"
menu_icon = "folder-open-1"
menu_icon = str(AdminIcon.PROJECT)
items = (
ContractDocumentCategoryAdmin,
DocumentCategoryAdmin,
Expand Down
4 changes: 2 additions & 2 deletions hypha/apply/review/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from wagtail.contrib.modeladmin.views import CreateView, InstanceSpecificView

from hypha.apply.review.models import ReviewForm
from hypha.apply.utils.admin import ListRelatedMixin
from hypha.apply.utils.admin import AdminIcon, ListRelatedMixin

from .admin_helpers import ButtonsWithClone
from .admin_views import CreateReviewFormView, EditReviewFormView
Expand All @@ -17,7 +17,7 @@ def __init__(self, *args, **kwargs):

class ReviewFormAdmin(ListRelatedMixin, ModelAdmin):
model = ReviewForm
menu_icon = "form"
menu_icon = str(AdminIcon.REVIEW_FORM)
list_display = ("name", "used_by")
button_helper_class = ButtonsWithClone
clone_view_class = CloneView
Expand Down
145 changes: 142 additions & 3 deletions hypha/apply/utils/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from enum import Enum, auto
from typing import Dict, Literal

from django.urls import reverse
from django.utils.safestring import mark_safe

Expand All @@ -18,9 +21,81 @@ def get_queryset(self, request):
related = [f"{form}_set__{field}" for form, field in self.related_models]
return qs.prefetch_related(*related)

def _list_related(self, obj, form, field):
def _get_icon_str(self, field: Literal["lab", "application", "round"]) -> str:
"""Get the icon for the specified field
Args:
field: only supports `lab`, `application` & `round`
Returns:
str: the correlated wagtail icon name
"""
match field:
case "lab":
icon_name = AdminIcon.LAB
case "application":
icon_name = AdminIcon.FUND
case "round":
icon_name = AdminIcon.ROUND
case "_":
icon_name = None

if not icon_name:
return ""

return f'<svg width="0.85rem" height="0.85rem" aria-hidden="true"><use href="#icon-{icon_name}"></use></svg> '

def _get_used_by_html(self, values: Dict[str, str | int], field: str) -> str | None:
"""Get the HTML for an object in the "Used By" column.
Attempts to insert a title, icon & edit URL if possible
Args:
values: a dict containing the keys of `<field>__id` & `<field>__title`.
field: the field of the used object
Returns:
A string if anything was able to be extracted, otherwise `None`
"""
icon_html = self._get_icon_str(field)
if title := values.get(f"{field}__title"):
if id := values.get(f"{field}__id"):
edit_url = reverse("wagtailadmin_pages:edit", args=(id,))
return f"<a href={edit_url}>{icon_html}{title}</a>"
# Edge case but if the object ID is missing, provide the title & icon w/o edit link
return f"{icon_html}{title}"

return None

def _list_related(
self,
obj,
form: Literal[
"applicationbasereviewform", "roundbasereviewform", "labbasereviewform"
],
field: Literal["lab", "application", "round"],
) -> str:
"""Get an HTML string containing all related objects
Args:
obj: a form object (ie. `ReviewForm`, `ApplicationForm`, etc.)
form: related form types to pull
field: the type being pulled
Returns:
str: an HTML string of all related funds, labs & rounds containing icons/links if they could be extracted
"""
related_values = getattr(obj, f"{form}_set").values(
f"{field}__title", f"{field}__id"
)
# return a string of the joined "used by" objects
return ", ".join(
getattr(obj, f"{form}_set").values_list(f"{field}__title", flat=True)
[
html_str
for value in related_values
if (html_str := self._get_used_by_html(value, field))
]
)

def used_by(self, obj):
Expand All @@ -29,7 +104,7 @@ def used_by(self, obj):
related = self._list_related(obj, form, field)
if related:
rows.append(related)
return ", ".join(rows)
return mark_safe(", ".join(rows))


class RelatedFormsMixin:
Expand Down Expand Up @@ -85,3 +160,67 @@ def build_urls(determination_forms):
return

return mark_safe("<br />".join(urls))


class AdminIcon(Enum):
"""
Enum used to keep Wagtail icons consistent across the admin interface
Icon names pulled from https://docs.wagtail.org/en/stable/advanced_topics/icons.html#available-icons
"""

ROUND = auto()
SCREENING_STATUS = auto()
SEALED_ROUND = auto()
FUND = auto()
REQUEST_FOR_PARTNERS = auto()
LAB = auto()
REVIEWER_ROLE = auto()
APPLICATION_FORM = auto()
APPLY = auto()
DOCUMENT_CATEGORY = auto()
CONTRACT_DOCUMENT_CATEGORY = auto()
PROJECT_FORM = auto()
PROJECT_SOW_FORM = auto()
PROJECT_REPORT_FORM = auto()
PROJECT = auto()
REVIEW_FORM = auto()
CATEGORY = auto()
META_TERM = auto()
DETERMINATION_FORM = auto()

def __get_wagtail_icon(self) -> str:
"""
Get the wagtail string for the specified icon
"""
match self:
case (
self.APPLICATION_FORM
| self.PROJECT_FORM
| self.PROJECT_SOW_FORM
| self.REVIEW_FORM
| self.PROJECT_REPORT_FORM
| self.DETERMINATION_FORM
):
return "form"
case self.FUND | self.LAB:
return "doc-empty"
case self.REQUEST_FOR_PARTNERS | self.REVIEWER_ROLE:
return "group"
case self.DOCUMENT_CATEGORY | self.CONTRACT_DOCUMENT_CATEGORY:
return "doc-full"
case self.SCREENING_STATUS | self.META_TERM:
return "tag"
case self.ROUND:
return "repeat"
case self.SEALED_ROUND:
return "lock"
case self.PROJECT:
return "folder-open-1"
case self.CATEGORY:
return "list-ul"
case self.APPLY:
return "folder-inverse"

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

0 comments on commit fccca82

Please sign in to comment.