feat(app): allow changing date and datetime format as a user setting

This commit is contained in:
Herculino Trotta
2025-01-20 19:35:22 -03:00
parent f68e954bc0
commit 60fe4c9681
21 changed files with 426 additions and 130 deletions
+32
View File
@@ -0,0 +1,32 @@
from django import template
from django.template.defaultfilters import date as date_filter
from django.utils import formats, timezone
register = template.Library()
@register.filter
def custom_date(value, user=None):
if not value:
return ""
# Determine if the value is a datetime or just a date
is_datetime = hasattr(value, "hour")
# Convert to current timezone if it's a datetime
if is_datetime and timezone.is_aware(value):
value = timezone.localtime(value)
if user and user.is_authenticated:
user_settings = user.settings
if is_datetime:
format_setting = user_settings.datetime_format
else:
format_setting = user_settings.date_format
return formats.date_format(value, format_setting, use_l10n=True)
return date_filter(
value, "SHORT_DATE_FORMAT" if not is_datetime else "SHORT_DATETIME_FORMAT"
)
+129
View File
@@ -30,3 +30,132 @@ def django_to_python_datetime(django_format):
python_format = python_format.replace(django_code, python_code)
return python_format
def django_to_airdatepicker_datetime(django_format):
format_map = {
# Time
"h": "h", # Hour (12-hour)
"H": "H", # Hour (24-hour)
"i": "m", # Minutes
"A": "AA", # AM/PM uppercase
"a": "aa", # am/pm lowercase
"P": "h:mm AA", # Localized time format (e.g., "2:30 PM")
# Date
"D": "E", # Short weekday name
"l": "EEEE", # Full weekday name
"j": "d", # Day of month without leading zero
"d": "dd", # Day of month with leading zero
"n": "M", # Month without leading zero
"m": "MM", # Month with leading zero
"M": "MMM", # Short month name
"F": "MMMM", # Full month name
"y": "yy", # Year, 2 digits
"Y": "yyyy", # Year, 4 digits
}
result = ""
i = 0
while i < len(django_format):
char = django_format[i]
if char == "\\": # Handle escaped characters
if i + 1 < len(django_format):
result += django_format[i + 1]
i += 2
continue
if char in format_map:
result += format_map[char]
else:
result += char
i += 1
return result
def django_to_airdatepicker_datetime_separated(django_format):
format_map = {
# Time formats
"h": "h", # Hour (12-hour)
"H": "H", # Hour (24-hour)
"i": "m", # Minutes
"A": "AA", # AM/PM uppercase
"a": "aa", # am/pm lowercase
"P": "h:mm AA", # Localized time format
# Date formats
"D": "E", # Short weekday name
"l": "EEEE", # Full weekday name
"j": "d", # Day of month without leading zero
"d": "dd", # Day of month with leading zero
"n": "M", # Month without leading zero
"m": "MM", # Month with leading zero
"M": "MMM", # Short month name
"F": "MMMM", # Full month name
"y": "yy", # Year, 2 digits
"Y": "yyyy", # Year, 4 digits
}
# Define which characters belong to time format
time_chars = {"h", "H", "i", "A", "a", "P"}
date_chars = {"D", "l", "j", "d", "n", "m", "M", "F", "y", "Y"}
date_parts = []
time_parts = []
current_part = []
is_time = False
i = 0
while i < len(django_format):
char = django_format[i]
if char == "\\": # Handle escaped characters
if i + 1 < len(django_format):
current_part.append(django_format[i + 1])
i += 2
continue
if char in format_map:
if char in time_chars:
# If we were building a date part, save it and start a time part
if current_part and not is_time:
date_parts.append("".join(current_part))
current_part = []
is_time = True
current_part.append(format_map[char])
elif char in date_chars:
# If we were building a time part, save it and start a date part
if current_part and is_time:
time_parts.append("".join(current_part))
current_part = []
is_time = False
current_part.append(format_map[char])
else:
# Handle separators
if char in "/:.-":
current_part.append(char)
elif char == " ":
if current_part:
if is_time:
time_parts.append("".join(current_part))
else:
date_parts.append("".join(current_part))
current_part = []
current_part.append(char)
i += 1
# Don't forget the last part
if current_part:
if is_time:
time_parts.append("".join(current_part))
else:
date_parts.append("".join(current_part))
date_format = "".join(date_parts)
time_format = "".join(time_parts)
# Clean up multiple spaces while preserving necessary ones
date_format = " ".join(filter(None, date_format.split()))
time_format = " ".join(filter(None, time_format.split()))
return date_format, time_format
+117 -73
View File
@@ -4,7 +4,11 @@ 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
from apps.common.utils.django import (
django_to_python_datetime,
django_to_airdatepicker_datetime,
django_to_airdatepicker_datetime_separated,
)
class AirDatePickerInput(widgets.DateInput):
@@ -14,104 +18,56 @@ class AirDatePickerInput(widgets.DateInput):
format=None,
clear_button=True,
auto_close=True,
user=None,
*args,
**kwargs,
):
attrs = attrs or {}
self.user = user
super().__init__(attrs=attrs, format=format, *args, **kwargs)
self.clear_button = clear_button
self.auto_close = auto_close
def _get_current_language(self):
@staticmethod
def _get_current_language():
"""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 _get_format(self):
"""Get the format string based on user settings or default"""
if self.format:
return self.format
if self.user and hasattr(self.user, "settings"):
user_format = self.user.settings.date_format
print(user_format)
if user_format == "SHORT_DATE_FORMAT":
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
return user_format
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
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
)
attrs["data-date-format"] = django_to_airdatepicker_datetime(self._get_format())
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
)
self.attrs["data-value"] = value
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 formats.date_format(value, format=self._get_format(), use_l10n=True)
return str(value)
@@ -123,8 +79,95 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
# 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
value.strip(),
django_to_python_datetime(self._get_format())
or django_to_python_datetime(get_format("SHORT_DATETIME_FORMAT")),
).strftime("%Y-%m-%d")
except (ValueError, TypeError) as e:
return value
return None
class AirDateTimePickerInput(widgets.DateTimeInput):
def __init__(
self,
attrs=None,
format=None,
timepicker=True,
clear_button=True,
auto_close=True,
user=None,
*args,
**kwargs,
):
attrs = attrs or {}
self.user = user
super().__init__(attrs=attrs, format=format, *args, **kwargs)
self.timepicker = timepicker
self.clear_button = clear_button
self.auto_close = auto_close
@staticmethod
def _get_current_language():
"""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 _get_format(self):
"""Get the format string based on user settings or default"""
if self.format:
return self.format
if self.user and hasattr(self.user, "settings"):
user_format = self.user.settings.datetime_format
if user_format == "SHORT_DATETIME_FORMAT":
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
return user_format
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
date_format, time_format = django_to_airdatepicker_datetime_separated(
self._get_format()
)
# 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"] = date_format
attrs["data-time-format"] = time_format
return attrs
def format_value(self, value):
"""Format the value for display in the widget."""
if value:
self.attrs["data-value"] = datetime.datetime.strftime(
value, "%Y-%m-%d %H:%M:00"
)
if value is None:
return ""
if isinstance(value, (datetime.date, datetime.datetime)):
return formats.date_format(value, format=self._get_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.strip(),
django_to_python_datetime(self._get_format())
or django_to_python_datetime(get_format("SHORT_DATETIME_FORMAT")),
).strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError) as e:
@@ -140,7 +183,8 @@ class AirMonthYearPickerInput(AirDatePickerInput):
# Store the Python format for internal use
self.python_format = "%B %Y"
def _get_month_names(self):
@staticmethod
def _get_month_names():
"""Get month names using Django's date translation"""
return {dates.MONTHS[i]: i for i in range(1, 13)}