Commit 6f9e279e authored by Rafael Monnerat's avatar Rafael Monnerat

Speed up News API

See merge request nexedi/slapos.core!540
parents 809dd9b3 eabf48f5
......@@ -108,12 +108,13 @@ class SlapOSComputeNodeMixin(object):
def _fillComputeNodeInformationCache(self, user):
key = '%s_%s' % (self.getReference(), user)
refresh_etag = self._calculateRefreshEtag()
try:
computer_dict = self._getCacheComputeNodeInformation(user)
self._getCachePlugin().set(key, DEFAULT_CACHE_SCOPE,
dict (
time=time.time(),
refresh_etag=self._calculateRefreshEtag(),
refresh_etag=refresh_etag,
data=computer_dict,
# Store the XML while SlapTool Still used
data_xml=self.getPortalObject().portal_slap._getSlapComputeNodeXMLFromDict(computer_dict)
......@@ -130,6 +131,10 @@ class SlapOSComputeNodeMixin(object):
# called on site
pass
# Also update cache for News Dict, so it speed up access of this UI.
key = '%s_partition_news' % self.getReference()
self._getCachedComputePartitionNewsDict(key, refresh_etag)
def _calculateRefreshEtag(self):
# check max indexation timestamp
# it is unlikely to get an empty catalog
......@@ -329,3 +334,61 @@ class SlapOSComputeNodeMixin(object):
self.getReference(), ', '.join([q.getRelativeUrl() for q \
in software_installation_list])
))
def getComputePartitionNewsDict(self):
key = '%s_partition_news' % self.getReference()
cache_plugin = self._getCachePlugin()
refresh_etag = self._calculateRefreshEtag()
try:
entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
except KeyError:
entry = None
if entry is not None and isinstance(entry.getValue(), dict):
cached_dict = entry.getValue()
cached_etag = cached_dict.get('refresh_etag', None)
if (refresh_etag != cached_etag):
return self._getCachedComputePartitionNewsDict(key, refresh_etag)
else:
return cached_dict.get('data')
return self._getCachedComputePartitionNewsDict(key, refresh_etag)
def _getCachedComputePartitionNewsDict(self, key, refresh_etag):
unrestrictedSearchResults = self.getPortalObject().portal_catalog.unrestrictedSearchResults
compute_partition_uid_list = [x.uid for x in unrestrictedSearchResults(
parent_uid=self.getUid(),
validation_state="validated",
portal_type="Compute Partition")]
software_instance_list = unrestrictedSearchResults(
portal_type="Software Instance",
default_aggregate_uid=compute_partition_uid_list,
validation_state="validated",
group_by_list=['default_aggregate_uid'],
select_list=['default_aggregate_uid', 'default_aggregate_title']
)
compute_partition_dict = { }
for software_instance in software_instance_list:
compute_partition_dict[software_instance.default_aggregate_title] = software_instance.getAccessStatus()
try:
self._getCachePlugin().set(key, DEFAULT_CACHE_SCOPE,
dict (
time=time.time(),
refresh_etag=refresh_etag,
data=compute_partition_dict
),
cache_duration=self.getPortalObject().portal_caches\
.getRamCacheRoot().get('compute_node_information_cache_factory'\
).cache_duration
)
except (Unauthorized, IndexError):
# XXX: Unauthorized hack. Race condition of not ready setup delivery which provides
# security information shall not make this method fail, as it will be
# called later anyway
# Note: IndexError ignored, as it happend in case if full reindex is
# called on site
pass
return compute_partition_dict
\ No newline at end of file
......@@ -251,6 +251,7 @@ def makeTestSlapOSCodingStyleTestCase(tested_business_template):
'slapos_hal_json_style/Project_closeRelatedAssignment',
'slapos_hal_json_style/Project_hasItem',
'slapos_hal_json_style/Document_isRequesterOrOwner',
'slapos_hal_json_style/Project_getComputeNodeTrackingList',
'slapos_hal_json_style/SaleInvoiceTransaction_getRelatedInstanceTreeReportLineList',
'slapos_hal_json_style/SaleInvoiceTransaction_getRelatedPaymentTransactionIntegrationId',
'slapos_hal_json_style/SoftwareInstallation_getSoftwareReleaseInformation',
......
......@@ -6,46 +6,12 @@
rJS(window)
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("updatePanel", "updatePanel")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("reload", "reload")
.declareAcquiredMethod("getSetting", "getSetting")
.declareAcquiredMethod("jio_get", "jio_get")
.declareAcquiredMethod("getUrlFor", "getUrlFor")
.declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("jio_getAttachment", "jio_getAttachment")
.declareAcquiredMethod("getTranslationList", "getTranslationList")
.allowPublicAcquisition("jio_allDocs", function (param_list) {
var gadget = this;
return gadget.jio_allDocs(param_list[0])
.push(function (result) {
var i, value, news, len = result.data.total_rows;
for (i = 0; i < len; i += 1) {
if (1 || (result.data.rows[i].value.hasOwnProperty("Project_getNewsDict"))) {
value = result.data.rows[i].id;
news = result.data.rows[i].value.Project_getNewsDict;
result.data.rows[i].value.Project_getNewsDict = {
field_gadget_param : {
css_class: "",
description: "The Status",
hidden: 0,
"default": {jio_key: value, result: news},
key: "status",
url: "gadget_slapos_status.html",
title: "Status",
type: "GadgetField"
}
};
result.data.rows[i].value["listbox_uid:list"] = {
key: "listbox_uid:list",
value: 2713
};
}
}
return result;
});
})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
......
......@@ -238,7 +238,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1001.59376.11750.51012</string> </value>
<value> <string>1009.4189.30038.53811</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -258,7 +258,7 @@
</tuple>
<state>
<tuple>
<float>1659068904.46</float>
<float>1686668301.75</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -28,65 +28,19 @@
var gadget = this;
return gadget.jio_allDocs(param_list[0])
.push(function (result) {
var i, value, value_jio_key, len = result.data.total_rows;
var i, len = result.data.total_rows;
for (i = 0; i < len; i += 1) {
if (result.data.rows[i].value.hasOwnProperty("portal_type")) {
// Use a User-friendly for the Website, this value should be translated
// most liketly
if (result.data.rows[i].value.portal_type === "Compute Node") {
value_jio_key = result.data.rows[i].id;
value = result.data.rows[i].value.Document_getNewsDict;
// Use a User-friendly for the Website, this value should be translated
// most liketly
result.data.rows[i].value.portal_type = "Server";
result.data.rows[i].value.Document_getNewsDict = {
field_gadget_param : {
css_class: "",
description: gadget.description_translation,
hidden: 0,
"default": {jio_key: value_jio_key, result: value},
key: "status",
url: "gadget_slapos_status.html",
title: gadget.title_translation,
type: "GadgetField"
}
};
}
if (result.data.rows[i].value.portal_type === "Instance Tree") {
value_jio_key = result.data.rows[i].id;
value = result.data.rows[i].value.Document_getNewsDict;
// Use a User-friendly for the Website, this value should be translated
// most liketly
result.data.rows[i].value.portal_type = "Service";
result.data.rows[i].value.Document_getNewsDict = {
field_gadget_param : {
css_class: "",
description: gadget.description_translation,
hidden: 0,
"default": {jio_key: value_jio_key, result: value},
key: "status",
url: "gadget_slapos_status.html",
title: gadget.title_translation,
type: "GadgetField"
}
};
}
if (result.data.rows[i].value.portal_type === "Computer Network") {
value_jio_key = result.data.rows[i].id;
value = result.data.rows[i].value.Document_getNewsDict;
// Use a User-friendly for the Website, this value should be translated
// most liketly
result.data.rows[i].value.portal_type = "Network";
result.data.rows[i].value.Document_getNewsDict = {
field_gadget_param : {
css_class: "",
description: gadget.description_translation,
hidden: 0,
"default": {jio_key: value_jio_key, result: value},
key: "status",
url: "gadget_slapos_status.html",
title: gadget.title_translation,
type: "GadgetField"
}
};
}
result.data.rows[i].value["listbox_uid:list"] = {
key: "listbox_uid:list",
......@@ -160,8 +114,7 @@
column_list = [
['title', result[2][0]],
['reference', result[2][1]],
['portal_type', result[2][10]],
['Document_getNewsDict', result[2][8]]
['portal_type', result[2][10]]
];
return result[0].render({
erp5_document: {
......@@ -199,20 +152,6 @@
"hidden": 0,
"type": "TextAreaField"
},
"my_monitoring_status": {
"description": "",
"title": result[2][4],
"default": {jio_key: gadget.state.jio_key,
result: gadget.state.doc.news},
"css_class": "",
"required": 1,
"editable": 0,
"url": "gadget_slapos_status.html",
"sandbox": "",
"key": "monitoring_status",
"hidden": 0,
"type": "GadgetField"
},
"listbox": {
"column_list": column_list,
"show_anchor": 0,
......
......@@ -249,7 +249,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>1000.54051.30712.44322</string> </value>
<value> <string>1009.4197.41431.23244</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -269,7 +269,7 @@
</tuple>
<state>
<tuple>
<float>1655115677.76</float>
<float>1686673000.91</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -2,15 +2,15 @@ from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
compute_node_dict = {}
compute_partition_dict = {}
node_dict = {}
partition_dict = {}
for compute_node in compute_node_list:
news_dict = compute_node.ComputeNode_getNewsDict()
compute_node_dict[compute_node.getReference()] = news_dict["compute_node"]
compute_partition_dict[compute_node.getReference()] = news_dict["partition"]
reference = compute_node.getReference()
node_dict[reference] = compute_node.getAccessStatus()
partition_dict[reference] = compute_node.getComputePartitionNewsDict()
return {"compute_node": compute_node_dict,
"partition": compute_partition_dict,
return {"compute_node": node_dict,
"partition": partition_dict,
"reference": context.getReference(),
"portal_type": context.getPortalType(),
"monitor_url": context.Base_getStatusMonitorUrl()}
"monitor_url": context.Base_getStatusMonitorUrl(compute_node_list=compute_node_list)}
base_url = 'https://monitor.app.officejs.com/#/?page=ojsm_dispatch&query=portal_type:"Software Instance" AND '
if context.getPortalType() == "Organisation":
compute_node_reference = ",".join([ '"' + i.getReference() + '"' for i in context.Organisation_getComputeNodeTrackingList()])
return base_url + "aggregate_reference:(%s)" % compute_node_reference
if context.getPortalType() == "Project":
compute_node_reference = ",".join([ '"' + i.getReference() + '"' for i in context.Project_getComputeNodeTrackingList()])
return base_url + "aggregate_reference:(%s)" % compute_node_reference
if context.getPortalType() == "Computer Network":
compute_node_reference = ",".join([ '"' + i.getReference() + '"' for i in context.getSubordinationRelatedValueList(portal_type="Compute Node")])
if context.getPortalType() in ["Organisation", "Computer Network"]:
if compute_node_list is None:
return ""
compute_node_reference = ",".join([ '"' + i.getReference() + '"' for i in compute_node_list])
return base_url + "aggregate_reference:(%s)" % compute_node_reference
if context.getPortalType() == "Instance Tree":
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<value> <string>compute_node_list=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -2,13 +2,9 @@ from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
def get_compute_partition_dict(reference):
def get_compute_partition_dict():
compute_node_dict = context.getAccessStatus()
compute_partition_dict = { }
for compute_partition in context.objectValues(portal_type="Compute Partition"):
software_instance = compute_partition.getAggregateRelatedValue(portal_type="Software Instance")
if software_instance is not None:
compute_partition_dict[compute_partition.getTitle()] = software_instance.getAccessStatus()
compute_partition_dict = context.getComputePartitionNewsDict()
return {"compute_node": compute_node_dict,
"partition": compute_partition_dict,
......@@ -16,5 +12,4 @@ def get_compute_partition_dict(reference):
"reference": compute_node_dict['reference'],
"monitor_url": context.Base_getStatusMonitorUrl()}
# Use Cache here, at least transactional one.
return get_compute_partition_dict(context.getReference())
return get_compute_partition_dict()
......@@ -120,10 +120,6 @@
<string>reference</string>
<string>Reference</string>
</tuple>
<tuple>
<string>Project_getNewsDict</string>
<string>Status</string>
</tuple>
</list>
</value>
</item>
......
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
return context.Base_getNewsDictFromComputeNodeList(
context.Project_getComputeNodeTrackingList())
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Project_getNewsDict</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -101,7 +101,6 @@
<string>my_reference</string>
<string>my_description</string>
<string>my_destination_decision</string>
<string>my_news</string>
</list>
</value>
</item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>default</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_news</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>default</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Message</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/Project_getNewsDict</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -488,6 +488,11 @@ class TestComputeNode_getNewsDict(TestSlapOSHalJsonStyleMixin):
}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
self.tic()
# Retest so cache is evaludated
news_dict = compute_node.ComputeNode_getNewsDict()
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
def test_stopped(self):
compute_node = self._makeComputeNode()
......@@ -514,6 +519,11 @@ class TestComputeNode_getNewsDict(TestSlapOSHalJsonStyleMixin):
}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
self.tic()
# Retest so cache is evaludated
news_dict = compute_node.ComputeNode_getNewsDict()
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
def test_destroyed(self):
compute_node = self._makeComputeNode()
......@@ -540,6 +550,11 @@ class TestComputeNode_getNewsDict(TestSlapOSHalJsonStyleMixin):
}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
self.tic()
# Retest so cache is evaludated
news_dict = compute_node.ComputeNode_getNewsDict()
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
def test_no_data(self):
compute_node = self._makeComputeNode()
......@@ -561,6 +576,11 @@ class TestComputeNode_getNewsDict(TestSlapOSHalJsonStyleMixin):
}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
self.tic()
# Retest so cache is evaludated
news_dict = compute_node.ComputeNode_getNewsDict()
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
def test_with_instance(self):
compute_node = self._makeComputeNode()
......@@ -596,6 +616,12 @@ class TestComputeNode_getNewsDict(TestSlapOSHalJsonStyleMixin):
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
self.tic()
# Retest so cache is evaludated
news_dict = compute_node.ComputeNode_getNewsDict()
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
class TestComputerNetwork_getNewsDict(TestSlapOSHalJsonStyleMixin):
def test(self):
......@@ -715,69 +741,6 @@ class TestOrganisation_getNewsDict(TestSlapOSHalJsonStyleMixin):
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
class TestProject_getNewsDict(TestSlapOSHalJsonStyleMixin):
@simulate('Project_getComputeNodeTrackingList',
'*args, **kwargs', 'return context.fake_compute_node_list')
def test(self):
project = self._makeProject()
compute_node = self._makeComputeNode()
instance = self._makeInstance()
instance.setAggregateValue(self.partition0)
project.fake_compute_node_list = [compute_node]
self.tic()
self._logFakeAccess(compute_node)
news_dict = project.Project_getNewsDict()
monitor_url = 'https://monitor.app.officejs.com/#/?page=ojsm_dispatch&query=portal_type:"Software Instance" AND aggregate_reference:("%s")' % (
compute_node.getReference()
)
expected_news_dict = {
'monitor_url': monitor_url,
'portal_type': 'Project',
'reference': project.getReference(),
'compute_node':
{ compute_node.getReference():
{u'created_at': u'%s' % self.created_at,
'no_data_since_15_minutes': 0,
'no_data_since_5_minutes': 0,
'portal_type': compute_node.getPortalType(),
'reference': compute_node.getReference(),
u'since': u'%s' % self.created_at,
u'state': u'start_requested',
u'text': u'#access OK',
u'user': u'SlapOS Master'}},
'partition':
{ compute_node.getReference():
{self.partition0.getReference(): {'created_at': self.created_at,
'no_data': 1,
'portal_type': instance.getPortalType(),
'reference': instance.getReference(),
'since': self.created_at,
'state': '',
'text': '#error no data found for %s' % (instance.getReference()),
'user': 'SlapOS Master'}
}
}
}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
def test_no_data(self):
project = self._makeProject()
news_dict = project.Project_getNewsDict()
expected_news_dict = {
'compute_node': {},
'partition': {},
'monitor_url': 'https://monitor.app.officejs.com/#/?page=ojsm_dispatch&query=portal_type:"Software Instance" AND aggregate_reference:()',
'portal_type': 'Project',
'reference': project.getReference()}
self.assertEqual(_decode_with_json(news_dict),
_decode_with_json(expected_news_dict))
class TestPerson_newLogin(TestSlapOSHalJsonStyleMixin):
def test_Person_newLogin_as_superuser(self):
person = self._makePerson(user=0)
......
......@@ -181,17 +181,6 @@
<td>//a[text()="TEST-SLAPOSJS-PROJECT 0"]</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>//td//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//td//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>//a[text()="TEST-SLAPOSJS-PROJECT 0"]</td>
......@@ -203,28 +192,6 @@
<tal:block metal:use-macro="here/Zuite_SlapOSCommonTemplate/macros/assert_page_header" />
</tal:block>
<tr>
<td>waitForElementPresent</td>
<td>//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<!-- Check on listbox -->
<tr>
<td>waitForElementPresent</td>
<td>//td//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>//td//div[contains(@class, 'main-status')]//div[contains(@class, 'ui-btn-ok')]//a[contains(@href, 'COMP-') and contains(text(), 'Node')]</td>
<td></td>
</tr>
<tal:block tal:define="pagination_configuration python: {'header': '(1)', 'footer': '${count} Records'};
dummy python: context.REQUEST.set('mapping', {'count': '1'})">
<tal:block metal:use-macro="here/Zuite_SlapOSCommonTemplate/macros/check_listbox_pagination_text" />
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment