from __future__ import annotations
from typing import TYPE_CHECKING, Final
from wildewidgets import (
ActionButtonModelTable,
BasicModelTable,
Block,
CardWidget,
CrispyFormModalWidget,
CrispyFormWidget,
Datagrid,
FormButton,
HorizontalLayoutBlock,
Link,
ListModelWidget,
ModalButton,
RowActionButton,
RowEditButton,
RowModelUrlButton,
ToggleableManyToManyFieldBlock,
TwoColumnLayout,
Widget,
WidgetListLayoutHeader,
)
from ..forms import (
ProjectCreateForm,
ProjectRelatedLinkCreateForm,
ProjectRelatedLinkUpdateForm,
)
from ..models import Classifier, Project, ProjectRelatedLink, Version
from .classifier import ClassifierFilterBlock
if TYPE_CHECKING:
from django.contrib.auth.models import AbstractUser
from django.db.models import Model, QuerySet
# ------------------------------------------------------
# Modals
# ------------------------------------------------------
# ------------------------------------------------------
# Project related widgets
# ------------------------------------------------------
class ProjectInfoWidget(CardWidget):
"""
A :py:class:`wildewidgets.CardWidget` containing a Tabler datagrid
that gives information about a :py:class:`sphinx_hosting.models.Project`.
"""
title: str = "Project Info"
icon: str = "info-square"
def __init__(self, project: Project, **kwargs):
super().__init__(**kwargs)
grid = Datagrid()
grid.add_item(title="Machine Name", content=project.machine_name)
grid.add_item(
title="Created", content=project.created.strftime("%Y-%m-%d %H:%M %Z")
)
grid.add_item(
title="Last Modified",
content=project.modified.strftime("%Y-%m-%d %H:%M %Z"),
)
self.set_widget(grid)
class ProjectClassifierSelectorWidget(ToggleableManyToManyFieldBlock):
"""
An editable listing of :py:class:`sphinx_hosting.models.Classifier`
objects for a particular :py:class:`sphinx_hosting.models.Project`.
It is used in the project edit view.
"""
model: type[Model] = Project
field_name: str = "classifiers"
title: str = "Classifiers"
icon: str = "collection"
class ProjectClassifierListWidget(ListModelWidget):
"""
A :py:class:`wildewidgets.ListModelWidget` that renders a list of
:py:class:`sphinx_hosting.models.Classifier` objects. It is used in the
project detail view as a read-only list of classifiers.
The editable version of this widget is
:py:class:`ProjectClassifierSelectorWidget`.
"""
paginate_by: int = 100
item_label: str = "Classifier"
title = "Classifiers"
icon = "collection"
def get_object_text(self, instance: Classifier) -> str:
return instance.name
def get_model_subblock(self, instance: Classifier) -> Block:
return Block(self.get_object_text(instance), tag="label")
# ------------------------------------------------------
# ProjectRelatedLink related widgets
# ------------------------------------------------------
# ------------------------------------------------------
# Datatables
# ------------------------------------------------------
class LatestVersionButton(RowModelUrlButton):
attribute: str = "get_latest_version_url"
text: str = "Read Docs"
color: str = "orange"
def is_visible(self, row: Project, user: AbstractUser) -> bool:
if row.latest_version is None:
return False
return super().is_visible(row, user)
[docs]class ProjectTable(ActionButtonModelTable):
"""
Displays a `dataTable <https://datatables.net>`_ of our
:py:class:`sphinx_hosting.models.Project` instances.
It's used as a the main widget in by :py:class:`ProjectTableWidget`.
"""
model: type[Model] = Project
#: Show this many rows per page
page_length: int = 25
#: Set to ``True`` to stripe our table rows
striped: bool = True
default_action_button_label: str = "Edit"
default_action_button_color_class: str = "outline-secondary"
#: A list of fields that we will list as columns. These are either fields
#: on our :py:attr:`model`, or defined as ``render_FIELD_NAME_column`` methods
#: on this class
fields: Final[list[str]] = [
"title",
"machine_name",
"classifiers",
"latest_version",
"description",
"latest_version_date",
]
#: A list of names of columns to hide by default.
hidden: Final[list[str]] = [
"classifiers",
"machine_name",
"latest_version_date",
]
#: A list of names of columns that will will not be sortable
#: (i.e. clicking on the column header will not sort the table)
unsortable: Final[list[str]] = [
"latest_version",
"latest_version_date",
]
#: A list of names of columns that will will not be searched when doing a
#: **global** search
unsearchable: Final[list[str]] = [
"classifiers",
"latest_version",
"latest_version_date",
]
#: A dict of column name to column label. We use it to override the
#: default labels for the named columns
verbose_names: Final[dict[str, str]] = {
"title": "Project Name",
"machine_name": "Machine Name",
"latest_version": "Latest Version",
"latest_version_date": "Import Date",
}
#: A dict of column names to alignment ("left", "right", "center")
alignment: Final[dict[str, str]] = {
"title": "left",
"description": "left",
"classifiers": "left",
"machine_name": "left",
"latest_version": "left",
"latest_version_date": "left",
}
actions: Final[list[RowActionButton]] = [
LatestVersionButton(),
RowModelUrlButton(
attribute="get_absolute_url", text="View", color="outline-secondary"
),
RowEditButton(permission="sphinxhostingcore.change_project", color="azure"),
]
[docs] def render_latest_version_column(self, row: Project, _: str) -> str:
"""
Render our ``latest_version`` column. This is the version string of the
:py:class:`sphinx_hosting.models.Version` that has the most recent
:py:attr:`sphinx_hosting.models.Version.modified` timestamp.
If there are not yet any :py:class:`sphinx_hosting.models.Version` instances for
this project, return empty string.
Args:
row: the ``Project`` we are rendering
colunn: the name of the column to render
Returns:
The version string of the most recently published version, or empty
string.
"""
version = row.latest_version
if version:
return version.version # type: ignore[attr-defined]
return ""
[docs] def render_latest_version_date_column(self, row: Project, _: str) -> str:
"""
Render our ``latest_version_date`` column. This is the last modified
date of the :py:class:`sphinx_hosting.models.Version` that has the most
recent :py:attr:`sphinx_hosting.models.Version.modified` timestamp.
If there are not yet any :py:class:`sphinx_hosting.models.Version`
instances for this project, return empty string.
Args:
row: the ``Project`` we are rendering
colunn: the name of the column to render
Returns:
The of the most recently published version, or empty
string.
"""
version = row.latest_version
if version:
return self.render_datetime_type_column(version.modified) # type: ignore[attr-defined]
return ""
[docs] def render_classifiers_column(self, row: Project, _: str) -> str:
"""
Render our ``classifiers`` column.
Args:
row: the ``Project`` we are rendering
colunn: the name of the column to render
Returns:
A ``<br>`` separated list of classifier names
"""
return "<br>".join(row.classifiers.values_list("name", flat=True)) # type: ignore[attr-defined]
[docs] def filter_classifiers_column(self, qs: QuerySet, _: str, value: str) -> QuerySet:
"""
Filter our results by the ``value``, a comma separated list of
:py:class:`sphinx_hosting.models.Classifier` names.
Args:
qs: the current :py:class:`QuerySet`
colunn: the name of the column to filter on
value: a comma-separated list of classifier names
Returns:
A :py:class:`QuerySet` filtered for rows that contain the selected
classifiers.
"""
classifier_ids = value.split(",")
return qs.filter(classifiers__id__in=classifier_ids).distinct()
[docs]class ProjectVersionTable(BasicModelTable):
"""
Displays a `dataTable <https://datatables.net>`_ of our
:py:class:`sphinx_hosting.models.Version` instances for a particular
:py:class:`sphinx_hosting.models.Project`.
It's used as a the main widget in by :py:class:`ProjectVersionTableWidget`.
"""
model: type[Model] = Version
#: Show this many rows per page
page_length: int = 25
#: Set to ``True`` to stripe our table rows
striped: bool = True
actions: bool = True
#: A list of fields that we will list as columns. These are either fields
#: on our :py:attr:`model`, or defined as ``render_FIELD_NAME_column`` methods
#: on this class
fields: Final[list[str]] = [
"version",
"num_pages",
"num_images",
"created",
"modified",
]
unsortable: Final[list[str]] = [
"num_pages",
"num_images",
]
#: A list of names of columns that will will not be searched when doing a
#: **global** search
unsearchable: Final[list[str]] = [
"num_pages",
"num_images",
"created",
"modified",
]
#: A dict of column name to column label. We use it to override the
#: default labels for the named columns
verbose_names: Final[dict[str, str]] = {
"title": "Version",
"num_pages": "# Pages",
"num_images": "# Images",
}
#: A dict of column names to alignment ("left", "right", "center")
alignment: Final[dict[str, str]] = {
"version": "left",
"num_pages": "right",
"num_images": "right",
"created": "left",
"modified": "left",
}
#: Sort so that the highest version number is on top
sort_ascending: bool = False
[docs] def __init__(self, *args, **kwargs) -> None:
"""
One of our ``kwargs`` must be ``project_id``, the ``pk`` of the
:py:class:`sphinx_hosting.models.Project` for which we want to list
:py:class:`sphinx_hosting.models.Version` objects.
This will get added to the :py:attr:`extra_data` dict in the ``kwargs``
key, from which we reference it.
"""
#: The pk of the :py:class:`sphinx_hosting.models.Project` for which to
#: list versions
self.project_id: int | None = kwargs.get("project_id")
super().__init__(*args, **kwargs)
if "project_id" in self.extra_data["kwargs"]:
self.project_id = int(self.extra_data["kwargs"]["project_id"])
[docs] def get_initial_queryset(self) -> QuerySet[Version]:
"""
Filter our :py:class:`sphinx_hosting.models.Version` objects by
:py:attr:`project_id`.
Returns:
A filtered :py:class:`QuerySet` on :py:class:`sphinx_hosting.models.Version`
"""
return (
super()
.get_initial_queryset()
.filter(project_id=self.project_id)
.order_by("-version")
)
[docs] def render_num_pages_column(self, row: Version, _: str) -> str:
"""
Render our ``num_pages`` column. This is the number of
:py:class:`sphinx_hosting.models.SphinxPage` objects imported for this
version.
Args:
row: the ``Version`` we are rendering
colunn: the name of the column to render
Returns:
The number of pages for this version.
"""
return row.pages.count()
[docs] def render_num_images_column(self, row: Version, _: str) -> str:
"""
Render our ``num_images`` column. This is the number of
:py:class:`sphinx_hosting.models.SphinxImage` objects imported for this
version.
Args:
row: the ``Version`` we are rendering
colunn: the name of the column to render
Returns:
The number of images for this version.
"""
return row.images.count()