From 742d211cbd576d77c771b436433e4fb628138772 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 17 Apr 2022 14:58:43 +0200
Subject: [PATCH 1/5] misc: force str() on gettext strings used with TemplateIO
 (#64295)

Lazy gettext strings from Django have a __radd__ method in 3.2 and this
makes adding to TemplateIO raise a TypeError (unsupported operand type(s)
for +: 'TemplateIO' and 'str')
---
 wcs/admin/fields.py               |  2 +-
 wcs/admin/forms.py                | 15 +++++++++------
 wcs/admin/roles.py                |  2 +-
 wcs/admin/settings.py             |  2 +-
 wcs/admin/users.py                | 15 ++++++++-------
 wcs/admin/utils.py                |  4 ++--
 wcs/admin/workflows.py            | 20 +++++++++++++-------
 wcs/backoffice/management.py      | 10 ++++++----
 wcs/fields.py                     |  4 ++--
 wcs/forms/common.py               |  9 +++++----
 wcs/forms/preview.py              |  2 +-
 wcs/qommon/admin/emails.py        |  2 +-
 wcs/qommon/errors.py              |  2 +-
 wcs/qommon/form.py                |  2 +-
 wcs/qommon/ident/franceconnect.py |  4 ++--
 wcs/wf/wscall.py                  |  4 ++--
 wcs/workflows.py                  |  4 ++--
 17 files changed, 58 insertions(+), 45 deletions(-)

diff --git a/wcs/admin/fields.py b/wcs/admin/fields.py
index ef7606104..2ae25df93 100644
--- a/wcs/admin/fields.py
+++ b/wcs/admin/fields.py
@@ -489,7 +489,7 @@ class FieldsDirectory(Directory):
                     current_page_no += 1
                 if str(field.id) == self.page_id:
                     r += ' - '
-                    r += _('page %d') % current_page_no
+                    r += str(_('page %d') % current_page_no)
                     r += ' - '
                     r += field.label
         r += htmltext('</h2>')
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 4a2db7e0d..4df7b147d 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -216,10 +216,10 @@ class AdminFieldsDirectory(FieldsDirectory):
         if hasattr(self.objectdef, 'disabled') and self.objectdef.disabled:
             r = TemplateIO(html=True)
             r += htmltext('<div class="warningnotice">')
-            r += _('This form is currently disabled.')
+            r += str(_('This form is currently disabled.'))
             if hasattr(self.objectdef, 'disabled_redirection') and self.objectdef.disabled_redirection:
                 r += htmltext(' (<a href="%s">') % self.objectdef.disabled_redirection
-                r += _('redirection')
+                r += str(_('redirection'))
                 r += htmltext('</a>)')
             r += htmltext(' <a href="../enable?back=fields">%s</a>') % _('Enable')
             r += htmltext('</div>')
@@ -1165,10 +1165,13 @@ class FormDefPage(Directory):
             self.html_top(title=self.formdef.name)
             r = TemplateIO(html=True)
             r += htmltext('<p>')
-            r += _('From %(here)s to %(there)s') % {
-                'here': self.formdef.workflow.name,
-                'there': new_workflow.name,
-            }
+            r += str(
+                _('From %(here)s to %(there)s')
+                % {
+                    'here': self.formdef.workflow.name,
+                    'there': new_workflow.name,
+                }
+            )
             r += htmltext('</p>')
             r += form.render()
             return r.getvalue()
diff --git a/wcs/admin/roles.py b/wcs/admin/roles.py
index 955e1c65b..02134782b 100644
--- a/wcs/admin/roles.py
+++ b/wcs/admin/roles.py
@@ -115,7 +115,7 @@ class RolePage(Directory):
                 r += htmltext('<p>%s</p>') % self.role.details
 
             if self.role.emails:
-                r += _('Emails:')
+                r += str(_('Emails:'))
                 r += htmltext('<ul>')
                 for email in self.role.emails:
                     r += htmltext('<li>%s</li>') % email
diff --git a/wcs/admin/settings.py b/wcs/admin/settings.py
index 6b2119133..8baa38ec9 100644
--- a/wcs/admin/settings.py
+++ b/wcs/admin/settings.py
@@ -403,7 +403,7 @@ class FileTypesDirectory(Directory):
             r += htmltext('</ul>')
         else:
             r += htmltext('<div class="infonotice"><p>')
-            r += _('There are no file type defined at the moment.')
+            r += str(_('There are no file type defined at the moment.'))
             r += htmltext('</p></div>')
 
         r += htmltext('<h3>%s</h3>') % _('New file type')
diff --git a/wcs/admin/users.py b/wcs/admin/users.py
index cfd7ec9d5..222fe5877 100644
--- a/wcs/admin/users.py
+++ b/wcs/admin/users.py
@@ -134,9 +134,10 @@ class UserPage(Directory):
 
         if self.user.deleted_timestamp:
             r += htmltext('<div class="warningnotice">')
-            r += _('Marked as deleted on %(date)s.') % {
-                'date': misc.localstrftime(self.user.deleted_timestamp)
-            }
+            r += str(
+                _('Marked as deleted on %(date)s.')
+                % {'date': misc.localstrftime(self.user.deleted_timestamp)}
+            )
             r += htmltext('</div>')
 
         r += htmltext('<div class="splitcontent-left">')
@@ -193,7 +194,7 @@ class UserPage(Directory):
                 except KeyError:
                     # removed role ?
                     r += htmltext('<li><em>')
-                    r += _('Unknown role (%s)') % k
+                    r += str(_('Unknown role (%s)') % k)
                     r += htmltext('</em></li>')
             r += htmltext('</ul></div>')
             r += htmltext('</div>')
@@ -218,12 +219,12 @@ class UserPage(Directory):
                         label = pid
                     federation = identity.getFederation(pid)
                     r += htmltext('<li>')
-                    r += _('Account federated with %s') % label
+                    r += str(_('Account federated with %s') % label)
                     r += htmltext('<br />')
                     if federation.localNameIdentifier:
-                        r += _("local: ") + federation.localNameIdentifier.content
+                        r += str(_("local: ") + federation.localNameIdentifier.content)
                     if federation.remoteNameIdentifier:
-                        r += _("remote: ") + federation.remoteNameIdentifier.content
+                        r += str(_("remote: ") + federation.remoteNameIdentifier.content)
                     r += htmltext('</li>')
                 r += htmltext('</ul></div>')
 
diff --git a/wcs/admin/utils.py b/wcs/admin/utils.py
index 74592d1e7..8053e18ed 100644
--- a/wcs/admin/utils.py
+++ b/wcs/admin/utils.py
@@ -33,13 +33,13 @@ def last_modification_block(obj):
             if get_request().user and str(get_request().user.id) != user_id:
                 warning_class = 'recent'
         r += htmltext('<p class="last-modification %s">') % warning_class
-        r += _('Last Modification:')
+        r += str(_('Last Modification:'))
         r += ' '
         r += localstrftime(timestamp)
         r += ' '
         if user_id:
             try:
-                r += _('by %s') % get_publisher().user_class.get(user_id).display_name
+                r += str(_('by %s') % get_publisher().user_class.get(user_id).display_name)
             except KeyError:
                 pass
         r += htmltext('</p>')
diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py
index ac464463e..1f9b2b485 100644
--- a/wcs/admin/workflows.py
+++ b/wcs/admin/workflows.py
@@ -803,9 +803,11 @@ class WorkflowStatusPage(Directory):
             r = TemplateIO(html=True)
             r += htmltext('<h2>%s %s</h2>') % (_('Deleting Status:'), self.status.name)
             r += htmltext('<p>')
-            r += _(
-                '''There are forms set to this status, they need to be changed before
+            r += str(
+                _(
+                    '''There are forms set to this status, they need to be changed before
                  this status can be deleted.'''
+                )
             )
             r += htmltext('</p>')
 
@@ -1336,7 +1338,7 @@ class GlobalActionPage(WorkflowStatusPage):
                 r += htmltext('<ul id="items-list" class="biglist readonly">')
             else:
                 r += htmltext('<p class="items">')
-                r += _('Use drag and drop with the handles to reorder items.')
+                r += str(_('Use drag and drop with the handles to reorder items.'))
                 r += htmltext('</p>')
                 r += htmltext('<ul id="items-list" class="biglist sortable">')
             for item in self.action.items:
@@ -1431,9 +1433,11 @@ class GlobalActionPage(WorkflowStatusPage):
         r = TemplateIO(html=True)
         if self.workflow.is_default():
             r += htmltext('<p>')
-            r += _(
-                '''This is the default workflow, you cannot edit it but you can
+            r += str(
+                _(
+                    '''This is the default workflow, you cannot edit it but you can
                  duplicate it to base your own workflow on it.'''
+                )
             )
             r += htmltext('</p>')
         else:
@@ -1636,9 +1640,11 @@ class WorkflowPage(Directory):
 
         if self.workflow.is_default():
             r += htmltext('<p>')
-            r += _(
-                '''This is the default workflow, you cannot edit it but you can
+            r += str(
+                _(
+                    '''This is the default workflow, you cannot edit it but you can
                  duplicate it to base your own workflow on it.'''
+                )
             )
             r += htmltext('</p>')
         elif self.workflow.is_readonly():
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index fb31fc1c5..b47a534c6 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -514,7 +514,7 @@ class ManagementDirectory(Directory):
                     klass = 'badge'
                 r += htmltext('<p class="details %s">' % klass)
                 if no_pending:
-                    r += _('%(pending)s open on %(total)s') % {'pending': no_pending, 'total': no_total}
+                    r += str(_('%(pending)s open on %(total)s') % {'pending': no_pending, 'total': no_total})
                 else:
                     r += ngettext('%(total)s item', '%(total)s items', no_total) % {'total': no_total}
                 r += htmltext('</p>')
@@ -2935,9 +2935,11 @@ class FormBackOfficeStatusPage(FormStatusPage):
         r = TemplateIO(html=True)
         if get_session() and get_session().is_anonymous_submitter(self.filled):
             r += htmltext('<div class="infonotice">')
-            r += _(
-                'This form has been accessed via its tracking code, it is '
-                'therefore displayed like you were also its owner.'
+            r += str(
+                _(
+                    'This form has been accessed via its tracking code, it is '
+                    'therefore displayed like you were also its owner.'
+                )
             )
             r += htmltext('</div>')
         r += super().receipt(*args, **kwargs)
diff --git a/wcs/fields.py b/wcs/fields.py
index b0f7d02e5..9dc787e2f 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -1326,10 +1326,10 @@ class BoolField(WidgetField):
             r += htmltext('<tr>')
             r += htmltext('<td class="label">')
             if o is True:
-                r += _('Yes')
+                r += str(_('Yes'))
                 value = True
             else:
-                r += _('No')
+                r += str(_('No'))
                 value = False
             r += htmltext('</td>')
             no = len([None for x in values if self.convert_value_from_str(x.data.get(self.id)) is value])
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index edf9569cd..0670bfbb4 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -249,9 +249,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
         endpoint_status = self.formdef.workflow.get_endpoint_status()
         r += htmltext('<p>')
         if self.filled.status in ['wf-%s' % x.id for x in endpoint_status]:
-            r += _('Your case has been handled by:')
+            r += str(_('Your case has been handled by:'))
         else:
-            r += _('Your case is handled by:')
+            r += str(_('Your case is handled by:'))
         r += htmltext('</p>')
         r += htmltext('<p id="receiver">')
         r += htmltext(handling_role.details.replace('\n', '<br />'))
@@ -629,8 +629,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
                     visitor_users.append('%s (%s)' % (visitor_name, time_ago))
                 if visitor_users:
                     r += htmltext('<div id="lock-notice" class="infonotice"><p>')
-                    r += _('Be warned forms of this user are also being looked at by: %s.') % ', '.join(
-                        visitor_users
+                    r += str(
+                        _('Be warned forms of this user are also being looked at by: %s.')
+                        % ', '.join(visitor_users)
                     )
                     r += ' '
                     r += htmltext('</p>')
diff --git a/wcs/forms/preview.py b/wcs/forms/preview.py
index 5410868a5..15aedfdc8 100644
--- a/wcs/forms/preview.py
+++ b/wcs/forms/preview.py
@@ -41,7 +41,7 @@ class PreviewFormPage(FormPage):
         self.html_top(self.formdef.name)
         r = TemplateIO(html=True)
         r += htmltext('<div class="warningnotice"><p>')
-        r += _('This was only a preview: form was not actually submitted.')
+        r += str(_('This was only a preview: form was not actually submitted.'))
         r += htmltext(' <a href=".">%s</a>') % _('Start another preview.')
         r += htmltext('</p></div>')
         return r.getvalue()
diff --git a/wcs/qommon/admin/emails.py b/wcs/qommon/admin/emails.py
index ce55d1d80..351044b98 100644
--- a/wcs/qommon/admin/emails.py
+++ b/wcs/qommon/admin/emails.py
@@ -153,7 +153,7 @@ class EmailsDirectory(Directory):
             if mail_redirection:
                 r += htmltext('<div class="infonotice">')
                 r += htmltext('<p>')
-                r += _('Warning: all emails are sent to <%s>') % mail_redirection
+                r += str(_('Warning: all emails are sent to <%s>') % mail_redirection)
                 r += htmltext(' <a href="../debug_options">%s</a>') % _('Debug Options')
                 r += htmltext('</p>')
                 r += htmltext('</div>')
diff --git a/wcs/qommon/errors.py b/wcs/qommon/errors.py
index 8a4d1934e..702029c9e 100644
--- a/wcs/qommon/errors.py
+++ b/wcs/qommon/errors.py
@@ -91,7 +91,7 @@ class InternalServerError(PublishError):
         r = TemplateIO(html=True)
 
         r += htmltext('<p>')
-        r += _('A fatal error happened. It has been recorded and will be available to administrators.')
+        r += str(_('A fatal error happened. It has been recorded and will be available to administrators.'))
         if self.logged_error:
             r += ' (id:%s)' % self.logged_error.id
         r += htmltext('</p>')
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 913e13c4c..4b01fbe2a 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -456,7 +456,7 @@ class CompositeWidget(quixote.form.CompositeWidget):
         r += htmltext('<tr>\n')
         for widget in self.get_widgets():
             r += htmltext('<th scope="col">')
-            r += widget.get_title()
+            r += str(widget.get_title())
             r += htmltext('</th>')
         r += htmltext('</tr>\n')
         return r.getvalue()
diff --git a/wcs/qommon/ident/franceconnect.py b/wcs/qommon/ident/franceconnect.py
index 685b71854..ff78b4983 100644
--- a/wcs/qommon/ident/franceconnect.py
+++ b/wcs/qommon/ident/franceconnect.py
@@ -237,10 +237,10 @@ class MethodAdminDirectory(Directory):
         r += htmltext('<h2>%s</h2>') % self.title
         fc_callback = pub.get_frontoffice_url() + '/ident/fc/callback'
         r += htmltext('<p>')
-        r += _('Callback URL is %s.') % fc_callback
+        r += str(_('Callback URL is %s.') % fc_callback)
         r += htmltext('</p>')
         r += htmltext('<p>')
-        r += _('Logout callback URL is %s.') % get_publisher().get_frontoffice_url()
+        r += str(_('Logout callback URL is %s.') % get_publisher().get_frontoffice_url())
         r += htmltext('</p>')
         r += htmltext('<p>')
         r += htmltext(
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index 1e6061671..5ae5dfe09 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -70,9 +70,9 @@ class JournalWsCallErrorPart(EvolutionPart):
         r += htmltext('<div class="ws-error">')
         r += htmltext('<h4 class="foldable folded">')
         if self.label:
-            r += _('Error during webservice call "%s"') % self.label
+            r += str(_('Error during webservice call "%s"') % self.label)
         else:
-            r += _('Error during webservice call')
+            r += str(_('Error during webservice call'))
         r += htmltext('</h4>')
         r += htmltext('<div>')
         r += htmltext('<p>%s</p>\n') % self.summary
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 8d2cd8d41..7c6860114 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -2371,12 +2371,12 @@ class WorkflowStatusItem(XmlSerialisable):
             return getattr(self, 'get_%s_parameter_view_value' % parameter)()
         value = getattr(self, parameter)
         if isinstance(value, bool):
-            return _('Yes') if value else _('No')
+            return str(_('Yes') if value else _('No'))
         elif hasattr(widget, 'options') and value:
             for option in widget.options:
                 if isinstance(option, tuple):
                     if option[0] == value:
-                        return option[1]
+                        return str(option[1])
                 else:
                     if option == value:
                         return option
-- 
2.35.2

