-
Notifications
You must be signed in to change notification settings - Fork 64
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
base: main
Are you sure you want to change the base?
Interval field #573
Changes from all commits
84875df
ca0ae15
8b3d29b
264733b
eb07d2d
e7cf9a7
410b1a4
ab67a81
8e45cfc
d347bf7
6ca6505
86594e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 ( | ||||||
|
@@ -18,10 +18,16 @@ | |||||
Union, | ||||||
) | ||||||
|
||||||
from babel.dates import format_timedelta | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to make this optional and require it only when using the Interval field. here is an example: starlette-admin/starlette_admin/fields.py Line 863 in 5b183e0
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example is not clear to me. Is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the user is not able to use it if the import fails, but they are required to install it before using the field. The idea is to make the arrow package optional, so users who don't want to use ArrowField are not forced to install it. |
||||||
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, | ||||||
|
@@ -1267,3 +1273,109 @@ 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(StringField): | ||||||
form_template: str = "forms/interval.html" | ||||||
|
||||||
async def parse_form_data( | ||||||
self, request: Request, form_data: FormData, action: RequestAction | ||||||
) -> Any: | ||||||
try: | ||||||
timedelta_params = { | ||||||
"weeks": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_weeks") == "" | ||||||
else int(form_data.get(f"{self.id}_weeks")) # type: ignore | ||||||
), | ||||||
"days": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_days") == "" | ||||||
else int(form_data.get(f"{self.id}_days")) # type: ignore | ||||||
), | ||||||
"hours": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_hours") == "" | ||||||
else int(form_data.get(f"{self.id}_hours")) # type: ignore | ||||||
), | ||||||
"minutes": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_minutes") == "" | ||||||
else int(form_data.get(f"{self.id}_minutes")) # type: ignore | ||||||
), | ||||||
"seconds": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_seconds") == "" | ||||||
else int(form_data.get(f"{self.id}_seconds")) # type: ignore | ||||||
), | ||||||
"microseconds": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_microseconds") == "" | ||||||
else int(form_data.get(f"{self.id}_microseconds")) # type: ignore | ||||||
), | ||||||
"milliseconds": ( | ||||||
0 | ||||||
if form_data.get(f"{self.id}_milliseconds") == "" | ||||||
else int(form_data.get(f"{self.id}_milliseconds")) # type: ignore | ||||||
), | ||||||
} | ||||||
return timedelta(**timedelta_params) | ||||||
except ValueError: | ||||||
return timedelta() | ||||||
|
||||||
async def serialize_value( | ||||||
self, request: Request, value: Any, action: RequestAction | ||||||
) -> Any: | ||||||
params = timedelta_to_components(value) | ||||||
if action != RequestAction.EDIT: | ||||||
string = ( | ||||||
format_timedelta( | ||||||
timedelta(weeks=params["weeks"]), | ||||||
granularity="week", | ||||||
threshold=params["weeks"], | ||||||
locale="en", | ||||||
) | ||||||
+ " " | ||||||
if params["weeks"] > 0 | ||||||
else "" | ||||||
) | ||||||
string += ( | ||||||
format_timedelta( | ||||||
timedelta(days=params["days"]), | ||||||
granularity="day", | ||||||
threshold=1, | ||||||
locale="en", | ||||||
) | ||||||
+ " " | ||||||
) | ||||||
string += ( | ||||||
format_timedelta( | ||||||
timedelta(hours=params["hours"]), | ||||||
granularity="hour", | ||||||
threshold=1, | ||||||
locale="en", | ||||||
) | ||||||
+ " " | ||||||
) | ||||||
string += ( | ||||||
format_timedelta( | ||||||
timedelta(minutes=params["minutes"]), | ||||||
granularity="minute", | ||||||
threshold=1, | ||||||
locale="en", | ||||||
) | ||||||
+ " " | ||||||
) | ||||||
string += format_timedelta( | ||||||
timedelta( | ||||||
seconds=params["seconds"], | ||||||
milliseconds=params["milliseconds"], | ||||||
microseconds=params["microseconds"], | ||||||
), | ||||||
granularity="second", | ||||||
threshold=1, | ||||||
locale="en", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function returns the current user locale
Suggested change
|
||||||
) | ||||||
return string | ||||||
return params |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<fieldset class="form-fieldset"> | ||
<div class="{% if error %}{{ field.error_class }}{% endif %} form-group row mb-3"> | ||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_weeks">Weeks:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_days">Days:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_hours">Hours:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_minutes">Minutes:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_seconds">Seconds:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_milliseconds">Milliseconds:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
|
||
<label class="col-2 col-form-label" | ||
for="{{ field.id }}_microseconds">Microseconds:</label> | ||
<div class="col-2"> | ||
<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 }}/> | ||
</div> | ||
{% if field.help_text %} | ||
<small class="form-hint">{{ field.help_text }}</small> | ||
{% endif %} | ||
</div> | ||
|
||
</fieldset> | ||
{% include "forms/_error.html" %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
from datetime import timedelta | ||
|
||
from starlette_admin.helpers import timedelta_to_components | ||
|
||
|
||
def test_zero_timedelta(): | ||
td = timedelta() | ||
expected = { | ||
"weeks": 0, | ||
"days": 0, | ||
"hours": 0, | ||
"minutes": 0, | ||
"seconds": 0, | ||
"milliseconds": 0, | ||
"microseconds": 0, | ||
} | ||
result = timedelta_to_components(td) | ||
assert result == expected | ||
|
||
|
||
def test_only_days(): | ||
td = timedelta(days=10) | ||
expected = { | ||
"weeks": 1, | ||
"days": 3, | ||
"hours": 0, | ||
"minutes": 0, | ||
"seconds": 0, | ||
"milliseconds": 0, | ||
"microseconds": 0, | ||
} | ||
result = timedelta_to_components(td) | ||
assert result == expected | ||
|
||
|
||
def test_complex_timedelta(): | ||
td = timedelta(days=15, seconds=3661, microseconds=123456) | ||
expected = { | ||
"weeks": 2, | ||
"days": 1, | ||
"hours": 1, | ||
"minutes": 1, | ||
"seconds": 1, | ||
"milliseconds": 123, | ||
"microseconds": 456, | ||
} | ||
result = timedelta_to_components(td) | ||
assert result == expected | ||
|
||
|
||
def test_no_microseconds(): | ||
td = timedelta(days=7, seconds=86399) | ||
expected = { | ||
"weeks": 1, | ||
"days": 0, | ||
"hours": 23, | ||
"minutes": 59, | ||
"seconds": 59, | ||
"milliseconds": 0, | ||
"microseconds": 0, | ||
} | ||
result = timedelta_to_components(td) | ||
assert result == expected | ||
|
||
|
||
def test_negative_timedelta(): | ||
td = timedelta(days=-7, seconds=-86399) | ||
expected = { | ||
"weeks": -2, | ||
"days": 6, | ||
"hours": 0, | ||
"minutes": 0, | ||
"seconds": 1, | ||
"milliseconds": 0, | ||
"microseconds": 0, | ||
} | ||
result = timedelta_to_components(td) | ||
assert result == expected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems odmantic does not support timedelta art049/odmantic#120, we can remove this and just keep for sqlalchemy