From 5353f4e58ba931d186aef9219515a7dbd2e00b24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 11 Nov 2019 20:10:57 +0100
Subject: [PATCH] general: replace unicode() calls by force_text() (#37572)

---
 tests/test_form_pages.py          |  3 +-
 wcs/admin/bounces.py              |  4 ++-
 wcs/admin/users.py                |  4 ++-
 wcs/api.py                        | 11 ++++---
 wcs/backoffice/management.py      | 17 +++++-----
 wcs/ctl/check_hobos.py            |  8 +++--
 wcs/data_sources.py               |  3 +-
 wcs/fields.py                     | 26 +++++++--------
 wcs/formdef.py                    | 30 +++++++++--------
 wcs/portfolio.py                  |  7 ++--
 wcs/qommon/__init__.py            |  5 +--
 wcs/qommon/form.py                |  5 +--
 wcs/qommon/misc.py                | 11 ++++---
 wcs/qommon/ods.py                 |  4 ++-
 wcs/qommon/publisher.py           |  5 +--
 wcs/qommon/saml2.py               |  3 +-
 wcs/qommon/template.py            |  6 ++--
 wcs/qommon/templatetags/qommon.py | 11 ++++---
 wcs/qommon/xml_storage.py         |  3 +-
 wcs/roles.py                      | 11 ++++---
 wcs/sql.py                        |  8 +++--
 wcs/variables.py                  |  9 +++---
 wcs/wf/export_to_model.py         |  3 +-
 wcs/wf/wscall.py                  |  6 ++--
 wcs/workflows.py                  | 53 ++++++++++++++++---------------
 wcs/wscalls.py                    |  7 ++--
 26 files changed, 147 insertions(+), 116 deletions(-)

diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index 678c6a6c5..d673e7a84 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -20,6 +20,7 @@ except ImportError:
     Image = None
 
 from quixote.http_request import Upload as QuixoteUpload
+from django.utils.encoding import force_text
 from wcs.qommon.emails import docutils
 from wcs.qommon.form import UploadedFile
 from wcs.qommon.ident.password_accounts import PasswordAccount
@@ -2017,7 +2018,7 @@ def form_password_field_submit(app, password):
     assert data.data == {'0': {
         'sha1': hashlib.sha1(password).hexdigest(),
         'md5': hashlib.md5(password).hexdigest(),
-        'cleartext': unicode(password, 'utf-8'),
+        'cleartext': force_text(password, 'utf-8'),
         }}
 
 def test_form_password_field_submit(pub):
diff --git a/wcs/admin/bounces.py b/wcs/admin/bounces.py
index 787eafb08..78b434c5a 100644
--- a/wcs/admin/bounces.py
+++ b/wcs/admin/bounces.py
@@ -17,6 +17,8 @@
 import time
 import email.Parser
 
+from django.utils.encoding import force_text
+
 from quixote import get_response, redirect
 from quixote.directory import Directory
 from quixote.html import htmltext, TemplateIO
@@ -100,7 +102,7 @@ class BouncePage(Directory):
             subject, charset = email.Header.decode_header(msg['Subject'])[0]
             if charset:
                 encoding = get_publisher().site_charset
-                r += unicode(subject, charset).encode(encoding)
+                r += force_text(subject, charset).encode(encoding)
             else:
                 r += subject
             r += htmltext('</div>')
diff --git a/wcs/admin/users.py b/wcs/admin/users.py
index 759ec1868..b551603c7 100644
--- a/wcs/admin/users.py
+++ b/wcs/admin/users.py
@@ -14,6 +14,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+from django.utils.encoding import force_text
+
 from quixote import get_publisher, get_response, get_request, get_session, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
@@ -433,7 +435,7 @@ class UsersDirectory(Directory):
         if get_request().form.get('q'):
             q = get_request().form.get('q')
             if type(q) is not unicode:
-                q = unicode(q, get_publisher().site_charset)
+                q = force_text(q, get_publisher().site_charset)
             r += htmltext('<input name="q" value="%s">') % q.encode(get_publisher().site_charset)
         else:
             r += htmltext('<input name="q">')
diff --git a/wcs/api.py b/wcs/api.py
index a74cba7d3..32e1af097 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -22,6 +22,7 @@ import sys
 from quixote import get_request, get_publisher, get_response, get_session
 from quixote.directory import Directory
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urllib
 from django.http import HttpResponse, HttpResponseBadRequest
 
@@ -477,7 +478,7 @@ class ApiFormdefsDirectory(Directory):
                 # anonymous API call, mark authentication as required
                 authentication_required = True
 
-            formdict = {'title': unicode(formdef.name, charset),
+            formdict = {'title': force_text(formdef.name, charset),
                         'slug': formdef.url_name,
                         'url': formdef.get_url(),
                         'description': formdef.description or '',
@@ -509,8 +510,8 @@ class ApiFormdefsDirectory(Directory):
                 formdict['functions'][wf_role_id] = workflow_function
 
             if formdef.category:
-                formdict['category'] = unicode(formdef.category.name, charset)
-                formdict['category_slug'] = unicode(formdef.category.url_name, charset)
+                formdict['category'] = force_text(formdef.category.name, charset)
+                formdict['category_slug'] = force_text(formdef.category.url_name, charset)
                 formdict['category_position'] = (formdef.category.position or 0)
             else:
                 formdict['category_position'] = sys.maxint
@@ -581,11 +582,11 @@ class ApiCategoriesDirectory(Directory):
         all_formdefs = FormDef.select(order_by='name', ignore_errors=True, lightweight=True)
         for category in categories:
             d = {}
-            d['title'] = unicode(category.name, charset)
+            d['title'] = force_text(category.name, charset)
             d['slug'] = category.url_name
             d['url'] = category.get_url()
             if category.description:
-                d['description'] = unicode(str(category.get_description_html_text()), charset)
+                d['description'] = force_text(str(category.get_description_html_text()), charset)
             formdefs = ApiFormdefsDirectory(category).get_list_forms(user,
                     formdefs=all_formdefs, list_all_forms=list_all_forms,
                     backoffice_submission=backoffice_submission)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 0ac6bb0eb..bc6eefd1d 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -26,6 +26,7 @@ try:
 except ImportError:
     xlwt = None
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urllib
 from django.utils.six import StringIO
 
@@ -339,7 +340,7 @@ class UsersViewDirectory(Directory):
         if get_request().form.get('q'):
             q = get_request().form.get('q')
             if type(q) is not unicode:
-                q = unicode(q, get_publisher().site_charset)
+                q = force_text(q, get_publisher().site_charset)
             r += htmltext('<input name="q" value="%s">') % q.encode(get_publisher().site_charset)
         else:
             r += htmltext('<input name="q">')
@@ -1189,7 +1190,7 @@ class FormPage(Directory):
             if get_request().form.get('q'):
                 q = get_request().form.get('q')
                 if type(q) is not unicode:
-                    q = unicode(q, get_publisher().site_charset)
+                    q = force_text(q, get_publisher().site_charset)
                 r += htmltext('<input class="inline-input" name="q" value="%s">') % q.encode(get_publisher().site_charset)
             else:
                 r += htmltext('<input class="inline-input" name="q">')
@@ -1850,21 +1851,21 @@ class FormPage(Directory):
                             get_request().get_server().lower(),
                             formdef.url_name,
                             formdata.id)
-                    vevent.add('summary').value = unicode(formdata.get_display_name(), charset)
+                    vevent.add('summary').value = force_text(formdata.get_display_name(), charset)
                     vevent.add('dtstart').value = make_datetime(formdata.data[start_date_field_id])
                     if end_date_field_id and formdata.data.get(end_date_field_id):
                         vevent.add('dtend').value = make_datetime(formdata.data[end_date_field_id])
                     vevent.dtstart.value_param = 'DATE'
                     backoffice_url = formdata.get_url(backoffice=True)
                     vevent.add('url').value = backoffice_url
-                    form_name = unicode(formdef.name, charset)
-                    status_name = unicode(formdata.get_status_label(), charset)
+                    form_name = force_text(formdef.name, charset)
+                    status_name = force_text(formdata.get_status_label(), charset)
                     description = '%s | %s | %s\n'  % (form_name, formdata.get_display_id(), status_name)
                     description += backoffice_url
                     # TODO: improve performance by loading all users in one
                     # single query before the loop
                     if formdata.user:
-                        description += '\n%s' % unicode(formdata.user.get_display_name(), charset)
+                        description += '\n%s' % force_text(formdata.user.get_display_name(), charset)
                     vevent.add('description').value = description
                     cal.add(vevent)
 
@@ -2533,12 +2534,12 @@ class FormBackOfficeStatusPage(FormStatusPage):
         def safe(v):
             if isinstance(v, str):
                 try:
-                    unicode(v, charset)
+                    force_text(v, charset)
                 except UnicodeDecodeError:
                     v = repr(v)
             else:
                 try:
-                    v = unicode(v).encode(charset)
+                    v = force_text(v).encode(charset)
                 except:
                     v = repr(v)
             return v
diff --git a/wcs/ctl/check_hobos.py b/wcs/ctl/check_hobos.py
index 71db6f52b..9bedd3113 100644
--- a/wcs/ctl/check_hobos.py
+++ b/wcs/ctl/check_hobos.py
@@ -24,6 +24,8 @@ import urllib2
 import urlparse
 import hashlib
 
+from django.utils.encoding import force_text
+
 from quixote import cleanup
 from ..qommon import misc
 from ..qommon.ctl import Command, make_option
@@ -337,7 +339,7 @@ class CmdCheckHobos(Command):
             if not admin_attribute:
                 admin_attribute = 'is_superuser=true'
             else:
-                admin_attribute = unicode(admin_attribute).encode('utf-8')
+                admin_attribute = force_text(admin_attribute).encode('utf-8')
             admin_attribute_dict = dict([admin_attribute.split('=')])
             pub.cfg['idp'][key_provider_id]['admin-attributes'] = admin_attribute_dict
             pub.cfg['idp'][key_provider_id]['nameidformat'] = 'unspecified'
@@ -428,8 +430,8 @@ class CmdCheckHobos(Command):
             if not 'variables' in config.sections():
                 config.add_section('variables')
             for key, value in variables.items():
-                key = unicode(key).encode('utf-8')
-                value = unicode(value).encode('utf-8')
+                key = force_text(key).encode('utf-8')
+                value = force_text(value).encode('utf-8')
                 config.set('variables', key, value)
 
         if not 'api-secrets' in config.sections():
diff --git a/wcs/data_sources.py b/wcs/data_sources.py
index 1b79669e6..6430673ec 100644
--- a/wcs/data_sources.py
+++ b/wcs/data_sources.py
@@ -18,6 +18,7 @@ import collections
 import hashlib
 import xml.etree.ElementTree as ET
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urllib
 from django.utils.six.moves.urllib import parse as urlparse
 
@@ -317,7 +318,7 @@ class NamedDataSource(XmlStorableObject):
     def export_data_source_to_xml(self, element, attribute_name, charset):
         data_source = getattr(self, attribute_name)
         ET.SubElement(element, 'type').text = data_source.get('type')
-        ET.SubElement(element, 'value').text = unicode(data_source.get('value') or '', charset)
+        ET.SubElement(element, 'value').text = force_text(data_source.get('value') or '', charset)
 
     def import_data_source_from_xml(self, element, charset):
         return {
diff --git a/wcs/fields.py b/wcs/fields.py
index bdf3f1bb0..c4db30386 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -28,7 +28,7 @@ import collections
 from quixote import get_request, get_publisher
 from quixote.html import htmltext, TemplateIO
 
-from django.utils.encoding import smart_text
+from django.utils.encoding import force_text, smart_text
 from django.utils.formats import date_format as django_date_format
 from django.utils.html import urlize
 from django.utils.six.moves.html_parser import HTMLParser
@@ -193,7 +193,7 @@ class Field(object):
     @property
     def unhtmled_label(self):
         charset = get_publisher().site_charset
-        return HTMLParser().unescape(unicode(
+        return HTMLParser().unescape(force_text(
             re.sub('<.*?>', ' ', self.label), charset)).strip().encode(charset)
 
     def get_admin_attributes(self):
@@ -241,11 +241,11 @@ class Field(object):
                 if type(val) is dict:
                     for k, v in val.items():
                         if isinstance(v, str):
-                            text_value = unicode(v, charset, 'replace')
+                            text_value = force_text(v, charset, errors='replace')
                         else:
                             # field having non str value in dictionnary field must overload
                             # import_to_xml to handle import
-                            text_value = unicode(v)
+                            text_value = force_text(v)
                         ET.SubElement(el, k).text = text_value
                 elif type(val) is list:
                     if attribute[-1] == 's':
@@ -253,12 +253,12 @@ class Field(object):
                     else:
                         atname = 'item'
                     for v in val:
-                        ET.SubElement(el, atname).text = unicode(v, charset, 'replace')
+                        ET.SubElement(el, atname).text = force_text(v, charset, errors='replace')
                 elif type(val) in (str, unicode):
                     if type(val) is unicode:
                         el.text = val
                     else:
-                        el.text = unicode(val, charset, 'replace')
+                        el.text = force_text(val, charset, errors='replace')
                 else:
                     el.text = str(val)
         return field
@@ -754,7 +754,7 @@ class StringField(WidgetField):
         value = value or ''
         if value.startswith('http://') or value.startswith('https://'):
             charset = get_publisher().site_charset
-            value = unicode(value, charset)
+            value = force_text(value, charset)
             return htmltext(urlize(value, nofollow=True, autoescape=True).encode(charset))
         return str(value)
 
@@ -1786,12 +1786,12 @@ class PageField(Field):
         for post_condition in self.post_conditions:
             post_condition_node = ET.SubElement(conditions_node, 'post_condition')
             condition_node = ET.SubElement(post_condition_node, 'condition')
-            ET.SubElement(condition_node, 'type').text = unicode(
-                    post_condition['condition'].get('type') or '', charset, 'replace')
-            ET.SubElement(condition_node, 'value').text = unicode(
-                    post_condition['condition'].get('value') or '', charset, 'replace')
-            ET.SubElement(post_condition_node, 'error_message').text = unicode(
-                    post_condition['error_message'] or '', charset, 'replace')
+            ET.SubElement(condition_node, 'type').text = force_text(
+                    post_condition['condition'].get('type') or '', charset, errors='replace')
+            ET.SubElement(condition_node, 'value').text = force_text(
+                    post_condition['condition'].get('value') or '', charset, errors='replace')
+            ET.SubElement(post_condition_node, 'error_message').text = force_text(
+                    post_condition['error_message'] or '', charset, errors='replace')
 
     def fill_admin_form(self, form):
         form.add(StringWidget, 'label', title = _('Label'), value = self.label,
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 5d35fafed..b99904a4f 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -24,6 +24,8 @@ import json
 import xml.etree.ElementTree as ET
 import datetime
 
+from django.utils.encoding import force_text
+
 from quixote import get_request, get_publisher
 from quixote.http_request import Upload
 
@@ -721,11 +723,11 @@ class FormDef(StorableObject):
     def export_to_json(self, include_id=False, indent=None, anonymise=True):
         charset = get_publisher().site_charset
         root = {}
-        root['name'] = unicode(self.name, charset)
+        root['name'] = force_text(self.name, charset)
         if include_id and self.id:
             root['id'] = str(self.id)
         if self.category:
-            root['category'] = unicode(self.category.name, charset)
+            root['category'] = force_text(self.category.name, charset)
             root['category_id'] = str(self.category.id)
         if self.workflow:
             root['workflow'] = self.workflow.get_json_export_dict(include_id=include_id)
@@ -868,7 +870,7 @@ class FormDef(StorableObject):
         for text_attribute in list(self.TEXT_ATTRIBUTES):
             if not hasattr(self, text_attribute) or not getattr(self, text_attribute):
                 continue
-            ET.SubElement(root, text_attribute).text = unicode(
+            ET.SubElement(root, text_attribute).text = force_text(
                     getattr(self, text_attribute), charset)
         for boolean_attribute in self.BOOLEAN_ATTRIBUTES:
             if not hasattr(self, boolean_attribute):
@@ -882,13 +884,13 @@ class FormDef(StorableObject):
 
         if self.category:
             elem = ET.SubElement(root, 'category')
-            elem.text = unicode(self.category.name, charset)
+            elem.text = force_text(self.category.name, charset)
             if include_id:
                 elem.attrib['category_id'] = str(self.category.id)
 
         if self.workflow:
             elem = ET.SubElement(root, 'workflow')
-            elem.text = unicode(self.workflow.name, charset)
+            elem.text = force_text(self.workflow.name, charset)
             if include_id:
                 elem.attrib['workflow_id'] = str(self.workflow.id)
 
@@ -921,12 +923,12 @@ class FormDef(StorableObject):
                     continue
                 role_id = str(role_id)
                 if role_id.startswith('_') or role_id == 'logged-users':
-                    role = unicode(role_id, charset)
+                    role = force_text(role_id, charset)
                 else:
                     try:
-                        role = unicode(Role.get(role_id).name, charset)
+                        role = force_text(Role.get(role_id).name, charset)
                     except KeyError:
-                        role = unicode(role_id, charset)
+                        role = force_text(role_id, charset)
                 sub = ET.SubElement(roles, 'role')
                 if include_id:
                     sub.attrib['role_id'] = role_id
@@ -939,12 +941,12 @@ class FormDef(StorableObject):
                     continue
                 role_id = str(role_id)
                 if role_id.startswith('_') or role_id == 'logged-users':
-                    role = unicode(role_id, charset)
+                    role = force_text(role_id, charset)
                 else:
                     try:
-                        role = unicode(Role.get(role_id).name, charset)
+                        role = force_text(Role.get(role_id).name, charset)
                     except KeyError:
-                        role = unicode(role_id, charset)
+                        role = force_text(role_id, charset)
                 sub = ET.SubElement(roles, 'role')
                 sub.attrib['role_key'] = role_key
                 if include_id:
@@ -957,7 +959,7 @@ class FormDef(StorableObject):
             element.attrib['varname'] = option
             option_value = self.workflow_options.get(option)
             if isinstance(option_value, basestring):
-                element.text = unicode(self.workflow_options.get(option, ''), charset)
+                element.text = force_text(self.workflow_options.get(option, ''), charset)
             elif hasattr(option_value, 'base_filename'):
                 ET.SubElement(element, 'filename').text = option_value.base_filename
                 ET.SubElement(element, 'content_type').text = (
@@ -973,12 +975,12 @@ class FormDef(StorableObject):
         for geoloc_key, geoloc_label in (self.geolocations or {}).items():
             element = ET.SubElement(geolocations, 'geolocation')
             element.attrib['key'] = geoloc_key
-            element.text = unicode(geoloc_label, charset)
+            element.text = force_text(geoloc_label, charset)
 
         if self.required_authentication_contexts:
             element = ET.SubElement(root, 'required_authentication_contexts')
             for auth_context in self.required_authentication_contexts:
-                ET.SubElement(element, 'method').text = unicode(auth_context)
+                ET.SubElement(element, 'method').text = force_text(auth_context)
 
         return root
 
diff --git a/wcs/portfolio.py b/wcs/portfolio.py
index 49990796c..eee0eaae7 100644
--- a/wcs/portfolio.py
+++ b/wcs/portfolio.py
@@ -19,6 +19,7 @@ import hashlib
 import urllib
 import base64
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urlparse
 
 from .qommon import get_logger
@@ -85,11 +86,11 @@ def push_document(user, filename, stream):
     charset = get_publisher().site_charset
     payload = {}
     if user.name_identifiers:
-        payload['user_nameid'] = unicode(user.name_identifiers[0], 'ascii')
+        payload['user_nameid'] = force_text(user.name_identifiers[0], 'ascii')
     elif user.email:
-        payload['user_email'] = unicode(user.email, 'ascii')
+        payload['user_email'] = force_text(user.email, 'ascii')
     payload['origin'] = urlparse.urlparse(get_publisher().get_frontoffice_url()).netloc
-    payload['file_name'] = unicode(filename, charset)
+    payload['file_name'] = force_text(filename, charset)
     stream.seek(0)
     payload['file_b64_content'] = base64.b64encode(stream.read())
     async_post = fargo_post_json_async('/api/documents/push/', payload)
diff --git a/wcs/qommon/__init__.py b/wcs/qommon/__init__.py
index 72d430517..a103eddc8 100644
--- a/wcs/qommon/__init__.py
+++ b/wcs/qommon/__init__.py
@@ -19,6 +19,7 @@ import threading
 
 import django.apps
 from django.conf import settings
+from django.utils.encoding import force_text
 from django.utils.six.moves import configparser as ConfigParser
 
 from quixote import get_publisher
@@ -32,13 +33,13 @@ def _(message):
     pub = get_publisher()
     if pub is None:
         return message
-    return unicode(pub.gettext(str(message)), 'utf-8').encode(pub.site_charset)
+    return force_text(pub.gettext(str(message)), 'utf-8').encode(pub.site_charset)
 
 def ngettext(*args):
     pub = get_publisher()
     if pub is None:
         return message
-    return unicode(pub.ngettext(*args), 'utf-8').encode(pub.site_charset)
+    return force_text(pub.ngettext(*args), 'utf-8').encode(pub.site_charset)
 
 from .publisher import get_cfg, get_logger, get_publisher_class
 from . import publisher
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index f55ca7aca..cfe87132e 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -62,6 +62,7 @@ from quixote.form import *
 from quixote.html import htmltext, htmltag, htmlescape, TemplateIO
 from quixote.util import randbytes
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.html_parser import HTMLParser
 from django.utils.six import StringIO
 
@@ -870,7 +871,7 @@ class EmailWidget(StringWidget):
                     self.error = _('invalid address domain')
                     return
                 if not type(domain) is unicode:
-                    domain = unicode(domain, 'utf-8', 'ignore')
+                    domain = force_text(domain, 'utf-8', errors='ignore')
                 try:
                     domain = domain.encode('idna')
                 except UnicodeError:
@@ -1488,7 +1489,7 @@ class WysiwygTextWidget(TextWidget):
             parser = HTMLParser()
             charset = get_publisher().site_charset
             def unquote_django(matchobj):
-                return parser.unescape(unicode(matchobj.group(0), charset)).encode(charset)
+                return parser.unescape(force_text(matchobj.group(0), charset)).encode(charset)
             self.value = re.sub('{[{%](.*?)[%}]}', unquote_django, self.value)
 
     def add_media(self):
diff --git a/wcs/qommon/misc.py b/wcs/qommon/misc.py
index d55104436..545fff20a 100644
--- a/wcs/qommon/misc.py
+++ b/wcs/qommon/misc.py
@@ -37,6 +37,7 @@ except ImportError:
 
 from django.conf import settings
 from django.utils import datetime_safe
+from django.utils.encoding import force_text
 from django.utils.html import strip_tags
 from django.template import engines, TemplateSyntaxError, VariableDoesNotExist
 from django.utils.six.moves.html_parser import HTMLParser
@@ -161,9 +162,9 @@ def simplify(s, space='-'):
         return ''
     if not isinstance(s, unicode):
         if get_publisher() and get_publisher().site_charset:
-            s = unicode('%s' % s, get_publisher().site_charset, 'ignore')
+            s = force_text('%s' % s, get_publisher().site_charset, errors='ignore')
         else:
-            s = unicode('%s' % s, 'iso-8859-1', 'ignore')
+            s = force_text('%s' % s, 'iso-8859-1', errors='ignore')
     s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore')
     s = re.sub(r'[^\w\s\'_\-%s]' % space, '', s).strip().lower()
     s = re.sub(r'[\s\'_\-%s]+' % space, space, s)
@@ -241,12 +242,12 @@ def site_encode(s):
     if isinstance(s, str):
         return s
     if not isinstance(s, unicode):
-        s = unicode(s)
+        s = force_text(s)
     return s.encode(get_publisher().site_charset)
 
 def ellipsize(s, length = 30):
     if type(s) is not unicode:
-        s = unicode(s, get_publisher().site_charset, 'replace')
+        s = force_text(s, get_publisher().site_charset, errors='replace')
     if not s or len(s) < length:
         return s.encode(get_publisher().site_charset)
     return s[:length-5].encode(get_publisher().site_charset) + ' (...)'
@@ -615,7 +616,7 @@ def normalize_geolocation(lat_lon):
 
 def html2text(text):
     if isinstance(text, (htmltext, str)):
-        text = unicode(str(text), get_publisher().site_charset)
+        text = force_text(str(text), get_publisher().site_charset)
     return site_encode(HTMLParser().unescape(strip_tags(text)))
 
 
diff --git a/wcs/qommon/ods.py b/wcs/qommon/ods.py
index c7e26f53c..6cdf03c5a 100644
--- a/wcs/qommon/ods.py
+++ b/wcs/qommon/ods.py
@@ -20,6 +20,8 @@ import re
 import xml.etree.ElementTree as ET
 import zipfile
 
+from django.utils.encoding import force_text
+
 from .misc import date_format, datetime_format, strftime
 from .evalutils import make_date, make_datetime
 
@@ -168,7 +170,7 @@ class WorkCell(object):
         if value is None:
             value = ''
         if type(value) is not unicode:
-            value = unicode(value, 'utf-8')
+            value = force_text(value, 'utf-8')
         self.worksheet = worksheet
         for i in range(0x20):  # remove control characters
             char = chr(i)
diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py
index 70008102e..0e20d1247 100644
--- a/wcs/qommon/publisher.py
+++ b/wcs/qommon/publisher.py
@@ -44,6 +44,7 @@ import xml.etree.ElementTree as ET
 from django.conf import settings
 from django.http import Http404
 from django.utils import translation
+from django.utils.encoding import force_text
 from django.utils.translation import gettext, ngettext
 
 from quixote.publish import Publisher, get_request, get_response, get_publisher, redirect
@@ -180,7 +181,7 @@ class QommonPublisher(Publisher, object):
     def _generate_plaintext_error(self, request, original_response,
                                   exc_type, exc_value, tb, limit = None):
         if exc_value:
-            exc_value = unicode(str(exc_value), errors='ignore').encode('ascii')
+            exc_value = force_text(str(exc_value), errors='ignore').encode('ascii')
         if not self.USE_LONG_TRACES:
             if not request:
                 # this happens when an exception is raised by an afterjob
@@ -269,7 +270,7 @@ class QommonPublisher(Publisher, object):
 
         error_summary = traceback.format_exception_only(exc_type, exc_value)
         error_summary = error_summary[0][0:-1] # de-listify and strip newline
-        error_summary = unicode(str(error_summary), errors='ignore').encode('ascii')
+        error_summary = force_text(str(error_summary), errors='ignore').encode('ascii')
 
         plain_error_msg = self._generate_plaintext_error(request,
                                                          original_response,
diff --git a/wcs/qommon/saml2.py b/wcs/qommon/saml2.py
index 9bd10d2fc..0513cec40 100644
--- a/wcs/qommon/saml2.py
+++ b/wcs/qommon/saml2.py
@@ -24,6 +24,7 @@ try:
 except ImportError:
     lasso = None
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urlparse
 
 from quixote import get_request, get_response, redirect, get_field, get_publisher
@@ -938,7 +939,7 @@ class Saml2Directory(Directory):
 
     def metadata(self):
         try:
-            metadata = unicode(open(misc.get_abs_path(
+            metadata = force_text(open(misc.get_abs_path(
                             get_cfg('sp')['saml2_metadata'])).read(), 'utf-8')
         except KeyError:
             raise errors.TraversalError()
diff --git a/wcs/qommon/template.py b/wcs/qommon/template.py
index bb003e9ef..2ffe5f653 100644
--- a/wcs/qommon/template.py
+++ b/wcs/qommon/template.py
@@ -24,7 +24,7 @@ from django.template import (engines,
                              TemplateSyntaxError as DjangoTemplateSyntaxError,
                              VariableDoesNotExist as DjangoVariableDoesNotExist)
 from django.template.loader import render_to_string
-from django.utils.encoding import smart_text
+from django.utils.encoding import force_text, smart_text
 from django.utils.safestring import SafeString, SafeUnicode
 
 from quixote import get_session, get_request, get_response, get_publisher
@@ -99,7 +99,7 @@ def get_theme_dict(theme_xml):
     publisher = get_publisher()
     def encode_string(x):
         if publisher:
-            return unicode(x).encode(publisher.site_charset)
+            return force_text(x).encode(publisher.site_charset)
         return x
     name = encode_string(tree.attrib['name'])
     version = tree.attrib.get('version')
@@ -533,7 +533,7 @@ def variable_resolve(self, context):
     if isinstance(value, SafeString):
         return SafeUnicode(value, 'utf-8')
     if isinstance(value, str):
-        return unicode(value, 'utf-8')
+        return force_text(value, 'utf-8')
     return value
 
 if not getattr(django.template.base.Variable, 'monkey_patched', False):
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index ccabdc024..ae55dd9b3 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -29,6 +29,7 @@ from pyproj import Geod
 from django import template
 from django.template import defaultfilters
 from django.utils import dateparse
+from django.utils.encoding import force_text
 from django.utils.safestring import mark_safe
 from wcs.qommon import evalutils
 from wcs.qommon import tokens
@@ -45,22 +46,22 @@ def get(mapping, key):
 
 @register.filter
 def startswith(string, substring):
-    return string and unicode(string).startswith(unicode(substring))
+    return string and force_text(string).startswith(force_text(substring))
 
 @register.filter
 def split(string, separator=' '):
     if not string:
         return []
-    return unicode(string).split(unicode(separator))
+    return force_text(string).split(force_text(separator))
 
 @register.filter
 def strip(string, chars=None):
     if not string:
         return ''
     if chars:
-        return unicode(string).strip(unicode(chars))
+        return force_text(string).strip(force_text(chars))
     else:
-        return unicode(string).strip()
+        return force_text(string).strip()
 
 @register.filter
 def parse_date(date_string):
@@ -316,7 +317,7 @@ def token_alphanum(length=4):
 
 @register.filter
 def token_check(token1, token2):
-    return unicode(token1).strip().upper() == unicode(token2).strip().upper()
+    return force_text(token1).strip().upper() == force_text(token2).strip().upper()
 
 
 def get_latlon(obj):
diff --git a/wcs/qommon/xml_storage.py b/wcs/qommon/xml_storage.py
index 83631437f..2f072874c 100644
--- a/wcs/qommon/xml_storage.py
+++ b/wcs/qommon/xml_storage.py
@@ -17,6 +17,7 @@
 import datetime
 import xml.etree.ElementTree as ET
 
+from django.utils.encoding import force_text
 from quixote import get_publisher
 
 from .misc import indent_xml
@@ -53,7 +54,7 @@ class XmlStorableObject(StorableObject):
         return root
 
     def export_str_to_xml(self, element, attribute_name, charset):
-        element.text = unicode(getattr(self, attribute_name), charset)
+        element.text = force_text(getattr(self, attribute_name), charset)
 
     def export_int_to_xml(self, element, attribute_name, charset):
         element.text = str(getattr(self, attribute_name))
diff --git a/wcs/roles.py b/wcs/roles.py
index 73fc4b847..8b649ed4a 100644
--- a/wcs/roles.py
+++ b/wcs/roles.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+from django.utils.encoding import force_text
 from quixote import get_publisher
 
 from .qommon import _, misc
@@ -86,13 +87,13 @@ class Role(StorableObject):
     def get_json_export_dict(self):
         charset = get_publisher().site_charset
         return {
-            'name': unicode(self.name, charset),
-            'text': unicode(self.name, charset), # generic key
+            'name': force_text(self.name, charset),
+            'text': force_text(self.name, charset), # generic key
             'allows_backoffice_access': self.allows_backoffice_access,
-            'emails': [unicode(email, charset) for email in self.emails or []],
-            'details': unicode(self.details or '', charset),
+            'emails': [force_text(email, charset) for email in self.emails or []],
+            'details': force_text(self.details or '', charset),
             'emails_to_members': self.emails_to_members,
-            'slug': unicode(self.slug, charset),
+            'slug': force_text(self.slug, charset),
             'id': self.id}
 
     @classmethod
diff --git a/wcs/sql.py b/wcs/sql.py
index 8d773ba9d..492e732e6 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -22,6 +22,8 @@ import time
 import re
 import cPickle
 
+from django.utils.encoding import force_text
+
 from quixote import get_publisher
 from . import qommon
 from .qommon.storage import _take, parse_clause as parse_storage_clause
@@ -260,10 +262,10 @@ def str_encode(value):
 
 def site_unicode(value):
     if not isinstance(value, basestring):
-        value = unicode(value)
+        value = force_text(value)
     if isinstance(value, unicode):
         return value
-    return unicode(value, get_publisher().site_charset)
+    return force_text(value, get_publisher().site_charset)
 
 def get_connection(new=False):
     if new:
@@ -1175,7 +1177,7 @@ class SqlMixin(object):
                 elif field.key == 'password':
                     d = {}
                     for fmt, val in value:
-                        d[fmt] = unicode(val, 'utf-8')
+                        d[fmt] = force_text(val, 'utf-8')
                     value = d
                 if sql_type == 'date':
                     value = value.timetuple()
diff --git a/wcs/variables.py b/wcs/variables.py
index d0f2d21d5..53702586a 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -16,6 +16,7 @@
 
 import warnings
 
+from django.utils.encoding import force_text
 from django.utils.functional import SimpleLazyObject
 
 from quixote import get_publisher, get_request
@@ -513,7 +514,7 @@ class LazyFieldVar(object):
         if self._field.convert_value_to_str:
             return self._field.convert_value_to_str(value)
         if isinstance(value, str):
-            return unicode(value, get_publisher().site_charset)
+            return force_text(value, get_publisher().site_charset)
         return value
 
     def __str__(self):
@@ -533,13 +534,13 @@ class LazyFieldVar(object):
         return value in self.get_value()
 
     def __unicode__(self):
-        return unicode(str(self), get_publisher().site_charset)
+        return force_text(str(self), get_publisher().site_charset)
 
     def __eq__(self, other):
-        return unicode(self) == unicode(other)
+        return force_text(self) == force_text(other)
 
     def __ne__(self, other):
-        return unicode(self) != unicode(other)
+        return force_text(self) != force_text(other)
 
     def __getitem__(self, key):
         if isinstance(key, (int, slice)):
diff --git a/wcs/wf/export_to_model.py b/wcs/wf/export_to_model.py
index 7fa21fbdd..ded1f6bad 100644
--- a/wcs/wf/export_to_model.py
+++ b/wcs/wf/export_to_model.py
@@ -23,6 +23,7 @@ import subprocess
 import tempfile
 import shutil
 
+from django.utils.encoding import force_text
 from django.utils.six import StringIO
 
 from quixote import get_response, get_request, get_publisher
@@ -420,7 +421,7 @@ class ExportToModel(WorkflowStatusItem):
                 if isinstance(t, unicode):
                     t = t.encode(get_publisher().site_charset)
                 t = template_on_context(context, t, autoescape=False)
-                return unicode(t, get_publisher().site_charset)
+                return force_text(t, get_publisher().site_charset)
             for node in root.iter():
                 got_blank_lines = False
                 # apply template to user-field-decl and update user-field-get
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index 5eaafd132..b2091bf86 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -23,6 +23,8 @@ import collections
 import mimetypes
 from StringIO import StringIO
 
+from django.utils.encoding import force_text
+
 from quixote.html import TemplateIO, htmltext
 
 from ..qommon import _
@@ -477,13 +479,13 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
             if type(key) is unicode:
                 ET.SubElement(item, 'name').text = key
             elif type(key) is str:
-                ET.SubElement(item, 'name').text = unicode(key, charset, 'replace')
+                ET.SubElement(item, 'name').text = force_text(key, charset, errors='replace')
             else:
                 raise AssertionError('unknown type for key (%r)' % key)
             if type(value) is unicode:
                 ET.SubElement(item, 'value').text = value
             elif type(value) is str:
-                ET.SubElement(item, 'value').text = unicode(value, charset, 'replace')
+                ET.SubElement(item, 'value').text = force_text(value, charset, errors='replace')
             else:
                 raise AssertionError('unknown type for value (%r)' % key)
 
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 3cd4fb252..ff28ef36f 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -25,6 +25,7 @@ import sys
 import time
 import uuid
 
+from django.utils.encoding import force_text
 from django.utils.six import StringIO
 
 from quixote import get_request, get_response, redirect
@@ -520,14 +521,14 @@ class Workflow(StorableObject):
         root = ET.Element('workflow')
         if include_id and self.id and not str(self.id).startswith('_'):
             root.attrib['id'] = str(self.id)
-        ET.SubElement(root, 'name').text = unicode(self.name, charset)
+        ET.SubElement(root, 'name').text = force_text(self.name, charset)
 
         roles_node = ET.SubElement(root, 'roles')
         if self.roles:
             for role_id, role_label in self.roles.items():
                 role_node = ET.SubElement(roles_node, 'role')
                 role_node.attrib['id'] = role_id
-                role_node.text = unicode(role_label, charset)
+                role_node.text = force_text(role_label, charset)
 
         if self.last_modification_time:
             elem = ET.SubElement(root, 'last_modification')
@@ -667,7 +668,7 @@ class Workflow(StorableObject):
     def get_json_export_dict(self, include_id=False):
         charset = get_publisher().site_charset
         root = {}
-        root['name'] = unicode(self.name, charset)
+        root['name'] = force_text(self.name, charset)
         if include_id and self.id:
             root['id'] = str(self.id)
         if self.last_modification_time:
@@ -675,14 +676,14 @@ class Workflow(StorableObject):
                                                            self.last_modification_time)
         roles = root['functions'] = {}
         for role, label in self.roles.iteritems():
-            roles[role] = unicode(label, charset)
+            roles[role] = force_text(label, charset)
         statuses = root['statuses'] = []
         endpoint_status_ids = [s.id for s in self.get_endpoint_status()]
         waitpoint_status_ids = [s.id for s in self.get_waitpoint_status()]
         for status in self.possible_status:
             statuses.append({
                 'id': status.id,
-                'name': unicode(status.name, charset),
+                'name': force_text(status.name, charset),
                 'forced_endpoint': status.forced_endpoint,
                 'endpoint': status.id in endpoint_status_ids,
                 'waitpoint': status.id in waitpoint_status_ids,
@@ -845,19 +846,19 @@ class XmlSerialisable(object):
                 val = getattr(self, attribute)
                 if type(val) is dict:
                     for k, v in val.items():
-                        ET.SubElement(el, k).text = unicode(v, charset, 'replace')
+                        ET.SubElement(el, k).text = force_text(v, charset, errors='replace')
                 elif type(val) is list:
                     if attribute[-1] == 's':
                         atname = attribute[:-1]
                     else:
                         atname = 'item'
                     for v in val:
-                        ET.SubElement(el, atname).text = unicode(str(v), charset, 'replace')
+                        ET.SubElement(el, atname).text = force_text(str(v), charset, errors='replace')
                 elif type(val) in (str, unicode):
                     if type(val) is unicode:
                         el.text = val
                     else:
-                        el.text = unicode(val, charset, 'replace')
+                        el.text = force_text(val, charset, errors='replace')
                 else:
                     el.text = str(val)
         return node
@@ -903,12 +904,12 @@ class XmlSerialisable(object):
                 continue
             role_id = str(role_id)
             if role_id.startswith('_') or role_id == 'logged-users':
-                role = unicode(role_id, charset)
+                role = force_text(role_id, charset)
             else:
                 try:
-                    role = unicode(Role.get(role_id).name, charset)
+                    role = force_text(Role.get(role_id).name, charset)
                 except KeyError:
-                    role = unicode(role_id, charset)
+                    role = force_text(role_id, charset)
             sub = ET.SubElement(el, 'item')
             sub.attrib['role_id'] = role_id
             sub.text = role
@@ -928,12 +929,12 @@ class XmlSerialisable(object):
             return
         role_id = str(getattr(self, attribute))
         if role_id.startswith('_') or role_id == 'logged-users':
-            role = unicode(role_id, charset)
+            role = force_text(role_id, charset)
         else:
             try:
-                role = unicode(Role.get(role_id).name, charset)
+                role = force_text(Role.get(role_id).name, charset)
             except KeyError:
-                role_id = role = unicode(role_id, charset)
+                role_id = role = force_text(role_id, charset)
         sub = ET.SubElement(item, attribute)
         if include_id:
             sub.attrib['role_id'] = role_id
@@ -1343,11 +1344,11 @@ class WorkflowGlobalAction(object):
 
     def export_to_xml(self, charset, include_id=False):
         status = ET.Element('action')
-        ET.SubElement(status, 'id').text = unicode(self.id, charset)
-        ET.SubElement(status, 'name').text = unicode(self.name, charset)
+        ET.SubElement(status, 'id').text = force_text(self.id, charset)
+        ET.SubElement(status, 'name').text = force_text(self.name, charset)
 
         if self.backoffice_info_text:
-            ET.SubElement(status, 'backoffice_info_text').text = unicode(
+            ET.SubElement(status, 'backoffice_info_text').text = force_text(
                     self.backoffice_info_text, charset)
 
         items = ET.SubElement(status, 'items')
@@ -1397,10 +1398,10 @@ class WorkflowCriticalityLevel(object):
 
     def export_to_xml(self, charset, include_id=False):
         level = ET.Element('criticality-level')
-        ET.SubElement(level, 'id').text = unicode(self.id, charset) if self.id else ''
-        ET.SubElement(level, 'name').text = unicode(self.name, charset)
+        ET.SubElement(level, 'id').text = force_text(self.id, charset) if self.id else ''
+        ET.SubElement(level, 'name').text = force_text(self.name, charset)
         if self.colour:
-            ET.SubElement(level, 'colour').text = unicode(self.colour, charset)
+            ET.SubElement(level, 'colour').text = force_text(self.colour, charset)
         return level
 
     def init_with_xml(self, elem, charset, include_id=False):
@@ -1629,17 +1630,17 @@ class WorkflowStatus(object):
 
     def export_to_xml(self, charset, include_id=False):
         status = ET.Element('status')
-        ET.SubElement(status, 'id').text = unicode(self.id, charset)
-        ET.SubElement(status, 'name').text = unicode(self.name, charset)
-        ET.SubElement(status, 'colour').text = unicode(self.colour, charset)
+        ET.SubElement(status, 'id').text = force_text(self.id, charset)
+        ET.SubElement(status, 'name').text = force_text(self.name, charset)
+        ET.SubElement(status, 'colour').text = force_text(self.colour, charset)
         if self.extra_css_class:
-            ET.SubElement(status, 'extra_css_class').text = unicode(self.extra_css_class, charset)
+            ET.SubElement(status, 'extra_css_class').text = force_text(self.extra_css_class, charset)
 
         if self.forced_endpoint:
             ET.SubElement(status, 'forced_endpoint').text = 'true'
 
         if self.backoffice_info_text:
-            ET.SubElement(status, 'backoffice_info_text').text = unicode(
+            ET.SubElement(status, 'backoffice_info_text').text = force_text(
                     self.backoffice_info_text, charset)
 
         visibility_node = ET.SubElement(status, 'visibility')
@@ -2293,7 +2294,7 @@ class CommentableWorkflowStatusItem(WorkflowStatusItem):
             if type(self.button_label) is unicode:
                 el.text = self.button_label
             elif type(self.button_label) is str:
-                el.text = unicode(self.button_label, charset, 'replace')
+                el.text = force_text(self.button_label, charset, errors='replace')
             else:
                 raise AssertionError('unknown type for button_label (%r)' % self.button_label)
 
diff --git a/wcs/wscalls.py b/wcs/wscalls.py
index 21321ef21..a4ebb1434 100644
--- a/wcs/wscalls.py
+++ b/wcs/wscalls.py
@@ -19,6 +19,7 @@ import json
 import sys
 import xml.etree.ElementTree as ET
 
+from django.utils.encoding import force_text
 from django.utils.six.moves.urllib import parse as urllib
 from django.utils.six.moves.urllib import parse as urlparse
 
@@ -144,13 +145,13 @@ class NamedWsCall(XmlStorableObject):
     def export_request_to_xml(self, element, attribute_name, charset):
         request = getattr(self, attribute_name)
         for attr in ('url', 'request_signature_key', 'method'):
-            ET.SubElement(element, attr).text = unicode(request.get(attr) or '', charset)
+            ET.SubElement(element, attr).text = force_text(request.get(attr) or '', charset)
         for attr in ('qs_data', 'post_data'):
             data_element = ET.SubElement(element, attr)
             for k, v in (request.get(attr) or {}).items():
                 sub = ET.SubElement(data_element, 'param')
-                sub.attrib['key'] = unicode(k, charset)
-                sub.text = unicode(v, charset)
+                sub.attrib['key'] = force_text(k, charset)
+                sub.text = force_text(v, charset)
         if request.get('post_formdata'):
             ET.SubElement(element, 'post_formdata')
 
-- 
2.24.0

