Skip to content
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

Replace jinja2-time extension #2544

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions changedetectionio/jinja_extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Jinja2 extensions."""
import arrow
from secrets import choice

from jinja2 import nodes
from jinja2.ext import Extension

class TimeExtension(Extension):
"""Jinja2 Extension for dates and times."""

tags = {'now'}

def __init__(self, environment):
"""Jinja2 Extension constructor."""
super().__init__(environment)

environment.extend(datetime_format='%Y-%m-%d')

def _datetime(self, timezone, operator, offset, datetime_format):
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):
if datetime_format is None:
datetime_format = self.environment.datetime_format
return arrow.now(timezone).strftime(datetime_format)

def parse(self, parser):
"""Parse datetime template and add datetime value."""
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)
2 changes: 1 addition & 1 deletion changedetectionio/safe_jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


def render(template_str, **args: t.Any) -> str:
jinja2_env = jinja2.sandbox.ImmutableSandboxedEnvironment(extensions=['jinja2_time.TimeExtension'])
jinja2_env = jinja2.sandbox.ImmutableSandboxedEnvironment(extensions=['changedetectionio.jinja_extensions.TimeExtension'])
output = jinja2_env.from_string(template_str).render(args)
return output[:JINJA2_MAX_RETURN_PAYLOAD_SIZE]

74 changes: 74 additions & 0 deletions changedetectionio/tests/test_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import time
from flask import url_for
from .util import live_server_setup, wait_for_all_checks
from ..safe_jinja import render
import arrow


def test_setup(client, live_server, measure_memory_usage):
Expand Down Expand Up @@ -56,3 +58,75 @@ def test_jinja2_security_url_query(client, live_server, measure_memory_usage):
assert b'is invalid and cannot be used' in res.data
# Some of the spewed output from the subclasses
assert b'dict_values' not in res.data

def test_timezone(mocker):
"""Verify that timezone is parsed."""

timezone = 'America/Buenos Aires'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
finalRender = render(f"{{% now '{timezone}' %}}")

assert finalRender == currentDate.strftime("%Y-%m-%d")

def test_format(mocker):
"""Verify that format is parsed."""

timezone = 'utc'
format = '%d %b %Y %H:%M:%S'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
finalRender = render(f"{{% now '{timezone}', '{format}' %}}")

assert finalRender == currentDate.strftime(format)

def test_add_time(mocker):
"""Verify that added time offset can be parsed."""

timezone = 'utc'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
finalRender = render(f"{{% now '{timezone}' + 'hours=2,seconds=30' %}}")

assert finalRender == currentDate.strftime("%Y-%m-%d")

def test_add_weekday(mocker):
"""Verify that added weekday offset can be parsed."""

timezone = 'utc'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
finalRender = render(f"{{% now '{timezone}' + 'weekday=1' %}}")

assert finalRender == currentDate.shift(weekday=1).strftime('%Y-%m-%d')


def test_substract_time(mocker):
"""Verify that substracted time offset can be parsed."""

timezone = 'utc'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
finalRender = render(f"{{% now '{timezone}' - 'minutes=11' %}}")

assert finalRender == currentDate.shift(minutes=-11).strftime("%Y-%m-%d")


def test_offset_with_format(mocker):
"""Verify that offset works together with datetime format."""

timezone = 'utc'
currentDate = arrow.now(timezone)
arrowNowMock = mocker.patch("arrow.now")
arrowNowMock.return_value = currentDate
format = '%d %b %Y %H:%M:%S'
finalRender = render(
f"{{% now '{timezone}' - 'days=2,minutes=33,seconds=1', '{format}' %}}"
)

assert finalRender == currentDate.shift(days=-2, minutes=-33, seconds=-1).strftime(format)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ werkzeug~=3.0

# Templating, so far just in the URLs but in the future can be for the notifications also
jinja2~=3.1
jinja2-time
arrow
openpyxl
# https://peps.python.org/pep-0508/#environment-markers
# https://github.com/dgtlmoon/changedetection.io/pull/1009
Expand All @@ -78,6 +78,7 @@ pyppeteerstealth>=0.0.4
# Include pytest, so if theres a support issue we can ask them to run these tests on their setup
pytest ~=7.2
pytest-flask ~=1.2
pytest-mock==3.14.0

# Pin jsonschema version to prevent build errors on armv6 while rpds-py wheels aren't available (1708)
jsonschema==4.17.3
Expand Down
Loading