Skip to content

Commit b0822f8

Browse files
authored
7112: Conditional Formats of Fields (#34)
1 parent 7693199 commit b0822f8

File tree

3 files changed

+274
-4
lines changed

3 files changed

+274
-4
lines changed

labkey/domain.py

+64-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
# limitations under the License.
1515
#
1616
from __future__ import unicode_literals
17-
import json
1817

1918
from labkey.utils import json_dumps, ServerContext
19+
from labkey.query import QueryFilter
2020

2121

2222
def strip_none_values(data, do_strip=True):
@@ -309,6 +309,67 @@ def to_json(self, strip_none=True):
309309
return strip_none_values(data, strip_none)
310310

311311

312+
def conditional_format(query_filter, bold=False, italic=False, strike_through=False, text_color="",
313+
background_color=""):
314+
# type: (any, bool, bool, bool, str, str) -> ConditionalFormat
315+
"""
316+
Creates a conditional format for use with an existing domain.
317+
Supports filter URL format as well as QueryFilter filter parameters.
318+
"""
319+
filter_str = ''
320+
if isinstance(query_filter, QueryFilter):
321+
filter_str = encode_conditional_format_filter(query_filter)
322+
elif isinstance(query_filter, list):
323+
if len(query_filter) > 2:
324+
raise Exception('Too many QueryFilters given for one conditional format.')
325+
if (not isinstance(query_filter[0], QueryFilter)) or \
326+
(len(query_filter) > 1 and not isinstance(query_filter[1], QueryFilter)):
327+
raise Exception('Please pass QueryFilter objects when updating a conditional format using a list filter.')
328+
329+
string_filters = list(map(lambda f: encode_conditional_format_filter(f), query_filter))
330+
filter_str = string_filters[0] + '&' + string_filters[1] if len(query_filter) == 2 else string_filters[0]
331+
else:
332+
filter_str = query_filter
333+
334+
cf = ConditionalFormat.from_data({
335+
'background_color': background_color,
336+
'bold': bold,
337+
'filter': filter_str,
338+
'italic': italic,
339+
'strike_through': strike_through,
340+
'text_color': text_color,
341+
})
342+
343+
return cf
344+
345+
346+
def encode_conditional_format_filter(query_filter):
347+
# type: (QueryFilter) -> str
348+
return 'format.column~{}={}'.format(query_filter.filter_type, query_filter.value)
349+
350+
351+
def __format_conditional_filters(field):
352+
# type: (dict) -> dict
353+
"""
354+
For every conditional format filter that is set as a QueryFilter, translates the given filter into LabKey
355+
filter URL format.
356+
"""
357+
if 'conditionalFormats' in field:
358+
for cf in field['conditionalFormats']:
359+
if 'filter' in cf and isinstance(cf['filter'], QueryFilter): # Supports one QueryFilter without list form
360+
cf['filter'] = encode_conditional_format_filter(cf['filter'])
361+
362+
elif 'filter' in cf and isinstance(cf['filter'], list): # Supports list of QueryFilters
363+
filters = []
364+
for query_filter in cf['filter']:
365+
filters.append(encode_conditional_format_filter(query_filter))
366+
if len(filters) > 2:
367+
raise Exception("Too many QueryFilters given for one conditional format.")
368+
cf['filter'] = filters[0] + '&' + filters[1] if len(filters) == 2 else filters[0]
369+
370+
return field
371+
372+
312373
def create(server_context, domain_definition, container_path=None):
313374
# type: (ServerContext, dict, str) -> Domain
314375
"""
@@ -326,6 +387,8 @@ def create(server_context, domain_definition, container_path=None):
326387

327388
domain = None
328389

390+
domain_fields = domain_definition['domainDesign']['fields']
391+
domain_definition['domainDesign']['fields'] = list(map(__format_conditional_filters, domain_fields))
329392
raw_domain = server_context.make_request(url, json_dumps(domain_definition), headers=headers)
330393

331394
if raw_domain is not None:

samples/domain_example.py

+75
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import unicode_literals
1717

1818
from labkey.utils import create_server_context
19+
from labkey.query import QueryFilter
1920
from labkey import domain
2021

2122
labkey_server = 'localhost:8080'
@@ -108,3 +109,77 @@
108109
drop_response = domain.drop(server_context, 'lists', 'BloodTypes')
109110
if 'success' in drop_response:
110111
print('The list domain was deleted.')
112+
113+
###################
114+
# Create a domain with a conditional format
115+
###################
116+
list_with_cf = {
117+
'kind': 'IntList',
118+
'domainDesign': {
119+
'name': 'ListWithConditionalFormats',
120+
'description': 'Test list',
121+
'fields': [{
122+
'name': 'rowId',
123+
'rangeURI': 'int'
124+
}, {
125+
'name': 'date',
126+
'rangeURI': 'date',
127+
'conditionalFormats': [{
128+
'filter': [
129+
QueryFilter('date', '10/29/1995', QueryFilter.Types.DATE_GREATER_THAN),
130+
QueryFilter('date', '10/31/1995', QueryFilter.Types.DATE_LESS_THAN)
131+
],
132+
'textcolor': 'f44e3b',
133+
'backgroundcolor': 'fcba03',
134+
'bold': True,
135+
'italic': False,
136+
'strikethrough': False
137+
}]
138+
}, {
139+
'name': 'age',
140+
'rangeURI': 'int',
141+
'conditionalFormats': [{
142+
'filter': QueryFilter('age', 500, QueryFilter.Types.GREATER_THAN),
143+
'textcolor': 'f44e3b',
144+
'backgroundcolor': 'fcba03',
145+
'bold': True,
146+
'italic': True,
147+
'strikethrough': False
148+
}]
149+
}]
150+
},
151+
'options': {
152+
'keyName': 'rowId',
153+
'keyType': 'AutoIncrementInteger'
154+
}
155+
}
156+
157+
domain_cf = domain.create(server_context, list_with_cf)
158+
159+
###################
160+
# Edit an existing domain's conditional format
161+
###################
162+
age_field = list(filter(lambda domain_field: domain_field.name == 'age', domain_cf.fields))[0]
163+
print('The filter on field "' + age_field.name + '" was: ' + age_field.conditional_formats[0].filter)
164+
165+
for field in domain_cf.fields:
166+
if field.name == 'age':
167+
cf = domain.conditional_format(query_filter='format.column~eq=30', text_color='ff0000')
168+
field.conditional_formats = [cf]
169+
if field.name == 'date':
170+
cf = domain.conditional_format(query_filter=QueryFilter('date', '10/30/1995', QueryFilter.Types.DATE_LESS_THAN),
171+
text_color='f44e3b')
172+
field.conditional_formats = [cf]
173+
174+
domain.save(server_context, 'lists', 'ListWithConditionalFormats', domain_cf)
175+
print('The filter on field "' + age_field.name + '" has been updated to: ' + age_field.conditional_formats[0].filter)
176+
177+
###################
178+
# Delete a domain's conditional format
179+
###################
180+
for field in domain_cf.fields:
181+
if field.name == 'age':
182+
field.conditional_formats = []
183+
184+
# Cleanup
185+
domain.drop(server_context, 'lists', 'ListWithConditionalFormats')

test/test_domain.py

+135-3
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
import unittest.mock as mock
2626

2727
from labkey import utils
28-
from labkey.domain import create, Domain, drop, get, infer_fields, save
28+
from labkey.domain import create, conditional_format, Domain, drop, encode_conditional_format_filter, \
29+
get, infer_fields, save
2930
from labkey.exceptions import RequestAuthorizationError
31+
from labkey.query import QueryFilter
3032

31-
from .utilities import MockLabKey, mock_server_context, success_test, success_test_get, throws_error_test, throws_error_test_get
33+
from .utilities import MockLabKey, mock_server_context, success_test, success_test_get, throws_error_test, \
34+
throws_error_test_get
3235

3336

3437
domain_controller = 'property'
@@ -244,14 +247,143 @@ def test_unauthorized(self):
244247
save, *self.args, **self.expected_kwargs)
245248

246249

250+
class TestConditionalFormatCreate(unittest.TestCase):
251+
252+
def setUp(self):
253+
254+
self.domain_definition = {
255+
'kind': 'IntList',
256+
'domainDesign': {
257+
'name': 'TheTestList_cf',
258+
'fields': [{
259+
'name': 'theKey',
260+
'rangeURI': 'int',
261+
'conditionalFormats': [{
262+
'filter': encode_conditional_format_filter(QueryFilter('theKey', 500)),
263+
'textColor': 'f44e3b',
264+
'backgroundColor': 'fcba03',
265+
'bold': True,
266+
'italic': True,
267+
'strikethrough': False
268+
}]
269+
}]
270+
},
271+
}
272+
273+
class MockCreate(MockLabKey):
274+
api = 'createDomain.api'
275+
default_action = domain_controller
276+
default_success_body = self.domain_definition
277+
278+
self.service = MockCreate()
279+
280+
self.expected_kwargs = {
281+
'expected_args': [self.service.get_server_url()],
282+
'data': json.dumps(self.domain_definition),
283+
'headers': {'Content-Type': 'application/json'},
284+
'timeout': 300
285+
}
286+
287+
self.args = [
288+
mock_server_context(self.service), self.domain_definition
289+
]
290+
291+
def test_success(self):
292+
test = self
293+
success_test(test, self.service.get_successful_response(),
294+
create, False, *self.args, **self.expected_kwargs)
295+
296+
def test_unauthorized(self):
297+
test = self
298+
throws_error_test(test, RequestAuthorizationError, self.service.get_unauthorized_response(),
299+
create, *self.args, **self.expected_kwargs)
300+
301+
302+
class TestConditionalFormatSave(unittest.TestCase):
303+
304+
schema_name = 'lists'
305+
query_name = 'TheTestList_cf'
306+
307+
def setUp(self):
308+
self.test_domain = Domain(**{
309+
'container': 'TestContainer',
310+
'description': 'A Test Domain',
311+
'domain_id': 5314,
312+
'fields': [{
313+
'name': 'theKey',
314+
'rangeURI': 'int'
315+
}]
316+
})
317+
318+
self.test_domain.fields[0].conditional_formats = [
319+
# create conditional format using our utility for a QueryFilter
320+
conditional_format(
321+
background_color='fcba03',
322+
bold=True,
323+
italic=True,
324+
query_filter=QueryFilter('theKey', 200),
325+
strike_through=True,
326+
text_color='f44e3b',
327+
),
328+
# create conditional format using our utility for a QueryFilter list
329+
conditional_format(
330+
background_color='fcba03',
331+
bold=True,
332+
italic=True,
333+
query_filter=[
334+
QueryFilter('theKey', 500, QueryFilter.Types.GREATER_THAN),
335+
QueryFilter('theKey', 1000, QueryFilter.Types.LESS_THAN)
336+
],
337+
strike_through=True,
338+
text_color='f44e3b',
339+
)
340+
]
341+
342+
class MockSave(MockLabKey):
343+
api = 'saveDomain.api'
344+
default_action = domain_controller
345+
default_success_body = {}
346+
347+
self.service = MockSave()
348+
349+
payload = {
350+
'domainDesign': self.test_domain.to_json(),
351+
'queryName': self.query_name,
352+
'schemaName': self.schema_name
353+
}
354+
355+
self.expected_kwargs = {
356+
'expected_args': [self.service.get_server_url()],
357+
'data': json.dumps(payload),
358+
'headers': {'Content-Type': 'application/json'},
359+
'timeout': 300
360+
}
361+
362+
self.args = [
363+
mock_server_context(self.service), self.schema_name, self.query_name, self.test_domain
364+
]
365+
366+
def test_success(self):
367+
test = self
368+
success_test(test, self.service.get_successful_response(),
369+
save, True, *self.args, **self.expected_kwargs)
370+
371+
def test_unauthorized(self):
372+
test = self
373+
throws_error_test(test, RequestAuthorizationError, self.service.get_unauthorized_response(),
374+
save, *self.args, **self.expected_kwargs)
375+
376+
247377
def suite():
248378
load_tests = unittest.TestLoader().loadTestsFromTestCase
249379
return unittest.TestSuite([
250380
load_tests(TestCreate),
251381
load_tests(TestDrop),
252382
load_tests(TestGet),
253383
load_tests(TestInferFields),
254-
load_tests(TestSave)
384+
load_tests(TestSave),
385+
load_tests(TestConditionalFormatCreate),
386+
load_tests(TestConditionalFormatSave)
255387
])
256388

257389

0 commit comments

Comments
 (0)