From 59eb84b6597fd2b8f4addeb6f5fa3d41a6b3dd80 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 8 Jul 2021 11:46:00 +0200
Subject: [PATCH] misc: replace get_logger().error() with newer record_error()
 (#55437)

---
 tests/form_pages/test_formdata.py |  7 ++++---
 tests/test_datasource.py          |  6 +++---
 tests/test_fc_auth.py             |  6 ++++--
 wcs/backoffice/management.py      |  2 +-
 wcs/portfolio.py                  | 10 +++++++---
 wcs/qommon/emails.py              | 23 ++++++++++++-----------
 wcs/qommon/ident/franceconnect.py | 23 +++++++++++------------
 wcs/qommon/ident/idp.py           |  3 +--
 wcs/qommon/ident/password.py      |  6 +++---
 wcs/qommon/saml2.py               |  7 +++----
 wcs/root.py                       | 19 ++++++++++---------
 wcs/wf/export_to_model.py         |  7 ++++---
 wcs/wf/notification.py            | 10 +++++-----
 wcs/wf/profile.py                 |  4 ++--
 wcs/wf/register_comment.py        |  7 +++----
 wcs/wf/roles.py                   | 12 +++++++++---
 wcs/workflows.py                  | 17 +++++++++--------
 17 files changed, 91 insertions(+), 78 deletions(-)

diff --git a/tests/form_pages/test_formdata.py b/tests/form_pages/test_formdata.py
index 77947183..6331fb08 100644
--- a/tests/form_pages/test_formdata.py
+++ b/tests/form_pages/test_formdata.py
@@ -761,9 +761,10 @@ def test_formdata_generated_document_odt_to_pdf_download_push_to_portfolio(
         http_post_request.return_value = None, 400, 'null', None  # fail
         resp = resp.form.submit('button_export_to')
         assert http_post_request.call_count == 1
-        assert caplog.records[-1].message.startswith(
-            "file 'template.pdf' failed to be pushed to portfolio of 'Foo"
-        )
+        if pub.is_using_postgresql():
+            assert pub.loggederror_class.select()[0].summary.startswith(
+                "file 'template.pdf' failed to be pushed to portfolio of 'Foo"
+            )
 
     # failed to push to portfolio, but document is here
     resp = resp.follow()  # $form/$id/create_doc
diff --git a/tests/test_datasource.py b/tests/test_datasource.py
index 96027e4b..41f292f9 100644
--- a/tests/test_datasource.py
+++ b/tests/test_datasource.py
@@ -141,12 +141,12 @@ def test_python_datasource_errors(pub, error_email, http_requests, emails, caplo
     # invalid python expression
     datasource = {'type': 'formula', 'value': 'foobar', 'notify_on_errors': True, 'record_on_errors': True}
     assert data_sources.get_items(datasource) == []
-    assert 'Failed to eval() Python data source' in emails.get_latest('subject')
+    assert "Failed to eval() Python data source ('foobar')" in emails.get_latest('subject')
     if pub.is_using_postgresql():
         assert pub.loggederror_class.count() == 1
         logged_error = pub.loggederror_class.select()[0]
         assert logged_error.workflow_id is None
-        assert logged_error.summary == "[DATASOURCE] Failed to eval() Python data source ('foobar')"
+        assert "Failed to eval() Python data source ('foobar')" in logged_error.summary
 
     # expression not iterable
     datasource = {'type': 'formula', 'value': '2', 'notify_on_errors': True, 'record_on_errors': True}
@@ -156,7 +156,7 @@ def test_python_datasource_errors(pub, error_email, http_requests, emails, caplo
         assert pub.loggederror_class.count() == 2
         logged_error = pub.loggederror_class.select()[1]
         assert logged_error.workflow_id is None
-        assert logged_error.summary == "[DATASOURCE] Python data source ('2') gave a non-iterable result"
+        assert "Python data source ('2')" in logged_error.summary
 
 
 def test_python_datasource_with_evalutils(pub):
diff --git a/tests/test_fc_auth.py b/tests/test_fc_auth.py
index fd8990f6..83ff1f44 100644
--- a/tests/test_fc_auth.py
+++ b/tests/test_fc_auth.py
@@ -194,7 +194,8 @@ def test_fc_login_page(caplog):
             }
         )
     )
-    assert 'user did not authorize login' in caplog.records[-1].message
+    if pub.is_using_postgresql():
+        assert 'user did not authorize login' in pub.loggederror_class.select()[-1].summary
     resp = app.get(
         '/ident/fc/callback?%s'
         % urllib.parse.urlencode(
@@ -204,7 +205,8 @@ def test_fc_login_page(caplog):
             }
         )
     )
-    assert 'whatever' in caplog.records[-1].message
+    if pub.is_using_postgresql():
+        assert 'whatever' in pub.loggederror_class.select()[-1].summary
 
     # Login existing user
     def logme(login_url):
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 01cf58a6..681e9d3c 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -203,7 +203,7 @@ class SendCodeFormdefDirectory(Directory):
             try:
                 sms_class.send(sender, [form.get_widget('sms').parse()], message)
             except errors.SMSError as e:
-                get_logger().error(e)
+                get_publisher().record_error(_('Could not send SMS'), formdata=formdata, exception=e)
             get_session().message = ('info', _('SMS with tracking code sent to the user'))
         else:
             # send mail
diff --git a/wcs/portfolio.py b/wcs/portfolio.py
index f76899a2..a7281d24 100644
--- a/wcs/portfolio.py
+++ b/wcs/portfolio.py
@@ -62,13 +62,14 @@ class fargo_post_json_async:
 def push_document(user, filename, stream):
     if not user:
         return
-    charset = get_publisher().site_charset
+    publisher = get_publisher()
+    charset = publisher.site_charset
     payload = {}
     if user.name_identifiers:
         payload['user_nameid'] = force_text(user.name_identifiers[0], 'ascii')
     elif user.email:
         payload['user_email'] = force_text(user.email, 'ascii')
-    payload['origin'] = urllib.parse.urlparse(get_publisher().get_frontoffice_url()).netloc
+    payload['origin'] = urllib.parse.urlparse(publisher.get_frontoffice_url()).netloc
     payload['file_name'] = force_text(filename, charset)
     stream.seek(0)
     payload['file_b64_content'] = force_text(base64.b64encode(stream.read()))
@@ -80,7 +81,10 @@ def push_document(user, filename, stream):
         if status == 200:
             get_logger().info('file %r pushed to portfolio of %r', filename, user.display_name)
         else:
-            get_logger().error('file %r failed to be pushed to portfolio of %r', filename, user.display_name)
+            publisher.record_error(
+                _('file %(filename)r failed to be pushed to portfolio of %(display_name)r')
+                % {'filename': filename, 'display_name': user.display_name}
+            )
 
     if get_response():
         get_response().add_after_job(
diff --git a/wcs/qommon/emails.py b/wcs/qommon/emails.py
index cbd07079..caaa1301 100644
--- a/wcs/qommon/emails.py
+++ b/wcs/qommon/emails.py
@@ -43,7 +43,7 @@ from django.template.loader import render_to_string
 from django.utils.safestring import mark_safe
 from quixote import get_publisher, get_request, get_response
 
-from . import errors, force_str
+from . import _, errors, force_str
 from .admin.emails import EmailsDirectory
 from .publisher import get_cfg, get_logger
 from .template import Template
@@ -350,35 +350,36 @@ def email(
 
 
 def create_smtp_server(emails_cfg, smtp_timeout=None):
+    publisher = get_publisher()
     try:
         s = smtplib.SMTP(emails_cfg.get('smtp_server', None) or 'localhost', timeout=smtp_timeout)
-    except socket.timeout:
-        get_logger().error('Failed to connect to SMTP server (timeout)')
+    except socket.timeout as e:
+        publisher.record_error(_('Failed to connect to SMTP server (timeout)'), exception=e)
         raise errors.EmailError('Failed to connect to SMTP server (timeout)')
     except OSError:
         # XXX: write message in a queue somewhere?
-        get_logger().error('Failed to connect to SMTP server')
+        publisher.record_error(_('Failed to connect to SMTP server'), exception=e)
         raise errors.EmailError('Failed to connect to SMTP server')
     if not s.sock:
-        get_logger().error('Failed to connect to SMTP server')
+        publisher.record_error(_('Failed to connect to SMTP server'))
         raise errors.EmailError('Failed to connect to SMTP server')
     rc_code, ehlo_answer = s.ehlo()
     if rc_code != 250:
-        get_logger().error('Failed to EHLO to SMTP server (%s)', rc_code)
+        publisher.record_error(_('Failed to EHLO to SMTP server (%s)') % rc_code)
         raise errors.EmailError('Failed to EHLO to SMTP server (%s)' % rc_code)
     if b'STARTTLS' in ehlo_answer:
         rc_code = s.starttls()[0]
         if rc_code != 220:
-            get_logger().error('Failed to STARTTLS to SMTP server (%s)', rc_code)
+            publisher.record_error(_('Failed to STARTTLS to SMTP server (%s)') % rc_code)
             raise errors.EmailError('Failed to STARTTLS to SMTP server (%s)' % rc_code)
     if emails_cfg.get('smtp_login'):
         try:
             s.login(emails_cfg.get('smtp_login') or '', emails_cfg.get('smtp_password') or '')
-        except smtplib.SMTPAuthenticationError:
-            get_logger().error('Failed to authenticate to SMTP server')
+        except smtplib.SMTPAuthenticationError as e:
+            publisher.record_error(_('Failed to authenticate to SMTP server'), exception=e)
             raise errors.EmailError('Failed to authenticate to SMTP server')
-        except smtplib.SMTPException:
-            get_logger().error('Failed to authenticate to SMTP server, unknown error.')
+        except smtplib.SMTPException as e:
+            publisher.record_error(_('Failed to authenticate to SMTP server, unknown error.'), exception=e)
             raise errors.EmailError('Failed to authenticate to SMTP server, unknown error.')
     return s
 
diff --git a/wcs/qommon/ident/franceconnect.py b/wcs/qommon/ident/franceconnect.py
index e11a8519..685b7185 100644
--- a/wcs/qommon/ident/franceconnect.py
+++ b/wcs/qommon/ident/franceconnect.py
@@ -28,7 +28,7 @@ from quixote.html import TemplateIO, htmltext
 from wcs.formdata import flatten_dict
 from wcs.workflows import WorkflowStatusItem
 
-from .. import _, get_cfg, get_logger, template
+from .. import _, get_cfg, template
 from ..backoffice.menu import html_top
 from ..form import (
     CompositeWidget,
@@ -324,7 +324,7 @@ class FCAuthMethod(AuthMethod):
         return False
 
     def get_access_token(self, code):
-        logger = get_logger()
+        publisher = get_publisher()
         session = get_session()
         fc_cfg = get_cfg('fc', {})
         client_id = fc_cfg.get('client_id')
@@ -345,11 +345,11 @@ class FCAuthMethod(AuthMethod):
             },
         )
         if status != 200:
-            logger.error('status from FranceConnect token_url is not 200')
+            publisher.record_error(_('Status from FranceConnect token_url is not 200'))
             return None
         result = json_loads(data)
         if 'error' in result:
-            logger.error('FranceConnect code resolution failed: %s', result['error'])
+            publisher.record_error(_('FranceConnect code resolution failed: %s') % result['error'])
             return None
         # check id_token nonce
         id_token = result['id_token']
@@ -358,12 +358,12 @@ class FCAuthMethod(AuthMethod):
         payload = json_loads(base64url_decode(force_bytes(payload)))
         nonce = hashlib.sha256(force_bytes(session.id)).hexdigest()
         if payload['nonce'] != nonce:
-            logger.error('FranceConnect returned nonce did not match')
+            publisher.record_error(_('FranceConnect returned nonce did not match'))
             return None
         return access_token, id_token
 
     def get_user_info(self, access_token):
-        logger = get_logger()
+        publisher = get_publisher()
         dummy, status, data, dummy = http_get_page(
             self.get_user_info_url(),
             headers={
@@ -371,9 +371,9 @@ class FCAuthMethod(AuthMethod):
             },
         )
         if status != 200:
-            logger.error(
-                'status from FranceConnect user_info_url is not 200 but %s and data is' ' %s',
-                status.data[:100],
+            publisher.record_error(
+                _('Status from FranceConnect user_info_url is not 200 but %(status)s and data is %(data)s')
+                % {'status': status, 'data': data[:100]}
             )
             return None
         return json_loads(data)
@@ -452,7 +452,6 @@ class FCAuthMethod(AuthMethod):
         pub = get_publisher()
         request = get_request()
         session = get_session()
-        logger = get_logger()
         state = request.form.get('state', '')
         next_url = (session.extra_user_variables or {}).pop(
             'fc_next_url_' + state, ''
@@ -464,7 +463,7 @@ class FCAuthMethod(AuthMethod):
             if error:
                 # we log only errors whose user is not responsible
                 msg = self.AUTHORIZATION_REQUEST_ERRORS.get(error)
-                logger.error(_('FranceConnect authentication failed: %s'), msg if msg else error)
+                pub.record_error(_('FranceConnect authentication failed: %s') % msg if msg else error)
             return redirect(next_url)
         access_token, id_token = self.get_access_token(request.form['code'])
         if not access_token:
@@ -493,7 +492,7 @@ class FCAuthMethod(AuthMethod):
 
         if not (user.name and user.email):
             # we didn't get useful attributes, forget it.
-            logger.error('failed to get name and/or email attribute from FranceConnect')
+            pub.record_error(_('Failed to get name and/or email attribute from FranceConnect'))
             return redirect(next_url)
 
         user.store()
diff --git a/wcs/qommon/ident/idp.py b/wcs/qommon/ident/idp.py
index 6736847b..73921aa1 100644
--- a/wcs/qommon/ident/idp.py
+++ b/wcs/qommon/ident/idp.py
@@ -29,7 +29,7 @@ from quixote import get_publisher, get_request, get_response, get_session, redir
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
 
-from .. import _, errors, get_cfg, get_logger, misc, saml2utils, template, x509utils
+from .. import _, errors, get_cfg, misc, saml2utils, template, x509utils
 from ..admin.menu import command_icon
 from ..backoffice.menu import html_top
 from ..form import (
@@ -84,7 +84,6 @@ class MethodDirectory(Directory):
         idps = get_cfg('idp', {})
 
         if not lasso:
-            get_logger().error('/login unavailable - lasso is not installed')
             raise Exception("lasso is missing, idp method cannot be used")
 
         if len(idps) == 0:
diff --git a/wcs/qommon/ident/password.py b/wcs/qommon/ident/password.py
index 2ec743c7..a640e0aa 100644
--- a/wcs/qommon/ident/password.py
+++ b/wcs/qommon/ident/password.py
@@ -23,7 +23,7 @@ from quixote.html import TemplateIO, htmltext
 
 from wcs.qommon.admin.texts import TextsDirectory
 
-from .. import _, emails, errors, get_cfg, get_logger, misc, ngettext
+from .. import _, emails, errors, get_cfg, misc, ngettext
 from .. import storage as st
 from .. import template, tokens
 from ..admin.emails import EmailsDirectory
@@ -606,7 +606,7 @@ class MethodDirectory(Directory):
 
         if identities_cfg.get('email-confirmation', False):
             if not user.email:
-                get_logger().error(
+                get_publisher().record_error(
                     _(
                         'Accounts are configured to require confirmation but accounts can be created without emails'
                     )
@@ -624,7 +624,7 @@ class MethodDirectory(Directory):
 
         if passwords_cfg.get('generate', True):
             if not user.email:
-                get_logger().error(
+                get_publisher().record_error(
                     _(
                         'Accounts are configured to have a generated password '
                         'but accounts can be created without emails'
diff --git a/wcs/qommon/saml2.py b/wcs/qommon/saml2.py
index 6c3f5348..523a2dff 100644
--- a/wcs/qommon/saml2.py
+++ b/wcs/qommon/saml2.py
@@ -79,7 +79,9 @@ def soap_endpoint(method):
         try:
             return method(*args, **kwargs)
         except Exception as e:
-            get_logger().error('Exception in method %r: %s; returning a SOAP error' % (method, e))
+            get_publisher().record_error(
+                _('Exception in method %r: returning a SOAP error') % method, exception=e
+            )
             fault = lasso.SoapFault.newFull('Internal Server Error', str(e))
             body = lasso.SoapBody()
             body.any = [fault]
@@ -125,9 +127,6 @@ class Saml2Directory(Directory):
     def _q_traverse(self, path):
         # if lasso is not installed, hide the saml endpoints
         if lasso is None:
-            if does_idp_authentication():
-                rel_path = os.path.join('/saml', *path)
-                get_logger().error('%s unavailable - lasso is not installed' % rel_path)
             raise errors.TraversalError()
         return Directory._q_traverse(self, path)
 
diff --git a/wcs/root.py b/wcs/root.py
index e1ada479..d065c318 100644
--- a/wcs/root.py
+++ b/wcs/root.py
@@ -82,9 +82,9 @@ class LoginDirectory(Directory):
             method = ident_methods[0]
             try:
                 return ident.login(method)
-            except KeyError:
-                msg = 'failed to login with method %s' % method
-                get_logger().error(msg)
+            except KeyError as e:
+                msg = _('Failed to login with method %s') % method
+                get_publisher().record_error(msg, exception=e)
                 return errors.TraversalError(msg)
         else:
             form = Form(enctype='multipart/form-data')
@@ -108,9 +108,10 @@ class LoginDirectory(Directory):
                 else:
                     try:
                         return ident.login(method)
-                    except KeyError:
-                        msg = 'failed to login with method %s' % method
-                        get_logger().error(msg)
+                    except KeyError as e:
+                        get_publisher().record_error(
+                            _('Failed to login with method %s') % method, exception=e
+                        )
                         return errors.TraversalError()
             else:
                 template.html_top(_('Login'))
@@ -148,8 +149,8 @@ class RegisterDirectory(Directory):
             method = ident_methods[0]
             try:
                 return ident.register(method)
-            except KeyError:
-                get_logger().error('failed to register with method %s' % method)
+            except KeyError as e:
+                get_publisher().record_error(_('Failed to register with method %s') % method, exception=e)
                 return errors.TraversalError()
         else:
             form = Form(enctype='multipart/form-data')
@@ -332,7 +333,7 @@ class RootDirectory(Directory):
                         }
                     )
                 except UploadStorageError as e:
-                    get_logger().error('upload storage error: %s' % e)
+                    get_publisher().record_error(_('Upload storage error'), exception=e)
                     results.append({'error': _('failed to store file (system error)')})
 
         get_response().set_content_type('application/json')
diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py
index f771d7f9..fcc90191 100644
--- a/wcs/wf/export_to_model.py
+++ b/wcs/wf/export_to_model.py
@@ -45,7 +45,7 @@ from wcs.workflows import (
     template_on_formdata,
 )
 
-from ..qommon import _, ezt, force_str, get_logger, misc
+from ..qommon import _, ezt, force_str, misc
 from ..qommon.form import (
     CheckboxWidget,
     ComputedExpressionWidget,
@@ -509,8 +509,9 @@ class ExportToModel(WorkflowStatusItem):
                 )
             )
         except TemplateError as e:
-            url = formdata.get_url()
-            get_logger().error('error in template for export to model [%s]: %s' % (url, str(e)))
+            get_publisher().record_error(
+                _('Error in template for export to model'), formdata=formdata, exception=e
+            )
             raise TemplatingError(_('Error in template: %s') % str(e))
 
     def apply_od_template_to_formdata(self, formdata):
diff --git a/wcs/wf/notification.py b/wcs/wf/notification.py
index c15c5340..cba1d4f9 100644
--- a/wcs/wf/notification.py
+++ b/wcs/wf/notification.py
@@ -20,7 +20,7 @@ from quixote import get_publisher
 
 from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
 
-from ..qommon import _, get_logger
+from ..qommon import _
 from ..qommon.form import ComputedExpressionWidget, SingleSelectWidget, StringWidget, TextWidget, WidgetList
 from ..qommon.template import TemplateError
 from .wscall import WebserviceCallStatusItem
@@ -124,16 +124,16 @@ class SendNotificationWorkflowStatusItem(WebserviceCallStatusItem):
         try:
             title = template_on_formdata(formdata, self.compute(self.title, render=False), autoescape=False)
         except TemplateError as e:
-            get_logger().error(
-                'error in template for notification title, ' 'mail could not be generated: %s' % str(e)
+            get_publisher().record_error(
+                _('error in template for notification title, mail could not be generated'), exception=e
             )
             return
 
         try:
             body = template_on_formdata(formdata, self.compute(self.body, render=False), autoescape=False)
         except TemplateError as e:
-            get_logger().error(
-                'error in template for notification body, ' 'mail could not be generated: %s' % str(e)
+            get_publisher().record_error(
+                _('error in template for notification body, mail could not be generated'), exception=e
             )
             return
 
diff --git a/wcs/wf/profile.py b/wcs/wf/profile.py
index 10bf2227..6e46737c 100644
--- a/wcs/wf/profile.py
+++ b/wcs/wf/profile.py
@@ -29,7 +29,7 @@ from ..qommon import _
 from ..qommon.form import CompositeWidget, ComputedExpressionWidget, SingleSelectWidget, WidgetListAsTable
 from ..qommon.ident.idp import is_idp_managing_user_attributes
 from ..qommon.misc import JSONEncoder, http_patch_request
-from ..qommon.publisher import get_cfg, get_logger
+from ..qommon.publisher import get_cfg
 
 
 def user_ws_url(user_uuid):
@@ -207,7 +207,7 @@ class UpdateUserProfileStatusItem(WorkflowStatusItem):
                 url, payload, headers={'Content-type': 'application/json'}
             )
             if status != 200:
-                get_logger().error('failed to update profile for user %r', user)
+                get_publisher().record_error(_('Failed to update profile for user %r') % user)
 
         if get_request():
             get_response().add_after_job(_('Updating user profile'), after_job)
diff --git a/wcs/wf/register_comment.py b/wcs/wf/register_comment.py
index f204f830..7b3bfee8 100644
--- a/wcs/wf/register_comment.py
+++ b/wcs/wf/register_comment.py
@@ -25,7 +25,7 @@ from wcs.workflows import (
     template_on_formdata,
 )
 
-from ..qommon import _, ezt, get_logger
+from ..qommon import _, ezt
 from ..qommon.form import SingleSelectWidget, TextWidget, WidgetList
 from ..qommon.template import TemplateError
 
@@ -139,9 +139,8 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
             formdata.evolution[-1].add_part(JournalEvolutionPart(formdata, self.comment, self.to))
             formdata.store()
         except TemplateError as e:
-            url = formdata.get_url()
-            get_logger().error(
-                'error in template for comment [%s], ' 'comment could not be generated: %s' % (url, str(e))
+            get_publisher().record_error(
+                _('Error in template, comment could not be generated'), formdata=formdata, exception=e
             )
 
 
diff --git a/wcs/wf/roles.py b/wcs/wf/roles.py
index 9a78d2bb..8f277c9d 100644
--- a/wcs/wf/roles.py
+++ b/wcs/wf/roles.py
@@ -26,7 +26,7 @@ from ..qommon import _
 from ..qommon.form import SingleSelectWidgetWithOther
 from ..qommon.ident.idp import is_idp_managing_user_attributes
 from ..qommon.misc import http_delete_request, http_post_request
-from ..qommon.publisher import get_cfg, get_logger
+from ..qommon.publisher import get_cfg
 
 
 def roles_ws_url(role_uuid, user_uuid):
@@ -112,7 +112,10 @@ class AddRoleWorkflowStatusItem(WorkflowStatusItem):
             signed_url = sign_ws_url(url)
             dummy, status, dummy, dummy = http_post_request(signed_url)
             if status != 201:
-                get_logger().error('failed to add role %r to user %r', role, user)
+                get_publisher().record_error(
+                    _('Failed to add role %(role)r to user %(user)r') % {'role': role, 'user': user},
+                    formdata=formdata,
+                )
 
         if get_request():
             get_response().add_after_job(_('Adding role'), after_job)
@@ -183,7 +186,10 @@ class RemoveRoleWorkflowStatusItem(WorkflowStatusItem):
             # pylint: disable=unused-variable
             response, status, data, auth_header = http_delete_request(signed_url)
             if status != 200:
-                get_logger().error('failed to remove role %r from user %r', role, user)
+                get_publisher().record_error(
+                    _('Failed to remove role %(role)r from user %(user)r') % {'role': role, 'user': user},
+                    formdata=formdata,
+                )
 
         if get_request():
             get_response().add_after_job(_('Removing role'), after_job)
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 2ac01b2e..4c65f2fe 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -35,7 +35,7 @@ from .fields import FileField
 from .formdata import Evolution
 from .formdef import FormDef, FormdefImportError
 from .mail_templates import MailTemplate
-from .qommon import _, emails, errors, ezt, force_str, get_cfg, get_logger, misc
+from .qommon import _, emails, errors, ezt, force_str, get_cfg, misc
 from .qommon.form import (
     CheckboxWidget,
     ComputedExpressionWidget,
@@ -2939,7 +2939,6 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
         if not (self.subject and self.body) and not self.mail_template:
             return
 
-        url = formdata.get_url()
         body = self.body
         subject = self.subject
         extra_attachments = None
@@ -2962,8 +2961,8 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
                 formdata, self.compute(body, render=False), autoescape=body.startswith('<')
             )
         except TemplateError as e:
-            get_logger().error(
-                'error in template for email body [%s], ' 'mail could not be generated: %s' % (url, str(e))
+            get_publisher().record_error(
+                _('Error in body template, mail could not be generated'), formdata=formdata, exception=e
             )
             return
 
@@ -2972,8 +2971,8 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
                 formdata, self.compute(subject, render=False), autoescape=False
             )
         except TemplateError as e:
-            get_logger().error(
-                'error in template for email subject [%s], ' 'mail could not be generated: %s' % (url, str(e))
+            get_publisher().record_error(
+                _('Error in subject template, mail could not be generated'), formdata=formdata, exception=e
             )
             return
 
@@ -3146,7 +3145,9 @@ class SendSMSWorkflowStatusItem(WorkflowStatusItem):
         try:
             sms_body = template_on_formdata(formdata, self.compute(self.body, render=False))
         except TemplateError as e:
-            get_logger().error('error in template for sms [%s], sms could not be generated' % str(e))
+            get_publisher().record_error(
+                _('Error in template, sms could not be generated'), formdata=formdata, exception=e
+            )
             return
 
         from .qommon import sms
@@ -3156,7 +3157,7 @@ class SendSMSWorkflowStatusItem(WorkflowStatusItem):
         try:
             sms.SMS.get_sms_class().send(sender, destinations, sms_body)
         except errors.SMSError as e:
-            get_logger().error(e)
+            get_publisher().record_error(_('Could not send SMS'), formdata=formdata, exception=e)
 
 
 register_item_class(SendSMSWorkflowStatusItem)
-- 
2.32.0

