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

Interval field #573

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
63 changes: 61 additions & 2 deletions starlette_admin/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import warnings
from dataclasses import asdict, dataclass
from dataclasses import field as dc_field
from datetime import date, datetime, time
from datetime import date, datetime, time, timedelta
from enum import Enum, IntEnum
from json import JSONDecodeError
from typing import (
Expand All @@ -21,7 +21,12 @@
from starlette.datastructures import FormData, UploadFile
from starlette.requests import Request
from starlette_admin._types import RequestAction
from starlette_admin.helpers import extract_fields, html_params, is_empty_file
from starlette_admin.helpers import (
extract_fields,
html_params,
is_empty_file,
timedelta_to_components,
)
from starlette_admin.i18n import (
format_date,
format_datetime,
Expand Down Expand Up @@ -1267,3 +1272,57 @@ def additional_css_links(

def additional_js_links(self, request: Request, action: RequestAction) -> List[str]:
return self.field.additional_js_links(request, action)


@dataclass
class IntervalField(BaseField):
render_function_key: str = "interval"
form_template: str = "forms/interval.html"
display_template: str = "displays/interval.html"

async def parse_form_data(
self, request: Request, form_data: FormData, action: RequestAction
) -> Any:
timedelta_params = {
"weeks": (
0
if form_data.get(f"{self.name}_weeks") == ""
regisin marked this conversation as resolved.
Show resolved Hide resolved
else int(form_data.get(f"{self.name}_weeks"))
),
"days": (
0
if form_data.get(f"{self.name}_days") == ""
else int(form_data.get(f"{self.name}_days"))
),
"hours": (
0
if form_data.get(f"{self.name}_hours") == ""
else int(form_data.get(f"{self.name}_hours"))
),
"minutes": (
0
if form_data.get(f"{self.name}_minutes") == ""
else int(form_data.get(f"{self.name}_minutes"))
),
"seconds": (
0
if form_data.get(f"{self.name}_seconds") == ""
else int(form_data.get(f"{self.name}_seconds"))
),
"microseconds": (
0
if form_data.get(f"{self.name}_microseconds") == ""
else int(form_data.get(f"{self.name}_microseconds"))
),
"milliseconds": (
0
if form_data.get(f"{self.name}_milliseconds") == ""
else int(form_data.get(f"{self.name}_milliseconds"))
),
}
return timedelta(**timedelta_params)

async def serialize_value(
self, request: Request, value: Any, action: RequestAction
) -> Any:
return timedelta_to_components(value)
32 changes: 32 additions & 0 deletions starlette_admin/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import re
from datetime import timedelta
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -151,3 +152,34 @@ def not_none(value: Optional[T]) -> T:
if value is not None:
return value
raise ValueError("Value can not be None") # pragma: no cover


def timedelta_to_components(td: timedelta):
regisin marked this conversation as resolved.
Show resolved Hide resolved
# Constants
seconds_in_minute = 60
seconds_in_hour = 3600
seconds_in_week = 604800

# Total seconds in the timedelta
total_seconds = td.total_seconds()

# Calculate each component
weeks = int(total_seconds // seconds_in_week)
total_seconds -= weeks * seconds_in_week

days = td.days % 7
hours = td.seconds // seconds_in_hour
minutes = (td.seconds % seconds_in_hour) // seconds_in_minute
seconds = td.seconds % seconds_in_minute
milliseconds = td.microseconds // 1000
microseconds = td.microseconds % 1000

return {
regisin marked this conversation as resolved.
Show resolved Hide resolved
"weeks": weeks,
"days": days,
"hours": hours,
"minutes": minutes,
"seconds": seconds,
"milliseconds": milliseconds,
"microseconds": microseconds,
}
16 changes: 16 additions & 0 deletions starlette_admin/statics/js/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,20 @@ const render = {
)
.join("")}</div>`;
},
interval: function render(data, type, full, meta, fieldOptions) {
if (!data) return null_column();
if (Array.isArray(data) && data.length == 0) return empty_column();
data = Array.isArray(data) ? data : [data];
if (type != "display") return data.map((d) => new URL(d.url));
regisin marked this conversation as resolved.
Show resolved Hide resolved
return `<div class="d-flex flex-column">${data
.map(
(e) => {
return Object.entries(e)
.filter(([key, value]) => value !== 0) // Filter out components with value 0
.map(([key, value]) => `<span>${value} ${key}</span>`) // Create spans for existing components
.join(''); // Join all the spans into a single string
}
)
.join("")}</div>`;
},
};
7 changes: 7 additions & 0 deletions starlette_admin/templates/displays/interval.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% if 'weeks' in data %}<span>{{data.weeks}} weeks</span>{% endif %}
{% if 'days' in data %}<span>{{data.days}} days</span>{% endif %}
{% if 'hours' in data %}<span>{{data.hours}} hours</span>{% endif %}
{% if 'minutes' in data %}<span>{{data.minutes}} minutes</span>{% endif %}
{% if 'seconds' in data %}<span>{{data.seconds}} seconds</span>{% endif %}
{% if 'milliseconds' in data %}<span>{{data.milliseconds}} milliseconds</span>{% endif %}
{% if 'microseconds' in data %}<span>{{data.microseconds}} microseconds</span>{% endif %}
33 changes: 33 additions & 0 deletions starlette_admin/templates/forms/interval.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div class="{% if error %}{{ field.error_class }}{% endif %}">
Copy link
Owner

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Hey, I'm not sure what to do with these converters..what do they do? I just mimic'd the "Time" one:

    @converts("Interval")
    def conv_interval(self, *args: Any, **kwargs: Any) -> BaseField:
        return IntervalField(
            **self._field_common(*args, **kwargs),
        )

Is that it?

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, that's it. The converter's role is to automatically find the appropriate field for each attribute. For SQLAlchemy, it uses the attribute type. Think of it as a map between the attribute type and the corresponding field.

Copy link
Author

Choose a reason for hiding this comment

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

Updated the pr.

<label for="{{ field.id }}_weeks">Weeks:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_weeks"
name="{{ field.id }}_weeks" value="{{ '' if data is none else data.weeks }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_days">Days:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_days"
name="{{ field.id }}_days" value="{{ '' if data is none else data.days }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_hours">Hours:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_hours"
name="{{ field.id }}_hours" value="{{ '' if data is none else data.hours }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_minutes">Minutes:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_minutes"
name="{{ field.id }}_minutes" value="{{ '' if data is none else data.minutes }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_seconds">Seconds:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_seconds"
name="{{ field.id }}_seconds" value="{{ '' if data is none else data.seconds }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_milliseconds">Milliseconds:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_milliseconds"
name="{{ field.id }}_milliseconds" value="{{ '' if data is none else data.milliseconds }}" {{ field.input_params() | safe }}/>

<label for="{{ field.id }}_microseconds">Microseconds:</label>
<input type="number" class="form-control {% if error %}is-invalid{% endif %}" id="{{ field.id }}_microseconds"
name="{{ field.id }}_microseconds" value="{{ '' if data is none else data.microseconds }}" {{ field.input_params() | safe }}/>
{% if field.help_text %}
<small class="form-hint">{{ field.help_text }}</small>
{% endif %}
</div>
{% include "forms/_error.html" %}