(core) add SELF_HYPERLINK() function for generating links to the current document

Summary:
 * Adds a `SELF_HYPERLINK()` python function, with optional keyword arguments to set a label, the page, and link parameters.
 * Adds a `UUID()` python function, since using python's uuid.uuidv4 hits a problem accessing /dev/urandom in the sandbox.  UUID makes no particular quality claims since it doesn't use an audited implementation.  A difficult to guess code is convenient for some use cases that `SELF_HYPERLINK()` enables.

The canonical URL for a document is mutable, but older versions generally forward.  So for implementation simplicity the document url is passed it on sandbox creation and remains fixed throughout the lifetime of the sandbox.  This could and should be improved in future.

The URL is passed into the sandbox as a `DOC_URL` environment variable.

The code for creating the URL is factored out of `Notifier.ts`. Since the url is a function of the organization as well as the document, some rejiggering is needed to make that information available to DocManager.

On document imports, the new document is registered in the database slightly earlier now, in order to keep the procedure for constructing the URL in different starting conditions more homogeneous.

Test Plan: updated test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2759
This commit is contained in:
Paul Fitzpatrick
2021-03-18 18:40:02 -04:00
parent b4c34cedad
commit 0c5f7cf0a7
14 changed files with 207 additions and 39 deletions

View File

@@ -1,4 +1,8 @@
# pylint: disable=redefined-builtin, line-too-long
from collections import OrderedDict
import os
from urllib import urlencode
import urlparse
from unimplemented import unimplemented
@unimplemented
@@ -71,6 +75,57 @@ def ROWS(range):
"""Returns the number of rows in a specified array or range."""
raise NotImplementedError()
def SELF_HYPERLINK(label=None, page=None, **kwargs):
"""
Creates a link to the current document. All parameters are optional.
The returned string is in URL format, optionally preceded by a label and a space
(the format expected for Grist Text columns with the HyperLink option enabled).
A numeric page number can be supplied, which will create a link to the
specified page. To find the numeric page number you need, visit a page
and examine its URL for a `/p/NN` part.
Any number of arguments of the form `LinkKey_NAME` may be provided, to set
`user.LinkKey.NAME` values that will be available in access rules. For example,
if a rule allows users to view rows when `user.LinkKey.Code == rec.Code`,
we might want to create links with `SELF_HYPERLINK(LinkKey_Code=$Code)`.
>>> SELF_HYPERLINK()
'https://docs.getgrist.com/sbaltsirg/Example'
>>> SELF_HYPERLINK(label='doc')
'doc https://docs.getgrist.com/sbaltsirg/Example'
>>> SELF_HYPERLINK(page=2)
'https://docs.getgrist.com/sbaltsirg/Example/p/2'
>>> SELF_HYPERLINK(LinkKey_Code='X1234')
'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234'
>>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo')
'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo'
>>> SELF_HYPERLINK(Linky_Link='Link')
Traceback (most recent call last):
...
TypeError: unexpected keyword argument 'Linky_Link' (not of form LinkKey_NAME)
"""
txt = os.environ.get('DOC_URL')
if not txt:
return None
if page:
txt += "/p/{}".format(page)
if kwargs:
parts = list(urlparse.urlparse(txt))
query = OrderedDict(urlparse.parse_qsl(parts[4]))
for [key, value] in kwargs.iteritems():
key_parts = key.split('LinkKey_')
if len(key_parts) == 2 and key_parts[0] == '':
query[key_parts[1] + '_'] = value
else:
raise TypeError("unexpected keyword argument '{}' (not of form LinkKey_NAME)".format(key))
parts[4] = urlencode(query)
txt = urlparse.urlunparse(parts)
if label:
txt = "{} {}".format(label, txt)
return txt
def VLOOKUP(table, **field_value_pairs):
"""
Vertical lookup. Searches the given table for a record matching the given `field=value`

View File

@@ -5,6 +5,7 @@ import itertools
import math as _math
import operator
import random
import uuid
from functions.info import ISNUMBER, ISLOGICAL
from functions.unimplemented import unimplemented
@@ -833,3 +834,7 @@ def TRUNC(value, places=0):
"""
# TRUNC seems indistinguishable from ROUNDDOWN.
return ROUNDDOWN(value, places)
def UUID():
"""Generate a random UUID-formatted string identifier."""
return str(uuid.UUID(bytes=[chr(random.randrange(0, 256)) for _ in xrange(0, 16)], version=4))

View File

@@ -1,4 +1,5 @@
import doctest
import os
import functions
import moment
@@ -18,6 +19,8 @@ def date_tearDown(doc_test):
# This works with the unittest module to turn all the doctests in the functions' doc-comments into
# unittest test cases.
def load_tests(loader, tests, ignore):
# Set DOC_URL for SELF_HYPERLINK()
os.environ['DOC_URL'] = 'https://docs.getgrist.com/sbaltsirg/Example'
tests.addTests(doctest.DocTestSuite(functions.date, setUp = date_setUp, tearDown = date_tearDown))
tests.addTests(doctest.DocTestSuite(functions.info, setUp = date_setUp, tearDown = date_tearDown))
tests.addTests(doctest.DocTestSuite(functions.logical))
@@ -26,4 +29,5 @@ def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(functions.text))
tests.addTests(doctest.DocTestSuite(functions.schedule,
setUp = date_setUp, tearDown = date_tearDown))
tests.addTests(doctest.DocTestSuite(functions.lookup))
return tests