Skip to content

Commit

Permalink
Start admin date ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
davegaeddert committed Jan 11, 2024
1 parent cb6cb06 commit a101fc5
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 80 deletions.
4 changes: 2 additions & 2 deletions bolt-admin/bolt/admin/cards/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .base import Card
from .charts import ChartCard, TrendCard
from .charts import ChartCard, DailyTrendCard

__all__ = [
"Card",
"ChartCard",
"TrendCard",
"DailyTrendCard",
]
14 changes: 13 additions & 1 deletion bolt-admin/bolt/admin/cards/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum

from bolt import jinja
from bolt.admin.dates import DatetimeRange, DatetimeRangeAliases
from bolt.utils.text import slugify


Expand All @@ -26,7 +27,18 @@ class Sizes(Enum):
link: str = ""
number: int | None = None

def render(self, request):
# All cards can utilize a date range
# which by default is the range of the page it's on
fixed_datetime_range: DatetimeRangeAliases | DatetimeRange | None = None

def render(self, request, datetime_range):
if self.fixed_datetime_range:
self.datetime_range = DatetimeRangeAliases.to_range(
self.fixed_datetime_range
)
# If fixed, show that on the card too (I guess you could use description for this)
else:
self.datetime_range = datetime_range
template = jinja.environment.get_template(self.template_name)
return template.render(self.get_context())

Expand Down
76 changes: 9 additions & 67 deletions bolt-admin/bolt/admin/cards/charts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import datetime
from datetime import timedelta
from enum import Enum

from bolt.utils import timezone
from bolt.utils.functional import cached_property

from .base import Card

Expand All @@ -20,76 +15,23 @@ def get_chart_data(self) -> dict:
raise NotImplementedError


class DateRange:
def __init__(self, start, end):
self.start = start
self.end = end

if isinstance(self.start, datetime.datetime):
self.start = self.start.date()

if isinstance(self.end, datetime.datetime):
self.end = self.end.date()

def days(self):
return (self.end - self.start).days

def __iter__(self):
return iter(self.start + timedelta(days=i) for i in range(0, self.days()))

def __repr__(self):
return f"DateRange({self.start}, {self.end})"

def __str__(self):
return f"{self.start} to {self.end}"

def __eq__(self, other):
return self.start == other.start and self.end == other.end

def __hash__(self):
return hash((self.start, self.end))

def __contains__(self, item):
return self.start <= item <= self.end


class TrendCard(ChartCard):
class Ranges(Enum):
LAST_365_DAYS = "last_365_days"
LAST_30_DAYS = "last_30_days"
LAST_7_DAYS = "last_7_days"

default_range: Ranges = Ranges.LAST_30_DAYS

class DailyTrendCard(ChartCard):
def get_description(self) -> str:
return str(self.date_range)

@cached_property
def date_range(self) -> DateRange:
return self.get_date_range()

def get_date_range(self) -> DateRange:
now = timezone.now()

if self.default_range == self.Ranges.LAST_365_DAYS:
return DateRange(now - timedelta(days=365), now)

if self.default_range == self.Ranges.LAST_30_DAYS:
return DateRange(now - timedelta(days=30), now)

if self.default_range == self.Ranges.LAST_7_DAYS:
return DateRange(now - timedelta(days=7), now)

raise ValueError(f"Invalid range: {self.default_range}")
return str(self.datetime_range)

def get_values(self) -> dict[datetime.date, int]:
raise NotImplementedError

def get_chart_data(self) -> dict:
date_labels = [date.strftime("%Y-%m-%d") for date in self.date_range]
date_labels = [date.strftime("%Y-%m-%d") for date in self.datetime_range]
date_values = self.get_values()
# Convert all to dates
# date_values = {
# date.date() if isinstance(date, datetime.datetime) else date: value
# for date, value in date_values.items()
# }

for date in self.date_range:
for date in self.datetime_range:
if date not in date_values:
date_values[date] = 0

Expand Down
72 changes: 72 additions & 0 deletions bolt-admin/bolt/admin/dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import datetime
from enum import Enum

from bolt.utils import timezone


class DatetimeRangeAliases(Enum):
LAST_365_DAYS = "last_365_days"
LAST_30_DAYS = "last_30_days"
LAST_7_DAYS = "last_7_days"
INHERIT = "inherit"

@classmethod
def to_range(cls, value: str) -> (datetime.datetime, datetime.datetime):
now = timezone.localtime()
if value == cls.LAST_365_DAYS:
return DatetimeRange(now - datetime.timedelta(days=365), now)
if value == cls.LAST_30_DAYS:
return DatetimeRange(now - datetime.timedelta(days=30), now)
if value == cls.LAST_7_DAYS:
return DatetimeRange(now - datetime.timedelta(days=7), now)
raise ValueError(f"Invalid range: {value}")


class DatetimeRange:
def __init__(self, start, end):
self.start = start
self.end = end

if isinstance(self.start, str) and self.start:
self.start = datetime.datetime.fromisoformat(self.start)

if isinstance(self.end, str) and self.end:
self.end = datetime.datetime.fromisoformat(self.end)

if isinstance(self.start, datetime.date):
self.start = timezone.localtime().replace(
year=self.start.year, month=self.start.month, day=self.start.day
)

if isinstance(self.end, datetime.date):
self.end = timezone.localtime().replace(
year=self.end.year, month=self.end.month, day=self.end.day
)

def as_tuple(self):
return (self.start, self.end)

def total_days(self):
return (self.end - self.start).days

def __iter__(self):
# Iters days currently... probably should have an iter_days method instead
return iter(
self.start.date() + datetime.timedelta(days=i)
for i in range(0, self.total_days())
)

def __repr__(self):
return f"DatetimeRange({self.start}, {self.end})"

def __str__(self):
return f"{self.start} to {self.end}"

def __eq__(self, other):
return self.start == other.start and self.end == other.end

def __hash__(self):
return hash((self.start, self.end))

def __contains__(self, item):
return self.start <= item <= self.end
27 changes: 19 additions & 8 deletions bolt-admin/bolt/admin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,30 @@ <h1 class="text-2xl font-semibold text-stone-700">
</div>
</div>

<div class="px-4 py-6 lg:px-8">
{% if cards %}
<div class="grid grid-cols-1 gap-6 mb-6 sm:grid-cols-2 lg:grid-cols-4">
{% for card in cards %}

{% if cards %}
<div class="px-4 mt-8 lg:px-8">
<div class="flex justify-end">
<div class="flex items-center">
<form method="GET">
<span class="text-xs text-gray-600">{{ time_zone }}</span>
<input class="text-sm border-gray-200 rounded-md" type="datetime-local" name="from" value="{{ from_datetime|strftime('%Y-%m-%dT%H:%M') }}" >
<input class="text-sm border-gray-200 rounded-md" type="datetime-local" name="to" value="{{ to_datetime|strftime('%Y-%m-%dT%H:%M') }}" >
<button type="submit" class="px-2 py-1 text-sm border border-gray-200 rounded-md">Go</button>
</form>
</div>
</div>
<div class="grid grid-cols-1 gap-6 mt-4 mb-6 sm:grid-cols-2 lg:grid-cols-4">
{% for card in cards %}
<admin.Card size=card.size.value>
{{ render_card(card)|safe }}
</admin.Card>
{% endfor %}
{% endfor %}
</div>
{% endif %}

<main>{% block content %}{% endblock %}</main>
</div>
{% endif %}

<main class="px-4 py-6 lg:px-8">{% block content %}{% endblock %}</main>
</div>

<!-- TODO only if installed -->
Expand Down
10 changes: 9 additions & 1 deletion bolt-admin/bolt/admin/templates/admin/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
<div>
<div class="flex items-center justify-between pb-2 border-b border-stone-200">
<div class="font-semibold">
{{ title }}
{% if table_style == "simple" %}
{{ title }}
{% else %}
{% if page.has_other_pages() %}
Page {{ page.number }} of {{ page.paginator.num_pages }} ({{ page.paginator.count }} results)
{% else %}
Showing all {{ page.paginator.count }} results
{% endif %}
{% endif %}
</div>
<div class="flex space-x-5">
{% if table_style == "simple" %}
Expand Down
16 changes: 15 additions & 1 deletion bolt-admin/bolt/admin/views/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import TYPE_CHECKING

from bolt.admin.dates import DatetimeRange, DatetimeRangeAliases
from bolt.db import models
from bolt.htmx.views import HTMXViewMixin
from bolt.http import HttpResponse, HttpResponseRedirect
from bolt.paginator import Paginator
from bolt.urls import reverse
from bolt.utils import timezone
from bolt.utils.text import slugify
from bolt.views import (
AuthViewMixin,
Expand Down Expand Up @@ -46,6 +48,8 @@ class AdminView(AuthViewMixin, TemplateView):
template_name = "admin/page.html"
cards: list["Card"] = []

default_datetime_range = DatetimeRangeAliases.LAST_365_DAYS

def get_context(self):
context = super().get_context()
context["title"] = self.get_title()
Expand All @@ -56,8 +60,18 @@ def get_context(self):
context["admin_registry"] = registry
context["cards"] = self.get_cards()
context["render_card"] = self.render_card
context["from_datetime"] = self.datetime_range.start
context["to_datetime"] = self.datetime_range.end
context["time_zone"] = timezone.get_current_timezone_name()
return context

def get_response(self):
default_range = DatetimeRangeAliases.to_range(self.default_datetime_range)
from_datetime = self.request.GET.get("from", default_range.start)
to_datetime = self.request.GET.get("to", default_range.end)
self.datetime_range = DatetimeRange(from_datetime, to_datetime)
return super().get_response()

@classmethod
def view_name(cls) -> str:
return f"view_{cls.get_slug()}"
Expand Down Expand Up @@ -113,7 +127,7 @@ def render_card(self, card: "Card"):
# response = card.as_view()(self.request)
# response.render()
# content = response.content.decode()
return card().render(self.request)
return card().render(self.request, self.datetime_range)


class AdminListView(HTMXViewMixin, AdminView):
Expand Down

0 comments on commit a101fc5

Please sign in to comment.