mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-06-08 23:52:51 +02:00
feat(app): allow changing date and datetime format as a user setting
This commit is contained in:
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user