diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index a51a912b3..bac68ec72 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -8,6 +8,7 @@ from django import forms from django.core.urlresolvers import reverse from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy +from django.utils.encoding import force_text from modoboa.core import signals as core_signals from modoboa.core.models import User @@ -98,7 +99,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if label is not None: self.add_error( "name", _("A %s with this name already exists") - % unicode(label) + % force_text(label) ) condition = ( self.cleaned_data["quota"] != 0 and @@ -120,7 +121,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if default_mailbox_quota == 0: self.add_error("default_mailbox_quota", msg) self.aliases = [] - for k in cleaned_data.keys(): + copied_data = cleaned_data.copy() + for k in copied_data.keys(): if not k.startswith("aliases"): continue if cleaned_data[k] == "": @@ -137,7 +139,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if label is not None: self.add_error( k, _("A %s with this name already exists") - % unicode(label) + % force_text(label) ) else: self.aliases.append(cleaned_data[k]) diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 5232468c8..b137dfa08 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -7,7 +7,9 @@ import random from django.core.urlresolvers import reverse from django.db import models -from django.utils.encoding import python_2_unicode_compatible, smart_text +from django.utils.encoding import ( + python_2_unicode_compatible, smart_text, force_str, force_bytes +) from django.utils.translation import ugettext as _, ugettext_lazy from reversion import revisions as reversion @@ -64,7 +66,7 @@ class Alias(AdminObject): """Generate a random address (local part).""" m = hashlib.md5() for x in random.sample(range(10000000), 60): - m.update(str(x)) + m.update(force_bytes(x)) return m.hexdigest()[:20] @property @@ -111,7 +113,6 @@ class Alias(AdminObject): is defined on the associated domain """ - to_create = [] for address in set(address_list): if not address: continue @@ -195,10 +196,11 @@ class Alias(AdminObject): self.post_create(user) def to_csv(self, csvwriter): - row = ["alias", self.address.encode("utf-8"), self.enabled] + row = ["alias", force_str(self.address), self.enabled] row += self.recipients csvwriter.writerow(row) + reversion.register(Alias) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index a521418b7..516378605 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -8,7 +8,9 @@ from functools import reduce from django.db import models from django.db.models.manager import Manager from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible, smart_text +from django.utils.encoding import ( + python_2_unicode_compatible, smart_text, force_text +) from django.utils.functional import cached_property from django.utils.translation import ugettext as _, ugettext_lazy @@ -281,7 +283,10 @@ class Domain(AdminObject): def to_csv(self, csvwriter): """Export domain and domain aliases to CSV format.""" csvwriter.writerow([ - "domain", self.name, self.quota, self.default_mailbox_quota, + "domain", + force_text(self.name), + self.quota, + self.default_mailbox_quota, self.enabled ]) for dalias in self.domainalias_set.all(): @@ -296,6 +301,7 @@ class Domain(AdminObject): for domalias in self.domainalias_set.all(): domalias.post_create(creator) + reversion.register(Domain) diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index 597860fc0..f069ca13d 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -222,12 +222,12 @@ class DomainTestCase(ModoTestCase): url = reverse("admin:domain_detail", args=[domain.pk]) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertIn("Summary", response.content) - self.assertIn("Administrators", response.content) - self.assertNotIn("Resources usage", response.content) + self.assertIn("Summary", response.content.decode()) + self.assertIn("Administrators", response.content.decode()) + self.assertNotIn("Resources usage", response.content.decode()) self.set_global_parameter("enable_domain_limits", True, app="limits") response = self.client.get(url) - self.assertIn("Resources usage", response.content) + self.assertIn("Resources usage", response.content.decode()) def test_statitics_widget(self): """Test statistics display in dashboard.""" diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 1b47bb906..5c5f7debd 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.core.urlresolvers import reverse +from django.utils.encoding import force_text from modoboa.lib.tests import ModoTestCase @@ -38,8 +39,8 @@ class ExportTestCase(ModoTestCase): ) def assertListEqual(self, list1, list2): - list1 = list1.split("\r\n") - list2 = list2.split("\r\n") + list1 = force_text(list1).split("\r\n") + list2 = force_text(list2).split("\r\n") self.assertEqual(len(list1), len(list2)) for entry in list1: if not entry: @@ -85,7 +86,7 @@ class ExportTestCase(ModoTestCase): response = self.__export_identities( idtfilter="account", grpfilter="SuperAdmins" ) - elements = response.content.strip().split(";") + elements = response.content.decode().strip().split(";") self.assertEqual(len(elements), 9) elements[2] = "" self.assertEqual( @@ -104,6 +105,6 @@ class ExportTestCase(ModoTestCase): def test_export_aliases(self): response = self.__export_identities(idtfilter="alias") self.assertEqual( - response.content.strip(), + response.content.decode().strip(), "alias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com" ) diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index ac3252245..dfb612bd3 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.core.files.base import ContentFile from django.core.urlresolvers import reverse -from django.utils.encoding import force_bytes from modoboa.core.models import User from modoboa.lib.tests import ModoTestCase @@ -84,14 +83,14 @@ domainalias;test.alias;test.com;True "sourcefile": f } ) - self.assertIn('Object already exists: domainalias', resp.content) + self.assertIn('Object already exists: domainalias', resp.content.decode()) def test_identities_import(self): response = self.client.get(reverse("admin:identity_import")) self.assertEqual(response.status_code, 200) self.assertIn("Provide a CSV", response.content.decode()) - f = ContentFile(b""" + f = ContentFile(""" account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 account; Truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com alias; alias1@test.com; True; user1@test.com @@ -193,7 +192,7 @@ domain; domain2.com; 1000; 200; False reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) - self.assertIn("You are not allowed to import domains", resp.content) + self.assertIn("You are not allowed to import domains", resp.content.decode()) f = ContentFile(b""" domainalias; domalias1.com; test.com; True """, name="identities.csv") @@ -202,7 +201,7 @@ domainalias; domalias1.com; test.com; True {"sourcefile": f, "crypt_password": True} ) self.assertIn( - "You are not allowed to import domain aliases", resp.content) + "You are not allowed to import domain aliases", resp.content.decode()) def test_import_quota_too_big(self): self.client.logout() @@ -231,7 +230,7 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com ) def test_import_duplicate(self): - f = ContentFile(b""" + f = ContentFile(""" account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com """, name="identities.csv") diff --git a/modoboa/admin/views/export.py b/modoboa/admin/views/export.py index 1044a9e29..89e6dabb0 100644 --- a/modoboa/admin/views/export.py +++ b/modoboa/admin/views/export.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import csv -import io from rfc6266 import build_header @@ -15,6 +14,7 @@ from django.http import HttpResponse from django.shortcuts import render from django.utils.translation import ugettext as _ from django.utils.encoding import smart_str +from django.utils import six from ..forms import ExportIdentitiesForm, ExportDomainsForm from ..lib import get_domains, get_identities @@ -50,7 +50,7 @@ def export_identities(request): if request.method == "POST": form = ExportIdentitiesForm(request.POST) form.is_valid() - fp = io.BytesIO() + fp = six.StringIO() csvwriter = csv.writer( fp, delimiter=smart_str(form.cleaned_data["sepchar"]) ) @@ -80,7 +80,7 @@ def export_domains(request): if request.method == "POST": form = ExportDomainsForm(request.POST) form.is_valid() - fp = io.BytesIO() + fp = six.StringIO() csvwriter = csv.writer( fp, delimiter=smart_str(form.cleaned_data["sepchar"]) ) diff --git a/modoboa/admin/views/import_.py b/modoboa/admin/views/import_.py index a157e71f9..44921471c 100644 --- a/modoboa/admin/views/import_.py +++ b/modoboa/admin/views/import_.py @@ -14,6 +14,7 @@ from django.db import transaction from django.shortcuts import render from django.utils.translation import ugettext as _ from django.utils.encoding import smart_str, force_text +from django.utils import six from reversion import revisions as reversion @@ -39,8 +40,14 @@ def importdata(request, formclass=ImportDataForm): form = formclass(request.POST, request.FILES) if form.is_valid(): try: + if six.PY2: + infile = request.FILES['sourcefile'] + else: + infile = StringIO( + force_text(request.FILES['sourcefile'].read()) + ) reader = csv.reader( - request.FILES['sourcefile'], + infile, delimiter=smart_str(form.cleaned_data['sepchar']) ) except csv.Error as inst: diff --git a/modoboa/core/models.py b/modoboa/core/models.py index bfd3a173b..baba71d07 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -9,7 +9,8 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone from django.utils.encoding import ( - python_2_unicode_compatible, smart_bytes, smart_text, force_str) + python_2_unicode_compatible, smart_bytes, smart_text, force_str, force_text +) from django.utils.functional import cached_property from django.utils.translation import ugettext as _, ugettext_lazy @@ -411,13 +412,13 @@ class User(PermissionsMixin): """ row = [ "account", - smart_bytes(self.username), - smart_bytes(self.password), - smart_bytes(self.first_name), - smart_bytes(self.last_name), - self.is_active, - self.role, - smart_bytes(self.email) + force_text(self.username), + force_text(self.password), + force_text(self.first_name), + force_text(self.last_name), + force_text(self.is_active), + force_text(self.role), + force_text(self.email) ] results = signals.account_exported.send( sender=self.__class__, user=self) diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index 586ff6efd..a449c8088 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -13,7 +13,7 @@ from random import Random import string from django.utils.crypto import constant_time_compare -from django.utils.encoding import force_text +from django.utils.encoding import force_text, force_bytes class PasswordHasher(object): @@ -36,7 +36,7 @@ class PasswordHasher(object): """ if self._target == 'ldap': return base64.b64encode(pwhash) - return force_text(pwhash) + return pwhash def encrypt(self, clearvalue): """Encrypt a password. @@ -51,8 +51,8 @@ class PasswordHasher(object): :rtype: str :return: encrypted password """ - pwhash = self._b64encode(self._encrypt(clearvalue)) - return '%s%s' % (self.scheme, pwhash) + pwhash = self._b64encode(self._encrypt(force_text(clearvalue))) + return '%s%s' % (self.scheme, force_text(pwhash)) def verify(self, clearvalue, hashed_value): """Verify a password against a hashed value. @@ -109,7 +109,7 @@ class MD5Hasher(PasswordHasher): return '{MD5}' def _encrypt(self, clearvalue, salt=None): - obj = hashlib.md5(clearvalue) + obj = hashlib.md5(force_bytes(clearvalue)) return obj.hexdigest() @@ -125,7 +125,7 @@ class SHA256Hasher(PasswordHasher): return '{SHA256}' def _encrypt(self, clearvalue, salt=None): - return hashlib.sha256(clearvalue).digest() + return hashlib.sha256(force_bytes(clearvalue)).digest() def _b64encode(self, pwhash): """Encode :keyword:`pwhash` using base64. diff --git a/modoboa/lib/ldap_utils.py b/modoboa/lib/ldap_utils.py index b25335c17..57767b69f 100644 --- a/modoboa/lib/ldap_utils.py +++ b/modoboa/lib/ldap_utils.py @@ -40,6 +40,7 @@ import ldap from django.conf import settings from django.utils.translation import ugettext as _ from django.utils.encoding import force_bytes +from django.utils import six from modoboa.core.password_hashers import get_password_hasher from modoboa.lib.exceptions import InternalError diff --git a/tox.ini b/tox.ini index 8f39cd949..1f949ddfd 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,8 @@ envlist = py27,py34 [testenv] changedir=test_project commands = -; python ../tests.py -; coverage run --source modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.relaydomains - coverage run --source modoboa manage.py test modoboa.admin + python ../tests.py + coverage run --source modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.relaydomains coverage report --show-missing deps =