mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-06 21:05:15 +02:00
feat(datepicker): drop native datepickers in favor of AirDatePicker for better compatibility
As Firefox (still) doesn't support month input type
This commit is contained in:
@@ -61,7 +61,6 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
|
||||
self._created_instance = instance
|
||||
return instance
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise ValidationError(
|
||||
self.error_messages["invalid_choice"], code="invalid_choice"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.common.widgets.datepicker import AirMonthYearPickerInput
|
||||
from apps.common.widgets.month_year import MonthYearWidget
|
||||
|
||||
|
||||
@@ -27,7 +29,7 @@ class MonthYearModelField(models.DateField):
|
||||
|
||||
|
||||
class MonthYearFormField(forms.DateField):
|
||||
widget = MonthYearWidget
|
||||
widget = AirMonthYearPickerInput
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -42,7 +44,11 @@ class MonthYearFormField(forms.DateField):
|
||||
date = datetime.datetime.strptime(value, "%Y-%m")
|
||||
return date.replace(day=1).date()
|
||||
except ValueError:
|
||||
raise ValidationError(_("Invalid date format. Use YYYY-MM."))
|
||||
try:
|
||||
date = datetime.datetime.strptime(value, "%Y-%m-%d")
|
||||
return date.replace(day=1).date()
|
||||
except ValueError:
|
||||
raise ValidationError(_("Invalid date format. Use YYYY-MM."))
|
||||
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, datetime.date):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
def django_to_python_datetime(django_format):
|
||||
mapping = {
|
||||
# Day
|
||||
"j": "%d", # Day of the month without leading zeros
|
||||
"d": "%d", # Day of the month with leading zeros
|
||||
"D": "%a", # Day of the week, short version
|
||||
"l": "%A", # Day of the week, full version
|
||||
# Month
|
||||
"n": "%m", # Month without leading zeros
|
||||
"m": "%m", # Month with leading zeros
|
||||
"M": "%b", # Month, short version
|
||||
"F": "%B", # Month, full version
|
||||
# Year
|
||||
"y": "%y", # Year, 2 digits
|
||||
"Y": "%Y", # Year, 4 digits
|
||||
# Time
|
||||
"g": "%I", # Hour (12-hour), without leading zeros
|
||||
"G": "%H", # Hour (24-hour), without leading zeros
|
||||
"h": "%I", # Hour (12-hour), with leading zeros
|
||||
"H": "%H", # Hour (24-hour), with leading zeros
|
||||
"i": "%M", # Minutes
|
||||
"s": "%S", # Seconds
|
||||
"a": "%p", # am/pm
|
||||
"A": "%p", # AM/PM
|
||||
"P": "%I:%M %p",
|
||||
}
|
||||
|
||||
python_format = django_format
|
||||
for django_code, python_code in mapping.items():
|
||||
python_format = python_format.replace(django_code, python_code)
|
||||
|
||||
return python_format
|
||||
@@ -0,0 +1,185 @@
|
||||
import datetime
|
||||
|
||||
from django.forms import widgets
|
||||
from django.utils import formats, translation, dates
|
||||
from django.utils.formats import get_format
|
||||
|
||||
from apps.common.utils.django import django_to_python_datetime
|
||||
|
||||
|
||||
class AirDatePickerInput(widgets.DateInput):
|
||||
def __init__(
|
||||
self,
|
||||
attrs=None,
|
||||
format=None,
|
||||
clear_button=True,
|
||||
auto_close=True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
attrs = attrs or {}
|
||||
|
||||
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||
|
||||
self.clear_button = clear_button
|
||||
self.auto_close = auto_close
|
||||
|
||||
def _get_current_language(self):
|
||||
"""Get current language code in format compatible with AirDatepicker"""
|
||||
lang_code = translation.get_language()
|
||||
# AirDatepicker uses simple language codes
|
||||
return lang_code.split("-")[0]
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||
|
||||
# Add data attributes for AirDatepicker configuration
|
||||
attrs["data-auto-close"] = str(self.auto_close).lower()
|
||||
attrs["data-clear-button"] = str(self.clear_button).lower()
|
||||
attrs["data-language"] = self._get_current_language()
|
||||
attrs["data-date-format"] = self.format or get_format(
|
||||
"SHORT_DATE_FORMAT", use_l10n=True
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def format_value(self, value):
|
||||
"""Format the value for display in the widget."""
|
||||
if value:
|
||||
self.attrs["data-value"] = (
|
||||
value # We use this to dynamically select the initial date on AirDatePicker
|
||||
)
|
||||
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
return formats.date_format(
|
||||
value, format=self.format or "SHORT_DATE_FORMAT", use_l10n=True
|
||||
)
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
class AirDateTimePickerInput(widgets.DateTimeInput):
|
||||
def __init__(
|
||||
self,
|
||||
attrs=None,
|
||||
format=None,
|
||||
timepicker=True,
|
||||
clear_button=True,
|
||||
auto_close=True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
attrs = attrs or {}
|
||||
|
||||
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||
|
||||
self.timepicker = timepicker
|
||||
self.clear_button = clear_button
|
||||
self.auto_close = auto_close
|
||||
|
||||
def _get_current_language(self):
|
||||
"""Get current language code in format compatible with AirDatepicker"""
|
||||
lang_code = translation.get_language()
|
||||
# AirDatepicker uses simple language codes
|
||||
return lang_code.split("-")[0]
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||
|
||||
# Add data attributes for AirDatepicker configuration
|
||||
attrs["data-timepicker"] = str(self.timepicker).lower()
|
||||
attrs["data-auto-close"] = str(self.auto_close).lower()
|
||||
attrs["data-clear-button"] = str(self.clear_button).lower()
|
||||
attrs["data-language"] = self._get_current_language()
|
||||
attrs["data-date-format"] = self.format or get_format(
|
||||
"SHORT_DATETIME_FORMAT", use_l10n=True
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def format_value(self, value):
|
||||
"""Format the value for display in the widget."""
|
||||
if value:
|
||||
self.attrs["data-value"] = (
|
||||
value # We use this to dynamically select the initial date on AirDatePicker
|
||||
)
|
||||
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
return formats.date_format(
|
||||
value, format=self.format or "SHORT_DATETIME_FORMAT", use_l10n=True
|
||||
)
|
||||
|
||||
return str(value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""Parse the datetime string from the form data."""
|
||||
value = super().value_from_datadict(data, files, name)
|
||||
if value:
|
||||
try:
|
||||
# This does it's best to convert Django's PHP-Style date format to a datetime format and reformat the
|
||||
# value to be read by Django. Probably could be improved
|
||||
return datetime.datetime.strptime(
|
||||
value,
|
||||
self.format
|
||||
or django_to_python_datetime(get_format("SHORT_DATETIME_FORMAT")),
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError) as e:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
class AirMonthYearPickerInput(AirDatePickerInput):
|
||||
def __init__(self, attrs=None, format=None, *args, **kwargs):
|
||||
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||
# Store the display format for AirDatepicker
|
||||
self.display_format = "MMMM yyyy"
|
||||
# Store the Python format for internal use
|
||||
self.python_format = "%B %Y"
|
||||
|
||||
def _get_month_names(self):
|
||||
"""Get month names using Django's date translation"""
|
||||
return {dates.MONTHS[i]: i for i in range(1, 13)}
|
||||
|
||||
def format_value(self, value):
|
||||
"""Format the value for display in the widget."""
|
||||
if value:
|
||||
self.attrs["data-value"] = (
|
||||
value # We use this to dynamically select the initial date on AirDatePicker
|
||||
)
|
||||
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = datetime.datetime.strptime(value, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return value
|
||||
if isinstance(value, (datetime.datetime, datetime.date)):
|
||||
# Use Django's date translation
|
||||
month_name = dates.MONTHS[value.month]
|
||||
return f"{month_name} {value.year}"
|
||||
return value
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""Convert the value from the widget format back to a format Django can handle."""
|
||||
value = super().value_from_datadict(data, files, name)
|
||||
if value:
|
||||
try:
|
||||
# Split the value into month name and year
|
||||
month_str, year_str = value.rsplit(" ", 1)
|
||||
year = int(year_str)
|
||||
|
||||
# Get month number from translated month name
|
||||
month_names = self._get_month_names()
|
||||
month = month_names.get(month_str)
|
||||
|
||||
if month and year:
|
||||
# Return the first day of the month in Django's expected format
|
||||
return datetime.date(year, month, 1).strftime("%Y-%m-%d")
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
return None
|
||||
@@ -6,9 +6,10 @@ from django.forms import CharField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.datepicker import AirDateTimePickerInput
|
||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||
from apps.currencies.models import Currency, ExchangeRate
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.currencies.models import Currency, ExchangeRate
|
||||
|
||||
|
||||
class CurrencyForm(forms.ModelForm):
|
||||
@@ -64,9 +65,10 @@ class CurrencyForm(forms.ModelForm):
|
||||
|
||||
class ExchangeRateForm(forms.ModelForm):
|
||||
date = forms.DateTimeField(
|
||||
widget=forms.DateTimeInput(
|
||||
attrs={"type": "datetime-local"}, format="%Y-%m-%dT%H:%M"
|
||||
)
|
||||
widget=AirDateTimePickerInput(
|
||||
clear_button=False,
|
||||
),
|
||||
label=_("Date"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from crispy_forms.bootstrap import FormActions
|
||||
from django import forms
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Row, Column
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.datepicker import AirDatePickerInput
|
||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.dca.models import DCAStrategy, DCAEntry
|
||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
|
||||
|
||||
class DCAStrategyForm(forms.ModelForm):
|
||||
@@ -61,7 +62,7 @@ class DCAEntryForm(forms.ModelForm):
|
||||
"notes",
|
||||
]
|
||||
widgets = {
|
||||
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"date": AirDatePickerInput(clear_button=False),
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from django_filters import Filter
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.common.fields.month_year import MonthYearFormField
|
||||
from apps.common.widgets.datepicker import AirDatePickerInput
|
||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||
from apps.common.widgets.tom_select import TomSelectMultiple
|
||||
from apps.currencies.models import Currency
|
||||
@@ -87,13 +88,13 @@ class TransactionsFilter(django_filters.FilterSet):
|
||||
date_start = django_filters.DateFilter(
|
||||
field_name="date",
|
||||
lookup_expr="gte",
|
||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
widget=AirDatePickerInput(),
|
||||
label=_("Date from"),
|
||||
)
|
||||
date_end = django_filters.DateFilter(
|
||||
field_name="date",
|
||||
lookup_expr="lte",
|
||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
widget=AirDatePickerInput(),
|
||||
label=_("Until"),
|
||||
)
|
||||
reference_date_start = MonthYearFilter(
|
||||
|
||||
@@ -16,10 +16,11 @@ from apps.common.fields.forms.dynamic_select import (
|
||||
DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from apps.common.fields.month_year import MonthYearFormField
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
|
||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
from apps.transactions.models import (
|
||||
Transaction,
|
||||
TransactionCategory,
|
||||
@@ -28,7 +29,6 @@ from apps.transactions.models import (
|
||||
RecurringTransaction,
|
||||
TransactionEntity,
|
||||
)
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
@@ -59,7 +59,14 @@ class TransactionForm(forms.ModelForm):
|
||||
label=_("Account"),
|
||||
widget=TomSelect(clear_button=False, group_by="group"),
|
||||
)
|
||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
||||
|
||||
date = forms.DateField(
|
||||
widget=AirDatePickerInput(clear_button=False), label=_("Date")
|
||||
)
|
||||
|
||||
reference_date = forms.DateField(
|
||||
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
@@ -77,7 +84,6 @@ class TransactionForm(forms.ModelForm):
|
||||
"entities",
|
||||
]
|
||||
widgets = {
|
||||
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
"account": TomSelect(clear_button=False, group_by="group"),
|
||||
}
|
||||
@@ -118,8 +124,8 @@ class TransactionForm(forms.ModelForm):
|
||||
css_class="form-row",
|
||||
),
|
||||
Row(
|
||||
Column("date", css_class="form-group col-md-6 mb-0"),
|
||||
Column("reference_date", css_class="form-group col-md-6 mb-0"),
|
||||
Column(Field("date"), css_class="form-group col-md-6 mb-0"),
|
||||
Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"),
|
||||
css_class="form-row",
|
||||
),
|
||||
"description",
|
||||
@@ -235,10 +241,13 @@ class TransferForm(forms.Form):
|
||||
)
|
||||
|
||||
date = forms.DateField(
|
||||
label=_("Date"),
|
||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
widget=AirDatePickerInput(clear_button=False), label=_("Date")
|
||||
)
|
||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
||||
|
||||
reference_date = forms.DateField(
|
||||
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||
)
|
||||
|
||||
description = forms.CharField(max_length=500, label=_("Description"))
|
||||
notes = forms.CharField(
|
||||
required=False,
|
||||
@@ -404,7 +413,10 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
queryset=TransactionEntity.objects.filter(active=True),
|
||||
)
|
||||
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
||||
|
||||
reference_date = forms.DateField(
|
||||
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InstallmentPlan
|
||||
@@ -424,10 +436,10 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
"entities",
|
||||
]
|
||||
widgets = {
|
||||
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"account": TomSelect(),
|
||||
"recurrence": TomSelect(clear_button=False),
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
"start_date": AirDatePickerInput(clear_button=False),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -646,7 +658,6 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
queryset=TransactionEntity.objects.filter(active=True),
|
||||
)
|
||||
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
||||
|
||||
class Meta:
|
||||
model = RecurringTransaction
|
||||
@@ -666,8 +677,9 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
"entities",
|
||||
]
|
||||
widgets = {
|
||||
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"end_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"start_date": AirDatePickerInput(clear_button=False),
|
||||
"end_date": AirDatePickerInput(),
|
||||
"reference_date": AirMonthYearPickerInput(),
|
||||
"recurrence_type": TomSelect(clear_button=False),
|
||||
"notes": forms.Textarea(
|
||||
attrs={
|
||||
|
||||
Reference in New Issue
Block a user