Mercurial > libervia-backend
comparison src/memory/memory.py @ 722:04aabc3f2684
core (memory): fixed setDefault behaviour + minor refactoring
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 28 Nov 2013 17:23:08 +0100 |
parents | 59c9a7ff903d |
children | e07afabc4a25 |
comparison
equal
deleted
inserted
replaced
721:0077912bc9ba | 722:04aabc3f2684 |
---|---|
32 from sat.memory.persistent import PersistentDict | 32 from sat.memory.persistent import PersistentDict |
33 from sat.core import exceptions | 33 from sat.core import exceptions |
34 | 34 |
35 SAVEFILE_DATABASE = "/sat.db" | 35 SAVEFILE_DATABASE = "/sat.db" |
36 NO_SECURITY_LIMIT = -1 | 36 NO_SECURITY_LIMIT = -1 |
37 INDIVIDUAL = "individual" | |
38 GENERAL = "general" | |
37 | 39 |
38 | 40 |
39 class Params(object): | 41 class Params(object): |
40 """This class manage parameters with xml""" | 42 """This class manage parameters with xml""" |
41 ### TODO: add desciption in params | 43 ### TODO: add desciption in params |
237 import_node(node, child) | 239 import_node(node, child) |
238 | 240 |
239 import_node(self.dom.documentElement, src_dom.documentElement) | 241 import_node(self.dom.documentElement, src_dom.documentElement) |
240 | 242 |
241 def __default_ok(self, value, name, category): | 243 def __default_ok(self, value, name, category): |
242 #FIXME: gof: will not work with individual parameters | 244 #FIXME: will not work with individual parameters |
243 self.setParam(name, value, category) # FIXME: better to set param xml value ??? | 245 self.setParam(name, value, category) |
244 | 246 |
245 def __default_ko(self, failure, name, category): | 247 def __default_ko(self, failure, name, category): |
246 error(_("Can't determine default value for [%(category)s/%(name)s]: %(reason)s") % {'category': category, 'name': name, 'reason': str(failure.value)}) | 248 error(_("Can't determine default value for [%(category)s/%(name)s]: %(reason)s") % {'category': category, 'name': name, 'reason': str(failure.value)}) |
247 | 249 |
248 def setDefault(self, name, category, callback, errback=None): | 250 def setDefault(self, name, category, callback, errback=None): |
252 @param category: category of the parameter | 254 @param category: category of the parameter |
253 @param callback: must return a string with the value (use deferred if needed) | 255 @param callback: must return a string with the value (use deferred if needed) |
254 @param errback: must manage the error with args failure, name, category | 256 @param errback: must manage the error with args failure, name, category |
255 """ | 257 """ |
256 #TODO: send signal param update if value changed | 258 #TODO: send signal param update if value changed |
257 node = self.__getParamNode(name, category, '@ALL@') | 259 #TODO: manage individual paramaters |
260 debug ("setDefault called for %(category)s/%(name)s" % {"category": category, "name": name}) | |
261 node = self._getParamNode(name, category, '@ALL@') | |
258 if not node: | 262 if not node: |
259 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) | 263 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) |
260 return | 264 return |
261 if node[1].getAttribute('default_cb') == 'yes': | 265 if node[1].getAttribute('default_cb') == 'yes': |
262 del node[1].attributes['default_cb'] | 266 # del node[1].attributes['default_cb'] # default_cb is not used anymore as a flag to know if we have to set the default value, |
263 d = defer.maybeDeferred(callback) | 267 # and we can still use it later e.g. to call a generic setDefault method |
264 d.addCallback(self.__default_ok, name, category) | 268 value = self._getParam(category, name, GENERAL) |
265 d.addErrback(errback or self.__default_ko, name, category) | 269 if value is None: # no value set by the user: we have the default value |
266 | 270 debug ("Default value to set, using callback") |
267 def __getAttr(self, node, attr, value): | 271 d = defer.maybeDeferred(callback) |
272 d.addCallback(self.__default_ok, name, category) | |
273 d.addErrback(errback or self.__default_ko, name, category) | |
274 | |
275 def _getAttr(self, node, attr, value): | |
268 """ get attribute value | 276 """ get attribute value |
269 @param node: XML param node | 277 @param node: XML param node |
270 @param attr: name of the attribute to get (e.g.: 'value' or 'type') | 278 @param attr: name of the attribute to get (e.g.: 'value' or 'type') |
271 @param value: user defined value""" | 279 @param value: user defined value""" |
272 if attr == 'value': | 280 if attr == 'value': |
293 @param attr: name of the attribute (default: "value") | 301 @param attr: name of the attribute (default: "value") |
294 @param profile: owner of the param (@ALL@ for everyone) | 302 @param profile: owner of the param (@ALL@ for everyone) |
295 | 303 |
296 @return: attribute""" | 304 @return: attribute""" |
297 #FIXME: looks really dirty and buggy, need to be reviewed/refactored | 305 #FIXME: looks really dirty and buggy, need to be reviewed/refactored |
298 node = self.__getParamNode(name, category) | 306 node = self._getParamNode(name, category) |
299 if not node: | 307 if not node: |
300 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) | 308 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) |
301 raise exceptions.NotFound | 309 raise exceptions.NotFound |
302 | 310 |
303 if node[0] == 'general': | 311 if node[0] == GENERAL: |
304 value = self.__getParam(None, category, name, 'general') | 312 value = self._getParam(category, name, GENERAL) |
305 return self.__getAttr(node[1], attr, value) | 313 return self._getAttr(node[1], attr, value) |
306 | 314 |
307 assert node[0] == 'individual' | 315 assert node[0] == INDIVIDUAL |
308 | 316 |
309 profile = self.getProfileName(profile_key) | 317 profile = self.getProfileName(profile_key) |
310 if not profile: | 318 if not profile: |
311 error(_('Requesting a param for an non-existant profile')) | 319 error(_('Requesting a param for an non-existant profile')) |
312 raise exceptions.ProfileUnknownError | 320 raise exceptions.ProfileUnknownError |
314 if profile not in self.params: | 322 if profile not in self.params: |
315 error(_('Requesting synchronous param for not connected profile')) | 323 error(_('Requesting synchronous param for not connected profile')) |
316 raise exceptions.NotConnectedProfileError(profile) | 324 raise exceptions.NotConnectedProfileError(profile) |
317 | 325 |
318 if attr == "value": | 326 if attr == "value": |
319 value = self.__getParam(profile, category, name) | 327 value = self._getParam(category, name, profile=profile) |
320 return self.__getAttr(node[1], attr, value) | 328 return self._getAttr(node[1], attr, value) |
321 | 329 |
322 def asyncGetStringParamA(self, name, category, attr="value", security_limit=NO_SECURITY_LIMIT, profile_key="@NONE@"): | 330 def asyncGetStringParamA(self, name, category, attr="value", security_limit=NO_SECURITY_LIMIT, profile_key="@NONE@"): |
323 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) | 331 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) |
324 d.addCallback(self.__type_to_string) | 332 d.addCallback(self.__type_to_string) |
325 return d | 333 return d |
328 """Helper method to get a specific attribute | 336 """Helper method to get a specific attribute |
329 @param name: name of the parameter | 337 @param name: name of the parameter |
330 @param category: category of the parameter | 338 @param category: category of the parameter |
331 @param attr: name of the attribute (default: "value") | 339 @param attr: name of the attribute (default: "value") |
332 @param profile: owner of the param (@ALL@ for everyone)""" | 340 @param profile: owner of the param (@ALL@ for everyone)""" |
333 node = self.__getParamNode(name, category) | 341 node = self._getParamNode(name, category) |
334 if not node: | 342 if not node: |
335 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) | 343 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) |
336 return None | 344 return None |
337 | 345 |
338 if not self.checkSecurityLimit(node[1], security_limit): | 346 if not self.checkSecurityLimit(node[1], security_limit): |
339 warning(_("Trying to get parameter '%s' in category '%s' without authorization!!!" | 347 warning(_("Trying to get parameter '%s' in category '%s' without authorization!!!" |
340 % (name, category))) | 348 % (name, category))) |
341 return None | 349 return None |
342 | 350 |
343 if node[0] == 'general': | 351 if node[0] == GENERAL: |
344 value = self.__getParam(None, category, name, 'general') | 352 value = self._getParam(category, name, GENERAL) |
345 return defer.succeed(self.__getAttr(node[1], attr, value)) | 353 return defer.succeed(self._getAttr(node[1], attr, value)) |
346 | 354 |
347 assert node[0] == 'individual' | 355 assert node[0] == INDIVIDUAL |
348 | 356 |
349 profile = self.getProfileName(profile_key) | 357 profile = self.getProfileName(profile_key) |
350 if not profile: | 358 if not profile: |
351 error(_('Requesting a param for a non-existant profile')) | 359 error(_('Requesting a param for a non-existant profile')) |
352 return defer.fail() | 360 return defer.fail() |
353 | 361 |
354 if attr != "value": | 362 if attr != "value": |
355 return defer.succeed(node[1].getAttribute(attr)) | 363 return defer.succeed(node[1].getAttribute(attr)) |
356 try: | 364 try: |
357 value = self.__getParam(profile, category, name) | 365 value = self._getParam(category, name, profile=profile) |
358 return defer.succeed(self.__getAttr(node[1], attr, value)) | 366 return defer.succeed(self._getAttr(node[1], attr, value)) |
359 except exceptions.ProfileNotInCacheError: | 367 except exceptions.ProfileNotInCacheError: |
360 #We have to ask data to the storage manager | 368 #We have to ask data to the storage manager |
361 d = self.storage.getIndParam(category, name, profile) | 369 d = self.storage.getIndParam(category, name, profile) |
362 return d.addCallback(lambda value: self.__getAttr(node[1], attr, value)) | 370 return d.addCallback(lambda value: self._getAttr(node[1], attr, value)) |
363 | 371 |
364 def __getParam(self, profile, category, name, _type='individual', cache=None): | 372 def _getParam(self, category, name, type_=INDIVIDUAL, cache=None, profile="@NONE@"): |
365 """Return the param, or None if it doesn't exist | 373 """Return the param, or None if it doesn't exist |
366 @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@) | |
367 @param category: param category | 374 @param category: param category |
368 @param name: param name | 375 @param name: param name |
369 @param _type: "general" or "individual" | 376 @param type_: GENERAL or INDIVIDUAL |
370 @param cache: temporary cache, to use when profile is not logged | 377 @param cache: temporary cache, to use when profile is not logged |
378 @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@) | |
371 @return: param value or None if it doesn't exist | 379 @return: param value or None if it doesn't exist |
372 """ | 380 """ |
373 if _type == 'general': | 381 if type_ == GENERAL: |
374 if (category, name) in self.params_gen: | 382 if (category, name) in self.params_gen: |
375 return self.params_gen[(category, name)] | 383 return self.params_gen[(category, name)] |
376 return None # This general param has the default value | 384 return None # This general param has the default value |
377 assert (_type == 'individual') | 385 assert (type_ == INDIVIDUAL) |
386 if profile == "@NONE@": | |
387 raise exceptions.ProfileNotSetError | |
378 if profile in self.params: | 388 if profile in self.params: |
379 cache = self.params[profile] # if profile is in main cache, we use it, | 389 cache = self.params[profile] # if profile is in main cache, we use it, |
380 # ignoring the temporary cache | 390 # ignoring the temporary cache |
381 elif cache is None: # else we use the temporary cache if it exists, or raise an exception | 391 elif cache is None: # else we use the temporary cache if it exists, or raise an exception |
382 raise exceptions.ProfileNotInCacheError | 392 raise exceptions.ProfileNotInCacheError |
398 # init the result document | 408 # init the result document |
399 prof_xml = minidom.parseString('<params/>') | 409 prof_xml = minidom.parseString('<params/>') |
400 cache = {} | 410 cache = {} |
401 | 411 |
402 for type_node in self.dom.documentElement.childNodes: | 412 for type_node in self.dom.documentElement.childNodes: |
403 if type_node.nodeName != 'general' and type_node.nodeName != 'individual': | 413 if type_node.nodeName != GENERAL and type_node.nodeName != INDIVIDUAL: |
404 continue | 414 continue |
405 # we use all params, general and individual | 415 # we use all params, general and individual |
406 for cat_node in type_node.childNodes: | 416 for cat_node in type_node.childNodes: |
407 if cat_node.nodeName != 'category': | 417 if cat_node.nodeName != 'category': |
408 continue | 418 continue |
434 if name not in dest_params: | 444 if name not in dest_params: |
435 # this is reached when a previous category exists | 445 # this is reached when a previous category exists |
436 dest_params[name] = param_node.cloneNode(True) | 446 dest_params[name] = param_node.cloneNode(True) |
437 dest_cat.appendChild(dest_params[name]) | 447 dest_cat.appendChild(dest_params[name]) |
438 | 448 |
439 profile_value = self.__getParam(profile, category, | 449 profile_value = self._getParam(category, |
440 name, type_node.nodeName, | 450 name, type_node.nodeName, |
441 cache=profile_cache) | 451 cache=profile_cache, profile=profile) |
442 if profile_value is not None: | 452 if profile_value is not None: |
443 # there is a value for this profile, we must change the default | 453 # there is a value for this profile, we must change the default |
444 dest_params[name].setAttribute('value', profile_value) | 454 dest_params[name].setAttribute('value', profile_value) |
445 if new_node: | 455 if new_node: |
446 prof_xml.documentElement.appendChild(dest_cat) | 456 prof_xml.documentElement.appendChild(dest_cat) |
507 return "<category />" | 517 return "<category />" |
508 | 518 |
509 d = self.__constructProfileXml(security_limit, profile) | 519 d = self.__constructProfileXml(security_limit, profile) |
510 return d.addCallback(returnCategoryXml) | 520 return d.addCallback(returnCategoryXml) |
511 | 521 |
512 def __getParamNode(self, name, category, _type="@ALL@"): # FIXME: is _type useful ? | 522 def _getParamNode(self, name, category, type_="@ALL@"): # FIXME: is type_ useful ? |
513 """Return a node from the param_xml | 523 """Return a node from the param_xml |
514 @param name: name of the node | 524 @param name: name of the node |
515 @param category: category of the node | 525 @param category: category of the node |
516 @_type: keyword for search: | 526 @type_: keyword for search: |
517 @ALL@ search everywhere | 527 @ALL@ search everywhere |
518 @GENERAL@ only search in general type | 528 @GENERAL@ only search in general type |
519 @INDIVIDUAL@ only search in individual type | 529 @INDIVIDUAL@ only search in individual type |
520 @return: a tuple with the node type and the the node, or None if not found""" | 530 @return: a tuple with the node type and the the node, or None if not found""" |
521 | 531 |
522 for type_node in self.dom.documentElement.childNodes: | 532 for type_node in self.dom.documentElement.childNodes: |
523 if (((_type == "@ALL@" or _type == "@GENERAL@") and type_node.nodeName == 'general') | 533 if (((type_ == "@ALL@" or type_ == "@GENERAL@") and type_node.nodeName == GENERAL) |
524 or ((_type == "@ALL@" or _type == "@INDIVIDUAL@") and type_node.nodeName == 'individual')): | 534 or ((type_ == "@ALL@" or type_ == "@INDIVIDUAL@") and type_node.nodeName == INDIVIDUAL)): |
525 for node in type_node.getElementsByTagName('category'): | 535 for node in type_node.getElementsByTagName('category'): |
526 if node.getAttribute("name") == category: | 536 if node.getAttribute("name") == category: |
527 params = node.getElementsByTagName("param") | 537 params = node.getElementsByTagName("param") |
528 for param in params: | 538 for param in params: |
529 if param.getAttribute("name") == name: | 539 if param.getAttribute("name") == name: |
546 profile = self.getProfileName(profile_key) | 556 profile = self.getProfileName(profile_key) |
547 if not profile: | 557 if not profile: |
548 error(_('Trying to set parameter for an unknown profile')) | 558 error(_('Trying to set parameter for an unknown profile')) |
549 return # TODO: throw an error | 559 return # TODO: throw an error |
550 | 560 |
551 node = self.__getParamNode(name, category, '@ALL@') | 561 node = self._getParamNode(name, category, '@ALL@') |
552 if not node: | 562 if not node: |
553 error(_('Requesting an unknown parameter (%(category)s/%(name)s)') | 563 error(_('Requesting an unknown parameter (%(category)s/%(name)s)') |
554 % {'category': category, 'name': name}) | 564 % {'category': category, 'name': name}) |
555 return | 565 return |
556 | 566 |
557 if not self.checkSecurityLimit(node[1], security_limit): | 567 if not self.checkSecurityLimit(node[1], security_limit): |
558 warning(_("Trying to set parameter '%s' in category '%s' without authorization!!!" | 568 warning(_("Trying to set parameter '%s' in category '%s' without authorization!!!" |
559 % (name, category))) | 569 % (name, category))) |
560 return | 570 return |
561 | 571 |
562 if node[0] == 'general': | 572 if node[0] == GENERAL: |
563 self.params_gen[(category, name)] = value | 573 self.params_gen[(category, name)] = value |
564 self.storage.setGenParam(category, name, value) | 574 self.storage.setGenParam(category, name, value) |
565 for profile in self.storage.getProfilesList(): | 575 for profile in self.storage.getProfilesList(): |
566 if self.host.isConnected(profile): | 576 if self.host.isConnected(profile): |
567 self.host.bridge.paramUpdate(name, value, category, profile) | 577 self.host.bridge.paramUpdate(name, value, category, profile) |
568 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) | 578 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile) |
569 return | 579 return |
570 | 580 |
571 assert (node[0] == 'individual') | 581 assert (node[0] == INDIVIDUAL) |
572 assert (profile_key != "@NONE@") | 582 assert (profile_key != "@NONE@") |
573 | 583 |
574 _type = node[1].getAttribute("type") | 584 type_ = node[1].getAttribute("type") |
575 if _type == "button": | 585 if type_ == "button": |
576 print "clique", node.toxml() | 586 print "clique", node.toxml() |
577 else: | 587 else: |
578 if self.host.isConnected(profile): # key can not exists if profile is not connected | 588 if self.host.isConnected(profile): # key can not exists if profile is not connected |
579 self.params[profile][(category, name)] = value | 589 self.params[profile][(category, name)] = value |
580 self.host.bridge.paramUpdate(name, value, category, profile) | 590 self.host.bridge.paramUpdate(name, value, category, profile) |
722 def deleteProfile(self, name): | 732 def deleteProfile(self, name): |
723 """Delete an existing profile | 733 """Delete an existing profile |
724 @param name: Name of the profile""" | 734 @param name: Name of the profile""" |
725 return self.params.deleteProfile(name) | 735 return self.params.deleteProfile(name) |
726 | 736 |
727 def addToHistory(self, from_jid, to_jid, message, _type='chat', extra=None, timestamp=None, profile="@NONE@"): | 737 def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile="@NONE@"): |
728 assert profile != "@NONE@" | 738 assert profile != "@NONE@" |
729 if extra is None: | 739 if extra is None: |
730 extra = {} | 740 extra = {} |
731 return self.storage.addToHistory(from_jid, to_jid, message, _type, extra, timestamp, profile) | 741 return self.storage.addToHistory(from_jid, to_jid, message, type_, extra, timestamp, profile) |
732 | 742 |
733 def getHistory(self, from_jid, to_jid, limit=0, between=True, profile="@NONE@"): | 743 def getHistory(self, from_jid, to_jid, limit=0, between=True, profile="@NONE@"): |
734 assert profile != "@NONE@" | 744 assert profile != "@NONE@" |
735 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, profile) | 745 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, profile) |
736 | 746 |
740 @param profile: which profile is using this server ?""" | 750 @param profile: which profile is using this server ?""" |
741 if profile not in self.server_features: | 751 if profile not in self.server_features: |
742 self.server_features[profile] = [] | 752 self.server_features[profile] = [] |
743 self.server_features[profile].append(feature) | 753 self.server_features[profile].append(feature) |
744 | 754 |
745 def addServerIdentity(self, category, _type, entity, profile): | 755 def addServerIdentity(self, category, type_, entity, profile): |
746 """Add an identity discovered from server | 756 """Add an identity discovered from server |
747 @param feature: string of the feature | 757 @param feature: string of the feature |
748 @param profile: which profile is using this server ?""" | 758 @param profile: which profile is using this server ?""" |
749 if not profile in self.server_identities: | 759 if not profile in self.server_identities: |
750 self.server_identities[profile] = {} | 760 self.server_identities[profile] = {} |
751 if (category, _type) not in self.server_identities[profile]: | 761 if (category, type_) not in self.server_identities[profile]: |
752 self.server_identities[profile][(category, _type)] = set() | 762 self.server_identities[profile][(category, type_)] = set() |
753 self.server_identities[profile][(category, _type)].add(entity) | 763 self.server_identities[profile][(category, type_)].add(entity) |
754 | 764 |
755 def getServerServiceEntities(self, category, _type, profile): | 765 def getServerServiceEntities(self, category, type_, profile): |
756 """Return all available entities for a service""" | 766 """Return all available entities for a service""" |
757 if profile in self.server_identities: | 767 if profile in self.server_identities: |
758 return self.server_identities[profile].get((category, _type), set()) | 768 return self.server_identities[profile].get((category, type_), set()) |
759 else: | 769 else: |
760 return None | 770 return None |
761 | 771 |
762 def getServerServiceEntity(self, category, _type, profile): | 772 def getServerServiceEntity(self, category, type_, profile): |
763 """Helper method to get first available entity for a service""" | 773 """Helper method to get first available entity for a service""" |
764 entities = self.getServerServiceEntities(category, _type, profile) | 774 entities = self.getServerServiceEntities(category, type_, profile) |
765 if entities is None: | 775 if entities is None: |
766 warning(_("Entities (%(category)s/%(type)s) not available, maybe they haven't been asked to server yet ?") % {"category": category, | 776 warning(_("Entities (%(category)s/%(type)s) not available, maybe they haven't been asked to server yet ?") % {"category": category, |
767 "type": _type}) | 777 "type": type_}) |
768 return None | 778 return None |
769 else: | 779 else: |
770 return list(entities)[0] if entities else None | 780 return list(entities)[0] if entities else None |
771 | 781 |
772 def hasServerFeature(self, feature, profile_key): | 782 def hasServerFeature(self, feature, profile_key): |
885 try: | 895 try: |
886 del self.entitiesCache[profile][entity_jid.userhost()] | 896 del self.entitiesCache[profile][entity_jid.userhost()] |
887 except KeyError: | 897 except KeyError: |
888 pass | 898 pass |
889 | 899 |
890 def addWaitingSub(self, _type, entity_jid, profile_key): | 900 def addWaitingSub(self, type_, entity_jid, profile_key): |
891 """Called when a subcription request is received""" | 901 """Called when a subcription request is received""" |
892 profile = self.getProfileName(profile_key) | 902 profile = self.getProfileName(profile_key) |
893 assert profile | 903 assert profile |
894 if profile not in self.subscriptions: | 904 if profile not in self.subscriptions: |
895 self.subscriptions[profile] = {} | 905 self.subscriptions[profile] = {} |
896 self.subscriptions[profile][entity_jid] = _type | 906 self.subscriptions[profile][entity_jid] = type_ |
897 | 907 |
898 def delWaitingSub(self, entity_jid, profile_key): | 908 def delWaitingSub(self, entity_jid, profile_key): |
899 """Called when a subcription request is finished""" | 909 """Called when a subcription request is finished""" |
900 profile = self.getProfileName(profile_key) | 910 profile = self.getProfileName(profile_key) |
901 assert profile | 911 assert profile |