(core) Update the current time in formulas automatically every hour

Summary: Adds a special user action `UpdateCurrentTime` which invalidates an internal engine dependency node that doesn't belong to any table but is 'used' by the `NOW()` function. Applies the action automatically every hour.

Test Plan: Added a Python test for the user action. Tested the interval periodically applying the action manually: {F43312}

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3389
This commit is contained in:
Alex Hall
2022-04-25 22:31:23 +02:00
parent 0beb2898cb
commit dc9e53edc8
8 changed files with 105 additions and 13 deletions

View File

@@ -3,7 +3,6 @@
The data engine ties the code generated from the schema with the document data, and with
dependency tracking.
"""
import contextlib
import itertools
import re
import rlcompleter
@@ -1141,6 +1140,23 @@ class Engine(object):
self.dep_graph.invalidate_deps(table._new_columns_node, depend.ALL_ROWS, self.recompute_map,
include_self=False)
def update_current_time(self):
self.dep_graph.invalidate_deps(self._current_time_node, depend.ALL_ROWS, self.recompute_map,
include_self=False)
def use_current_time(self):
"""
Add a dependency on the current time to the current evaluating node,
so that calling update_current_time() will invalidate the node and cause its reevaluation.
"""
if not self._current_node:
return
table_id = self._current_node[0]
table = self.tables[table_id]
self._use_node(self._current_time_node, table._identity_relation)
_current_time_node = ("#now", None)
def mark_lookupmap_for_cleanup(self, lookup_map_column):
"""
Once a LookupMapColumn seems no longer used, it's added here. We'll check after recomputing

View File

@@ -447,10 +447,11 @@ def NOW(tz=None):
"""
Returns the `datetime` object for the current time.
"""
engine = docmodel.global_docmodel._engine
engine.use_current_time()
return datetime.datetime.now(_get_tzinfo(tz))
def SECOND(time):
"""
Returns the seconds of `datetime`, as an integer from 0 to 59.

View File

@@ -60,8 +60,6 @@ class UserTable(object):
def __init__(self, model_class):
docmodel.enhance_model(model_class)
self.Model = model_class
column_ids = {col for col in model_class.__dict__ if not col.startswith("_")}
column_ids.add('id')
self.table = None
def _set_table_impl(self, table_impl):

View File

@@ -1336,3 +1336,59 @@ class TestUserActions(test_engine.EngineTestCase):
[5, 2],
[6, 2],
])
def test_update_current_time(self):
self.load_sample(self.sample)
self.apply_user_action(["AddEmptyTable"])
self.add_column('Table1', 'now', isFormula=True, formula='NOW()', type='Any')
# No records with NOW() in a formula yet, so this action should have no effect at all.
out_actions = self.apply_user_action(["UpdateCurrentTime"])
self.assertOutActions(out_actions, {})
class FakeDatetime(object):
counter = 0
@classmethod
def now(cls, *_):
cls.counter += 1
return cls.counter
import datetime
original = datetime.datetime
# This monkeypatch depends on NOW() using `import datetime`
# as opposed to `from datetime import datetime`
datetime.datetime = FakeDatetime
def check(expected_now):
self.assertEqual(expected_now, FakeDatetime.counter)
self.assertTableData('Table1', cols="subset", data=[
["id", "now"],
[1, expected_now],
])
try:
# The counter starts at 0. Adding an initial record calls FakeDatetime.now() for the 1st time.
# The call increments the counter to 1 before returning.
self.add_record('Table1')
check(1)
# Testing that unrelated actions don't change the time
self.apply_user_action(["AddEmptyTable"])
self.add_record("Table2")
self.apply_user_action(["Calculate"]) # only recalculates for fresh docs
check(1)
# Actually testing that the time is updated as requested
self.apply_user_action(["UpdateCurrentTime"])
check(2)
out_actions = self.apply_user_action(["UpdateCurrentTime"])
check(3)
self.assertOutActions(out_actions, {
"direct": [False],
"stored": [["UpdateRecord", "Table1", 1, {"now": 3}]],
"undo": [["UpdateRecord", "Table1", 1, {"now": 2}]],
})
finally:
# Revert the monkeypatch
datetime.datetime = original

View File

@@ -319,6 +319,14 @@ class UserActions(object):
"""
pass
@useraction
def UpdateCurrentTime(self):
"""
Somewhat similar to Calculate, trigger calculation
of any cells that depend on the current time.
"""
self._engine.update_current_time()
#----------------------------------------
# User actions on records.
#----------------------------------------