mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-03 16:17:51 +00:00
221 lines
8.1 KiB
Python
221 lines
8.1 KiB
Python
"""
|
|
Jinja2 TimeExtension - Custom date/time handling for templates.
|
|
|
|
This extension provides the {% now %} tag for Jinja2 templates, offering timezone-aware
|
|
date/time formatting with support for time offsets.
|
|
|
|
Why This Extension Exists:
|
|
The Arrow library has a now() function (arrow.now()), but Jinja2 templates cannot
|
|
directly call Python functions - they need extensions or filters to expose functionality.
|
|
|
|
This TimeExtension serves as a Jinja2-to-Arrow bridge that:
|
|
|
|
1. Makes Arrow accessible in templates - Jinja2 requires registering functions/tags
|
|
through extensions. You cannot use arrow.now() directly in a template.
|
|
|
|
2. Provides template-friendly syntax - Instead of complex Python code, you get clean tags:
|
|
{% now 'UTC' %}
|
|
{% now 'UTC' + 'hours=2' %}
|
|
{% now 'Europe/London', '%Y-%m-%d' %}
|
|
|
|
3. Adds convenience features on top of Arrow:
|
|
- Default timezone from environment variable (TZ) or config
|
|
- Default datetime format configuration
|
|
- Offset syntax parsing: 'hours=2,minutes=30' → shift(hours=2, minutes=30)
|
|
- Empty string timezone support to use configured defaults
|
|
|
|
4. Maintains security - Works within Jinja2's sandboxed environment so users
|
|
cannot access arbitrary Python code or objects.
|
|
|
|
Essentially, this is a Jinja2 wrapper around arrow.now() and arrow.shift() that
|
|
provides user-friendly template syntax while maintaining security.
|
|
|
|
Basic Usage:
|
|
{% now 'UTC' %}
|
|
# Output: Wed, 09 Dec 2015 23:33:01
|
|
|
|
Custom Format:
|
|
{% now 'UTC', '%Y-%m-%d %H:%M:%S' %}
|
|
# Output: 2015-12-09 23:33:01
|
|
|
|
Timezone Support:
|
|
{% now 'America/New_York' %}
|
|
{% now 'Europe/London' %}
|
|
{% now '' %} # Uses default timezone from environment.default_timezone
|
|
|
|
Time Offsets (Addition):
|
|
{% now 'UTC' + 'hours=2' %}
|
|
{% now 'UTC' + 'hours=2,minutes=30' %}
|
|
{% now 'UTC' + 'days=1,hours=2,minutes=15,seconds=10' %}
|
|
|
|
Time Offsets (Subtraction):
|
|
{% now 'UTC' - 'minutes=11' %}
|
|
{% now 'UTC' - 'days=2,minutes=33,seconds=1' %}
|
|
|
|
Time Offsets with Custom Format:
|
|
{% now 'UTC' + 'hours=2', '%Y-%m-%d %H:%M:%S' %}
|
|
# Output: 2015-12-10 01:33:01
|
|
|
|
Weekday Support (for finding next/previous weekday):
|
|
{% now 'UTC' + 'weekday=0' %} # Next Monday (0=Monday, 6=Sunday)
|
|
{% now 'UTC' + 'weekday=4' %} # Next Friday
|
|
|
|
Configuration:
|
|
- Default timezone: Set via TZ environment variable or override environment.default_timezone
|
|
- Default format: '%a, %d %b %Y %H:%M:%S' (can be overridden via environment.datetime_format)
|
|
|
|
Environment Customization:
|
|
from changedetectionio.jinja2_custom import create_jinja_env
|
|
|
|
jinja2_env = create_jinja_env()
|
|
jinja2_env.default_timezone = 'America/New_York' # Override default timezone
|
|
jinja2_env.datetime_format = '%Y-%m-%d %H:%M' # Override default format
|
|
|
|
Supported Offset Parameters:
|
|
- years, months, weeks, days
|
|
- hours, minutes, seconds, microseconds
|
|
- weekday (0=Monday through 6=Sunday, must be integer)
|
|
|
|
Note:
|
|
This extension uses the Arrow library for timezone-aware datetime handling.
|
|
All timezone names should be valid IANA timezone identifiers (e.g., 'America/New_York').
|
|
"""
|
|
import arrow
|
|
|
|
from jinja2 import nodes
|
|
from jinja2.ext import Extension
|
|
import os
|
|
|
|
class TimeExtension(Extension):
|
|
"""
|
|
Jinja2 Extension providing the {% now %} tag for timezone-aware date/time rendering.
|
|
|
|
This extension adds two attributes to the Jinja2 environment:
|
|
- datetime_format: Default strftime format string (default: '%a, %d %b %Y %H:%M:%S')
|
|
- default_timezone: Default timezone for rendering (default: TZ env var or 'UTC')
|
|
|
|
Both can be overridden after environment creation by setting the attributes directly.
|
|
"""
|
|
|
|
tags = {'now'}
|
|
|
|
def __init__(self, environment):
|
|
"""Jinja2 Extension constructor."""
|
|
super().__init__(environment)
|
|
|
|
environment.extend(
|
|
datetime_format='%a, %d %b %Y %H:%M:%S',
|
|
default_timezone=os.getenv('TZ', 'UTC').strip()
|
|
)
|
|
|
|
def _datetime(self, timezone, operator, offset, datetime_format):
|
|
"""
|
|
Get current datetime with time offset applied.
|
|
|
|
Args:
|
|
timezone: IANA timezone identifier (e.g., 'UTC', 'America/New_York') or empty string for default
|
|
operator: '+' for addition or '-' for subtraction
|
|
offset: Comma-separated offset parameters (e.g., 'hours=2,minutes=30')
|
|
datetime_format: strftime format string or None to use environment default
|
|
|
|
Returns:
|
|
Formatted datetime string with offset applied
|
|
|
|
Example:
|
|
_datetime('UTC', '+', 'hours=2,minutes=30', '%Y-%m-%d %H:%M:%S')
|
|
# Returns current time + 2.5 hours
|
|
"""
|
|
# Use default timezone if none specified
|
|
if not timezone or timezone == '':
|
|
timezone = self.environment.default_timezone
|
|
|
|
d = arrow.now(timezone)
|
|
|
|
# parse shift params from offset and include operator
|
|
shift_params = {}
|
|
for param in offset.split(','):
|
|
interval, value = param.split('=')
|
|
shift_params[interval.strip()] = float(operator + value.strip())
|
|
|
|
# Fix weekday parameter can not be float
|
|
if 'weekday' in shift_params:
|
|
shift_params['weekday'] = int(shift_params['weekday'])
|
|
|
|
d = d.shift(**shift_params)
|
|
|
|
if datetime_format is None:
|
|
datetime_format = self.environment.datetime_format
|
|
return d.strftime(datetime_format)
|
|
|
|
def _now(self, timezone, datetime_format):
|
|
"""
|
|
Get current datetime without any offset.
|
|
|
|
Args:
|
|
timezone: IANA timezone identifier (e.g., 'UTC', 'America/New_York') or empty string for default
|
|
datetime_format: strftime format string or None to use environment default
|
|
|
|
Returns:
|
|
Formatted datetime string for current time
|
|
|
|
Example:
|
|
_now('America/New_York', '%Y-%m-%d %H:%M:%S')
|
|
# Returns current time in New York timezone
|
|
"""
|
|
# Use default timezone if none specified
|
|
if not timezone or timezone == '':
|
|
timezone = self.environment.default_timezone
|
|
|
|
if datetime_format is None:
|
|
datetime_format = self.environment.datetime_format
|
|
return arrow.now(timezone).strftime(datetime_format)
|
|
|
|
def parse(self, parser):
|
|
"""
|
|
Parse the {% now %} tag and generate appropriate AST nodes.
|
|
|
|
This method is called by Jinja2 when it encounters a {% now %} tag.
|
|
It parses the tag syntax and determines whether to call _now() or _datetime()
|
|
based on whether offset operations (+ or -) are present.
|
|
|
|
Supported syntax:
|
|
{% now 'timezone' %} -> calls _now()
|
|
{% now 'timezone', 'format' %} -> calls _now()
|
|
{% now 'timezone' + 'offset' %} -> calls _datetime()
|
|
{% now 'timezone' + 'offset', 'format' %} -> calls _datetime()
|
|
{% now 'timezone' - 'offset', 'format' %} -> calls _datetime()
|
|
|
|
Args:
|
|
parser: Jinja2 parser instance
|
|
|
|
Returns:
|
|
nodes.Output: AST output node containing the formatted datetime string
|
|
"""
|
|
lineno = next(parser.stream).lineno
|
|
|
|
node = parser.parse_expression()
|
|
|
|
if parser.stream.skip_if('comma'):
|
|
datetime_format = parser.parse_expression()
|
|
else:
|
|
datetime_format = nodes.Const(None)
|
|
|
|
if isinstance(node, nodes.Add):
|
|
call_method = self.call_method(
|
|
'_datetime',
|
|
[node.left, nodes.Const('+'), node.right, datetime_format],
|
|
lineno=lineno,
|
|
)
|
|
elif isinstance(node, nodes.Sub):
|
|
call_method = self.call_method(
|
|
'_datetime',
|
|
[node.left, nodes.Const('-'), node.right, datetime_format],
|
|
lineno=lineno,
|
|
)
|
|
else:
|
|
call_method = self.call_method(
|
|
'_now',
|
|
[node, datetime_format],
|
|
lineno=lineno,
|
|
)
|
|
return nodes.Output([call_method], lineno=lineno) |