mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
2a206dfcf8
Summary: This gives a mechanism for controlling access control within a document that is distinct from (though implemented with the same machinery as) granular access rules. It was hard to find a good way to insert this that didn't dissolve in a soup of complications, so here's what I went with: * When reading rules, if there are shares, extra rules are added. * If there are shares, all rules are made conditional on a "ShareRef" user property. * "ShareRef" is null when a doc is accessed in normal way, and the row id of a share when accessed via a share. There's no UI for controlling shares (George is working on it for forms), but you can do it by editing a `_grist_Shares` table in a document. Suppose you make a fresh document with a single page/table/widget, then to create an empty share you can do: ``` gristDocPageModel.gristDoc.get().docData.sendAction(['AddRecord', '_grist_Shares', null, {linkId: 'xyz', options: '{"publish": true}'}]) ``` If you look at the home db now there should be something in the `shares` table: ``` $ sqlite3 -table landing.db "select * from shares" +----+------------------------+------------------------+--------------+---------+ | id | key | doc_id | link_id | options | +----+------------------------+------------------------+--------------+---------+ | 1 | gSL4g38PsyautLHnjmXh2K | 4qYuace1xP2CTcPunFdtan | xyz | ... | +----+------------------------+------------------------+--------------+---------+ ``` If you take the key from that (gSL4g38PsyautLHnjmXh2K in this case) and replace the document's urlId in its URL with `s.<key>` (in this case `s.gSL4g38PsyautLHnjmXh2K` then you can use the regular document landing page (it will be quite blank initially) or API endpoint via the share. E.g. for me `http://localhost:8080/o/docs/s0gSL4g38PsyautLHnjmXh2K/share-inter-3` accesses the doc. To actually share some material - useful commands: ``` gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Views_section').getRecords() gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Views_section', 1, {shareOptions: '{"publish": true, "form": true}'}]) gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Pages').getRecords() gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Pages', 1, {shareRef: 1}]) ``` For a share to be effective, at least one page needs to have its shareRef set to the rowId of the share, and at least one widget on one of those pages needs to have its shareOptions set to {"publish": "true", "form": "true"} (meaning turn on sharing, and include form sharing), and the share itself needs {"publish": true} on its options. I think special shares are kind of incompatible with public sharing, since by their nature (allowing access to all endpoints) they easily expose the docId, and changing that would be hard. Test Plan: tests added Reviewers: dsagal, georgegevoian Reviewed By: dsagal, georgegevoian Subscribers: jarek, dsagal Differential Revision: https://phab.getgrist.com/D4144
657 lines
24 KiB
Python
657 lines
24 KiB
Python
import datetime
|
|
import sys
|
|
|
|
import test_engine
|
|
import testsamples
|
|
from autocomplete_context import repr_example, eval_suggestion
|
|
from schema import RecalcWhen
|
|
|
|
|
|
class TestCompletion(test_engine.EngineTestCase):
|
|
user = {
|
|
'Name': 'Foo',
|
|
'UserID': 1,
|
|
'UserRef': '1',
|
|
'StudentInfo': ['Students', 1],
|
|
'LinkKey': {},
|
|
'Origin': None,
|
|
'Email': 'foo@example.com',
|
|
'Access': 'owners',
|
|
'SessionID': 'u1',
|
|
'IsLoggedIn': True,
|
|
'ShareRef': None
|
|
}
|
|
|
|
def setUp(self):
|
|
super(TestCompletion, self).setUp()
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# To test different column types, we add some differently-typed columns to the sample.
|
|
self.add_column('Students', 'school', type='Ref:Schools', visibleCol=10)
|
|
self.add_column('Students', 'homeAddress', type='Ref:Address', visibleCol=21)
|
|
self.add_column('Students', 'birthDate', type='Date')
|
|
self.add_column('Students', 'lastVisit', type='DateTime:America/New_York')
|
|
self.add_column('Schools', 'yearFounded', type='Int')
|
|
self.add_column('Schools', 'budget', type='Numeric')
|
|
self.add_column('Schools', 'lastModified',
|
|
type="DateTime:America/Los_Angeles", isFormula=False, formula="NOW()",
|
|
recalcWhen=RecalcWhen.MANUAL_UPDATES
|
|
)
|
|
self.add_column('Schools', 'lastModifier',
|
|
type="Text", isFormula=False, formula="foo@getgrist.com",
|
|
recalcWhen=RecalcWhen.MANUAL_UPDATES
|
|
)
|
|
self.update_record('Schools', 3, budget='123.45', yearFounded='2010', lastModified='2018-01-01')
|
|
self.update_record('Students', 1, homeAddress=11, school=1)
|
|
# Create a summary table of Students grouped by school
|
|
self.apply_user_action(["CreateViewSection", 1, 0, "record", [22], None])
|
|
|
|
def test_keyword(self):
|
|
self.assertEqual(self.autocomplete("for", "Address", "city"),
|
|
["for", "format("])
|
|
|
|
def test_grist(self):
|
|
self.assertEqual(self.autocomplete("gri", "Address", "city"),
|
|
["grist"])
|
|
|
|
def test_value(self):
|
|
# Should only appear if column exists and is a trigger formula.
|
|
self.assertEqual(
|
|
self.autocomplete("val", "Schools", "lastModified"),
|
|
["value"]
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("val", "Students", "schoolCities"),
|
|
[]
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("val", "Students", "nonexistentColumn"),
|
|
[]
|
|
)
|
|
self.assertEqual(self.autocomplete("valu", "Schools", "lastModifier"),
|
|
["value"])
|
|
# Should have same type as column.
|
|
self.assert_autocomplete_includes("value.", "Schools", "lastModifier",
|
|
{'value.startswith(', 'value.replace(', 'value.title('}
|
|
)
|
|
self.assert_autocomplete_includes("value.", "Schools", "lastModified",
|
|
{'value.month', 'value.strftime(', 'value.replace('}
|
|
)
|
|
self.assert_autocomplete_includes("value.m", "Schools", "lastModified",
|
|
{'value.month', 'value.minute'}
|
|
)
|
|
|
|
def test_user(self):
|
|
# Should only appear if column exists and is a trigger formula.
|
|
self.assertEqual(self.autocomplete("use", "Schools", "lastModified"),
|
|
["user"])
|
|
self.assertEqual(self.autocomplete("use", "Students", "schoolCities"),
|
|
[])
|
|
self.assertEqual(self.autocomplete("use", "Students", "nonexistentColumn"),
|
|
[])
|
|
self.assertEqual(self.autocomplete("user", "Schools", "lastModifier"),
|
|
["user"])
|
|
self.assertEqual(
|
|
self.autocomplete("user.", "Schools", "lastModified", row_id=2),
|
|
[
|
|
('user.Access', "'owners'"),
|
|
('user.Email', "'foo@example.com'"),
|
|
('user.IsLoggedIn', 'True'),
|
|
('user.LinkKey', None),
|
|
('user.Name', "'Foo'"),
|
|
('user.Origin', 'None'),
|
|
('user.SessionID', "'u1'"),
|
|
('user.ShareRef', 'None'),
|
|
('user.StudentInfo', 'Students[1]'),
|
|
('user.UserID', '1'),
|
|
('user.UserRef', "'1'")
|
|
]
|
|
)
|
|
# Should follow user attribute references and autocomplete those types.
|
|
self.assertEqual(
|
|
self.autocomplete("user.StudentInfo.", "Schools", "lastModified", row_id=2),
|
|
[
|
|
('user.StudentInfo.birthDate', 'None'),
|
|
('user.StudentInfo.firstName', "'Barack'"),
|
|
('user.StudentInfo.homeAddress', 'Address[11]'),
|
|
('user.StudentInfo.homeAddress.city', "'New York'"),
|
|
('user.StudentInfo.id', '1'),
|
|
('user.StudentInfo.lastName', "'Obama'"),
|
|
('user.StudentInfo.lastVisit', 'None'),
|
|
('user.StudentInfo.school', 'Schools[1]'),
|
|
('user.StudentInfo.school.name', "'Columbia'"),
|
|
('user.StudentInfo.schoolCities', repr(u'New York:Colombia')),
|
|
('user.StudentInfo.schoolIds', repr(u'1:2')),
|
|
('user.StudentInfo.schoolName', "'Columbia'"),
|
|
]
|
|
)
|
|
# Should not show user attribute completions if user doesn't have attribute.
|
|
user2 = {
|
|
'Name': 'Bar',
|
|
'Origin': None,
|
|
'Email': 'baro@example.com',
|
|
'LinkKey': {},
|
|
'UserID': 2,
|
|
'UserRef': '2',
|
|
'Access': 'owners',
|
|
'SessionID': 'u2',
|
|
'IsLoggedIn': True,
|
|
'ShareRef': None
|
|
}
|
|
self.assertEqual(
|
|
self.autocomplete("user.", "Schools", "lastModified", user2, row_id=2),
|
|
[
|
|
('user.Access', "'owners'"),
|
|
('user.Email', "'baro@example.com'"),
|
|
('user.IsLoggedIn', 'True'),
|
|
('user.LinkKey', None),
|
|
('user.Name', "'Bar'"),
|
|
('user.Origin', 'None'),
|
|
('user.SessionID', "'u2'"),
|
|
('user.ShareRef', 'None'),
|
|
('user.UserID', '2'),
|
|
('user.UserRef', "'2'"),
|
|
]
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("user.StudentInfo.", "Schools", "schoolCities", user2),
|
|
[]
|
|
)
|
|
|
|
def test_function(self):
|
|
self.assertEqual(self.autocomplete("MEDI", "Address", "city"),
|
|
[('MEDIAN', '(value, *more_values)', True)])
|
|
self.assert_autocomplete_includes("ma", "Address", "city", {
|
|
('MAX', '(value, *more_values)', True),
|
|
('MAXA', '(value, *more_values)', True),
|
|
'map(',
|
|
'math',
|
|
'max(',
|
|
})
|
|
|
|
def test_member(self):
|
|
self.assertEqual(self.autocomplete("datetime.tz", "Address", "city"),
|
|
["datetime.tzinfo("])
|
|
|
|
def test_case_insensitive(self):
|
|
self.assertEqual(self.autocomplete("medi", "Address", "city"),
|
|
[('MEDIAN', '(value, *more_values)', True)])
|
|
self.assertEqual(self.autocomplete("std", "Address", "city"), [
|
|
('STDEV', '(value, *more_values)', True),
|
|
('STDEVA', '(value, *more_values)', True),
|
|
('STDEVP', '(value, *more_values)', True),
|
|
('STDEVPA', '(value, *more_values)', True)
|
|
])
|
|
self.assertEqual(
|
|
self.autocomplete("stu", "Address", "city"),
|
|
[
|
|
'Students',
|
|
('Students.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students.lookupRecords', '(colName=<value>, ...)', True),
|
|
'Students.lookupRecords(homeAddress=$id)',
|
|
'Students_summary_school',
|
|
('Students_summary_school.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students_summary_school.lookupRecords', '(colName=<value>, ...)', True)
|
|
],
|
|
)
|
|
|
|
# Add a table name whose lowercase version conflicts with a builtin.
|
|
self.apply_user_action(['AddTable', 'Max', []])
|
|
self.assertEqual(self.autocomplete("max", "Address", "city"), [
|
|
('MAX', '(value, *more_values)', True),
|
|
('MAXA', '(value, *more_values)', True),
|
|
'Max',
|
|
('Max.lookupOne', '(colName=<value>, ...)', True),
|
|
('Max.lookupRecords', '(colName=<value>, ...)', True),
|
|
'max(',
|
|
])
|
|
self.assertEqual(self.autocomplete("MAX", "Address", "city"), [
|
|
('MAX', '(value, *more_values)', True),
|
|
('MAXA', '(value, *more_values)', True),
|
|
])
|
|
|
|
|
|
def test_suggest_globals_and_tables(self):
|
|
# Should suggest globals and table names.
|
|
self.assertEqual(self.autocomplete("ME", "Address", "city"),
|
|
[('MEDIAN', '(value, *more_values)', True)])
|
|
self.assertEqual(
|
|
self.autocomplete("Ad", "Address", "city"),
|
|
[
|
|
'Address',
|
|
('Address.lookupOne', '(colName=<value>, ...)', True),
|
|
('Address.lookupRecords', '(colName=<value>, ...)', True),
|
|
],
|
|
)
|
|
self.assertGreaterEqual(set(self.autocomplete("S", "Address", "city")), {
|
|
'Schools',
|
|
'Students',
|
|
('SUM', '(value1, *more_values)', True),
|
|
('STDEV', '(value, *more_values)', True),
|
|
})
|
|
self.assertGreaterEqual(set(self.autocomplete("s", "Address", "city")), {
|
|
'Schools',
|
|
'Students',
|
|
'sum(',
|
|
('SUM', '(value1, *more_values)', True),
|
|
('STDEV', '(value, *more_values)', True),
|
|
})
|
|
self.assertEqual(
|
|
self.autocomplete("Addr", "Schools", "budget"),
|
|
[
|
|
'Address',
|
|
('Address.lookupOne', '(colName=<value>, ...)', True),
|
|
('Address.lookupRecords', '(colName=<value>, ...)', True),
|
|
],
|
|
)
|
|
|
|
def test_suggest_columns(self):
|
|
self.assertEqual(self.autocomplete("$ci", "Address", "city"),
|
|
["$city"])
|
|
self.assertEqual(self.autocomplete("rec.i", "Address", "city"),
|
|
["rec.id"])
|
|
self.assertEqual(len(self.autocomplete("$", "Address", "city")),
|
|
2)
|
|
|
|
# A few more detailed examples.
|
|
self.assertEqual(self.autocomplete("$", "Students", "school"),
|
|
['$birthDate', '$firstName', '$homeAddress', '$homeAddress.city',
|
|
'$id', '$lastName', '$lastVisit',
|
|
'$school', '$school.name', '$schoolCities', '$schoolIds', '$schoolName'])
|
|
self.assertEqual(self.autocomplete("$fi", "Students", "birthDate"),
|
|
['$firstName'])
|
|
self.assertEqual(self.autocomplete("$school", "Students", "lastVisit"),
|
|
['$school', '$school.name', '$schoolCities', '$schoolIds', '$schoolName'])
|
|
|
|
def test_suggest_lookup_methods(self):
|
|
# Should suggest lookup formulas for tables.
|
|
address_dot_completion = self.autocomplete("Address.", "Students", "firstName")
|
|
# In python 3.9.7, rlcompleter stops adding parens for property attributes,
|
|
# see https://bugs.python.org/issue44752 - seems like a minor issue, so leave test
|
|
# tolerant.
|
|
property_aware_completer = address_dot_completion[0] == 'Address.Record'
|
|
self.assertEqual(address_dot_completion, [
|
|
'Address.Record' if property_aware_completer else ('Address.Record', '', True),
|
|
'Address.RecordSet' if property_aware_completer else ('Address.RecordSet', '', True),
|
|
'Address.all',
|
|
('Address.lookupOne', '(colName=<value>, ...)', True),
|
|
('Address.lookupRecords', '(colName=<value>, ...)', True),
|
|
])
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("Address.lookup", "Students", "lastName"),
|
|
[
|
|
('Address.lookupOne', '(colName=<value>, ...)', True),
|
|
('Address.lookupRecords', '(colName=<value>, ...)', True),
|
|
]
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("address.look", "Students", "schoolName"),
|
|
[
|
|
('Address.lookupOne', '(colName=<value>, ...)', True),
|
|
('Address.lookupRecords', '(colName=<value>, ...)', True),
|
|
]
|
|
)
|
|
|
|
def test_suggest_column_type_methods(self):
|
|
# Should treat columns as correct types.
|
|
self.assert_autocomplete_includes("$firstName.", "Students", "firstName",
|
|
{'$firstName.startswith(', '$firstName.replace(', '$firstName.title('}
|
|
)
|
|
self.assert_autocomplete_includes("$birthDate.", "Students", "lastName",
|
|
{'$birthDate.month', '$birthDate.strftime(', '$birthDate.replace('}
|
|
)
|
|
self.assert_autocomplete_includes("$lastVisit.m", "Students", "firstName",
|
|
{'$lastVisit.month', '$lastVisit.minute'}
|
|
)
|
|
self.assert_autocomplete_includes("$school.", "Students", "firstName",
|
|
{'$school.address', '$school.name', '$school.yearFounded', '$school.budget'}
|
|
)
|
|
self.assertEqual(self.autocomplete("$school.year", "Students", "lastName"),
|
|
['$school.yearFounded'])
|
|
self.assert_autocomplete_includes("$yearFounded.", "Schools", "budget",
|
|
{
|
|
'$yearFounded.denominator', # Only integers have this
|
|
'$yearFounded.bit_length(', # and this
|
|
'$yearFounded.real'
|
|
}
|
|
)
|
|
self.assert_autocomplete_includes("$budget.", "Schools", "budget",
|
|
{'$budget.is_integer(', '$budget.real'} # Only floats have this
|
|
)
|
|
|
|
def test_suggest_follows_references(self):
|
|
# Should follow references and autocomplete those types.
|
|
self.assertEqual(
|
|
self.autocomplete("$school.name.st", "Students", "firstName"),
|
|
['$school.name.startswith(', '$school.name.strip(']
|
|
)
|
|
self.assert_autocomplete_includes("$school.yearFounded.","Students", "firstName",
|
|
{
|
|
'$school.yearFounded.denominator',
|
|
'$school.yearFounded.bit_length(',
|
|
'$school.yearFounded.real'
|
|
}
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("$school.address.", "Students", "lastName"),
|
|
['$school.address.city', '$school.address.id']
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("$school.address.city.st", "Students", "lastName"),
|
|
['$school.address.city.startswith(', '$school.address.city.strip(']
|
|
)
|
|
|
|
def test_suggest_lookup_early(self):
|
|
# For part of a table name, suggest lookup methods early,
|
|
# including a 'reverse reference' lookup, i.e. `<refcol to current table>=$id`,
|
|
# but only for `lookupRecords`, not `lookupOne`.
|
|
self.assertEqual(
|
|
self.autocomplete("stu", "Schools", "name"),
|
|
[
|
|
'Students',
|
|
('Students.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students.lookupRecords', '(colName=<value>, ...)', True),
|
|
# i.e. Students.school is a reference to Schools
|
|
'Students.lookupRecords(school=$id)',
|
|
'Students_summary_school',
|
|
('Students_summary_school.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students_summary_school.lookupRecords', '(colName=<value>, ...)', True),
|
|
'Students_summary_school.lookupRecords(school=$id)',
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("scho", "Address", "city"),
|
|
[
|
|
'Schools',
|
|
('Schools.lookupOne', '(colName=<value>, ...)', True),
|
|
('Schools.lookupRecords', '(colName=<value>, ...)', True),
|
|
# i.e. Schools.address is a reference to Address
|
|
'Schools.lookupRecords(address=$id)',
|
|
],
|
|
)
|
|
|
|
# Same as above, but the formula is being entered in 'Students' instead of 'Address',
|
|
# which means there's no reverse reference to suggest.
|
|
self.assertEqual(
|
|
self.autocomplete("scho", "Students", "firstName"),
|
|
[
|
|
'Schools',
|
|
('Schools.lookupOne', '(colName=<value>, ...)', True),
|
|
('Schools.lookupRecords', '(colName=<value>, ...)', True),
|
|
],
|
|
)
|
|
|
|
# Test from within a summary table
|
|
self.assertEqual(
|
|
self.autocomplete("stu", "Students_summary_school", "count"),
|
|
[
|
|
'Students',
|
|
('Students.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students.lookupRecords', '(colName=<value>, ...)', True),
|
|
'Students_summary_school',
|
|
('Students_summary_school.lookupOne', '(colName=<value>, ...)', True),
|
|
('Students_summary_school.lookupRecords', '(colName=<value>, ...)', True),
|
|
],
|
|
)
|
|
|
|
def test_suggest_lookup_arguments(self):
|
|
# Typing in the full `.lookupRecords(` should suggest keyword argument (i.e. column) names,
|
|
# in addition to reference lookups, including the reverse reference lookups above.
|
|
self.assertEqual(
|
|
self.autocomplete("Schools.lookupRecords(", "Address", "city"),
|
|
[
|
|
'Schools.lookupRecords(address=',
|
|
'Schools.lookupRecords(address=$id)',
|
|
'Schools.lookupRecords(budget=',
|
|
'Schools.lookupRecords(id=',
|
|
'Schools.lookupRecords(lastModified=',
|
|
'Schools.lookupRecords(lastModifier=',
|
|
'Schools.lookupRecords(name=',
|
|
'Schools.lookupRecords(yearFounded=',
|
|
],
|
|
)
|
|
|
|
# In addition to reverse reference lookups, suggest other lookups involving two reference
|
|
# columns (one from the looked up table, one from the current table) targeting the same table,
|
|
# e.g. `address=$homeAddress` in the two cases below.
|
|
self.assertEqual(
|
|
self.autocomplete("Schools.lookupRecords(", "Students", "firstName"),
|
|
[
|
|
'Schools.lookupRecords(address=',
|
|
'Schools.lookupRecords(address=$homeAddress)',
|
|
'Schools.lookupRecords(budget=',
|
|
'Schools.lookupRecords(id=',
|
|
'Schools.lookupRecords(lastModified=',
|
|
'Schools.lookupRecords(lastModifier=',
|
|
'Schools.lookupRecords(name=',
|
|
'Schools.lookupRecords(yearFounded=',
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("Students.lookupRecords(", "Schools", "name"),
|
|
[
|
|
'Students.lookupRecords(birthDate=',
|
|
'Students.lookupRecords(firstName=',
|
|
'Students.lookupRecords(homeAddress=',
|
|
'Students.lookupRecords(homeAddress=$address)',
|
|
'Students.lookupRecords(id=',
|
|
'Students.lookupRecords(lastName=',
|
|
'Students.lookupRecords(lastVisit=',
|
|
'Students.lookupRecords(school=',
|
|
'Students.lookupRecords(school=$id)',
|
|
'Students.lookupRecords(schoolCities=',
|
|
'Students.lookupRecords(schoolIds=',
|
|
'Students.lookupRecords(schoolName=',
|
|
],
|
|
)
|
|
|
|
# Add some more reference columns to test that all combinations are offered
|
|
self.add_column('Students', 'homeAddress2', type='Ref:Address')
|
|
self.add_column('Schools', 'address2', type='Ref:Address')
|
|
# This leads to `Students.lookupRecords(moreAddresses=CONTAINS($address[2]))`
|
|
self.add_column('Students', 'moreAddresses', type='RefList:Address')
|
|
# This doesn't affect anything, because there's no way to do the opposite of CONTAINS()
|
|
self.add_column('Schools', 'otherAddresses', type='RefList:Address')
|
|
self.assertEqual(
|
|
self.autocomplete("Students.lookupRecords(", "Schools", "name"),
|
|
[
|
|
'Students.lookupRecords(birthDate=',
|
|
'Students.lookupRecords(firstName=',
|
|
'Students.lookupRecords(homeAddress2=',
|
|
'Students.lookupRecords(homeAddress2=$address)',
|
|
'Students.lookupRecords(homeAddress2=$address2)',
|
|
'Students.lookupRecords(homeAddress=',
|
|
'Students.lookupRecords(homeAddress=$address)',
|
|
'Students.lookupRecords(homeAddress=$address2)',
|
|
'Students.lookupRecords(id=',
|
|
'Students.lookupRecords(lastName=',
|
|
'Students.lookupRecords(lastVisit=',
|
|
'Students.lookupRecords(moreAddresses=',
|
|
'Students.lookupRecords(moreAddresses=CONTAINS($address))',
|
|
'Students.lookupRecords(moreAddresses=CONTAINS($address2))',
|
|
'Students.lookupRecords(school=',
|
|
'Students.lookupRecords(school=$id)',
|
|
'Students.lookupRecords(schoolCities=',
|
|
'Students.lookupRecords(schoolIds=',
|
|
'Students.lookupRecords(schoolName=',
|
|
],
|
|
)
|
|
|
|
def autocomplete(self, formula, table, column, user=None, row_id=None):
|
|
"""
|
|
Mild convenience over self.engine.autocomplete.
|
|
Only returns suggestions without example values, unless row_id is specified.
|
|
"""
|
|
user = user or self.user
|
|
results = self.engine.autocomplete(formula, table, column, row_id or 1, user)
|
|
if row_id is None:
|
|
return [result for result, value in results]
|
|
else:
|
|
return results
|
|
|
|
def assert_autocomplete_includes(self, formula, table, column, expected, user=None, row_id=None):
|
|
completions = self.autocomplete(formula, table, column, user=user, row_id=row_id)
|
|
|
|
def replace_completion(completion):
|
|
if isinstance(completion, str) and completion.endswith('()'):
|
|
# Python 3.10+ autocompletes the closing paren for methods with no arguments.
|
|
# This allows the test to check for `somestring.title(` and work across Python versions.
|
|
assert sys.version_info >= (3, 10)
|
|
return completion[:-1]
|
|
return completion
|
|
|
|
completions = set(replace_completion(completion) for completion in completions)
|
|
self.assertGreaterEqual(completions, expected)
|
|
|
|
def test_example_values(self):
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Schools", "name", row_id=1),
|
|
[
|
|
('$address', 'Address[11]'),
|
|
('$budget', '0.0'),
|
|
('$id', '1'),
|
|
('$lastModified', 'None'),
|
|
('$lastModifier', repr(u'')),
|
|
('$name', "'Columbia'"),
|
|
('$yearFounded', '0'),
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Schools", "name", row_id=3),
|
|
[
|
|
('$address', 'Address[13]'),
|
|
('$budget', '123.45'),
|
|
('$id', '3'),
|
|
('$lastModified', '2018-01-01 12:00am'),
|
|
('$lastModifier', None),
|
|
('$name', "'Yale'"),
|
|
('$yearFounded', '2010'),
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Address", "name", row_id=1),
|
|
[
|
|
('$city', repr(u'')), # for Python 2/3 compatibility
|
|
('$id', '0'), # row_id 1 doesn't exist!
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Address", "name", row_id=11),
|
|
[
|
|
('$city', "'New York'"),
|
|
('$id', '11'),
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Address", "name", row_id='new'),
|
|
[
|
|
('$city', "'West Haven'"),
|
|
('$id', '14'), # row_id 'new' gets replaced with the maximum row ID in the table
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("$", "Students", "name", row_id=1),
|
|
[
|
|
('$birthDate', 'None'),
|
|
('$firstName', "'Barack'"),
|
|
('$homeAddress', 'Address[11]'),
|
|
('$homeAddress.city', "'New York'"),
|
|
('$id', '1'),
|
|
('$lastName', "'Obama'"),
|
|
('$lastVisit', 'None'),
|
|
('$school', 'Schools[1]'),
|
|
('$school.name', "'Columbia'"),
|
|
('$schoolCities', repr(u'New York:Colombia')),
|
|
('$schoolIds', repr(u'1:2')),
|
|
('$schoolName', "'Columbia'"),
|
|
],
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.autocomplete("rec", "Students", "name", row_id=1),
|
|
[
|
|
# Mixture of suggestions with and without values
|
|
(('RECORD', '(record_or_list, dates_as_iso=False, expand_refs=0)', True), None),
|
|
('rec', 'Students[1]'),
|
|
],
|
|
)
|
|
|
|
def test_repr(self):
|
|
date = datetime.date(2019, 12, 31)
|
|
dtime = datetime.datetime(2019, 12, 31, 13, 23)
|
|
self.assertEqual(repr_example(date), "2019-12-31")
|
|
self.assertEqual(repr_example(dtime), "2019-12-31 1:23pm")
|
|
self.assertEqual(repr_example([1, 'a', dtime, date]),
|
|
"[1, 'a', 2019-12-31 1:23pm, 2019-12-31]")
|
|
|
|
prefix = "<BadRepr instance at 0x"
|
|
self.assertEqual(repr_example(BadRepr())[:len(prefix)], prefix)
|
|
|
|
big_list = [9] * 100000
|
|
self.assertEqual(len(big_list), 100000)
|
|
big_list_repr = repr_example(big_list)
|
|
self.assertEqual(len(big_list_repr), 605)
|
|
self.assertEqual(big_list_repr, "[%s...]" % ("9, " * 200))
|
|
|
|
def test_eval_suggestion(self):
|
|
class Record(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __repr__(self):
|
|
return "Record(%s)" % self.name
|
|
|
|
@property
|
|
def bad(self):
|
|
raise Exception("bad")
|
|
|
|
rec = Record('rec')
|
|
rec.subrec = Record('subrec')
|
|
rec.subrec.meaning = 42
|
|
rec.bad_repr = BadRepr()
|
|
rec.big = "a" * 100000
|
|
user = Record('user')
|
|
user.email = 'my_email'
|
|
user.LinkKey = Record('LinkKey')
|
|
user.LinkKey.id = 123
|
|
|
|
self.assertEqual(eval_suggestion('rec', rec, user), 'Record(rec)')
|
|
self.assertEqual(eval_suggestion('rec.subrec', rec, user), 'Record(subrec)')
|
|
self.assertEqual(eval_suggestion('rec.subrec.meaning', rec, user), '42')
|
|
|
|
self.assertEqual(eval_suggestion('rec.spam', rec, user), None) # doesn't exist
|
|
self.assertEqual(eval_suggestion('rec.bad', rec, user), None) # property raises an error
|
|
|
|
# attribute exists, but repr() raises an error
|
|
prefix = "<BadRepr instance at 0x"
|
|
self.assertEqual(eval_suggestion('rec.bad_repr', rec, user)[:len(prefix)], prefix)
|
|
|
|
# attribute exists, but repr() is too long and gets truncated
|
|
big_repr = repr_example(rec.big)
|
|
self.assertEqual(eval_suggestion('rec.big', rec, user), big_repr)
|
|
self.assertEqual(len(big_repr), 200)
|
|
|
|
# No string representations for these two
|
|
self.assertEqual(eval_suggestion('user', rec, user), None)
|
|
self.assertEqual(eval_suggestion('user.LinkKey', rec, user), None)
|
|
|
|
self.assertEqual(eval_suggestion('user.email', rec, user), "'my_email'")
|
|
self.assertEqual(eval_suggestion('user.LinkKey.id', rec, user), '123')
|
|
|
|
self.assertEqual(eval_suggestion('user.spam', rec, user), None) # doesn't exist
|
|
self.assertEqual(eval_suggestion('user.bad', rec, user), None) # property raises an error
|
|
|
|
self.assertEqual(eval_suggestion('subrec', rec, user), None) # other variables not supported
|
|
|
|
|
|
class BadRepr(object):
|
|
def __repr__(self):
|
|
raise Exception("Bad repr")
|