Browse Source

Merge branch 'master' of https://github.com/rafalp/Misago

Rafał Pitoń 8 years ago
parent
commit
65fad1b6a6
87 changed files with 672 additions and 695 deletions
  1. 3 2
      misago/acl/tests/test_providers.py
  2. 6 6
      misago/acl/tests/test_roleadmin_views.py
  3. 1 1
      misago/acl/testutils.py
  4. 1 1
      misago/admin/auth.py
  5. 12 19
      misago/admin/tests/test_admin_views.py
  6. 4 1
      misago/admin/views/generic/formsbuttons.py
  7. 2 2
      misago/admin/views/generic/list.py
  8. 2 1
      misago/admin/views/index.py
  9. 5 6
      misago/categories/models.py
  10. 15 21
      misago/categories/tests/test_categories_admin_views.py
  11. 2 2
      misago/categories/tests/test_category_model.py
  12. 16 24
      misago/categories/tests/test_permissions_admin_views.py
  13. 9 8
      misago/categories/tests/test_prunecategories.py
  14. 3 2
      misago/categories/tests/test_synchronizecategories.py
  15. 13 13
      misago/categories/tests/test_views.py
  16. 4 2
      misago/conf/hydrators.py
  17. 2 2
      misago/conf/migrations/0001_initial.py
  18. 5 5
      misago/conf/tests/test_admin_views.py
  19. 1 1
      misago/conf/tests/test_context_processors.py
  20. 2 1
      misago/core/exceptionhandler.py
  21. 3 3
      misago/core/management/progressbar.py
  22. 3 3
      misago/core/serializer.py
  23. 3 1
      misago/core/shortcuts.py
  24. 4 4
      misago/core/tests/test_apipaginator.py
  25. 2 4
      misago/core/tests/test_decorators.py
  26. 11 18
      misago/core/tests/test_errorpages.py
  27. 2 1
      misago/core/tests/test_serializer.py
  28. 3 2
      misago/core/tests/test_setup.py
  29. 3 3
      misago/core/tests/test_shortcuts.py
  30. 4 5
      misago/core/tests/test_views.py
  31. 3 1
      misago/core/utils.py
  32. 2 1
      misago/faker/management/commands/createfakebans.py
  33. 2 1
      misago/faker/management/commands/createfakecategories.py
  34. 4 3
      misago/faker/management/commands/createfakethreads.py
  35. 2 1
      misago/faker/management/commands/createfakeusers.py
  36. 4 4
      misago/legal/tests.py
  37. 2 1
      misago/legal/views.py
  38. 2 1
      misago/markup/checksums.py
  39. 2 1
      misago/markup/pipeline.py
  40. 1 1
      misago/threads/api/threadendpoints/merge.py
  41. 2 1
      misago/threads/checksums.py
  42. 5 4
      misago/threads/tests/test_subscriptions.py
  43. 3 2
      misago/threads/tests/test_synchronizethreads.py
  44. 18 16
      misago/threads/tests/test_thread_patch_api.py
  45. 5 3
      misago/threads/tests/test_threads_api.py
  46. 24 22
      misago/threads/tests/test_threads_merge_api.py
  47. 177 155
      misago/threads/tests/test_threadslists.py
  48. 3 2
      misago/threads/tests/test_typestree.py
  49. 2 2
      misago/threads/viewmodels/category.py
  50. 1 1
      misago/threads/views/goto.py
  51. 1 1
      misago/users/avatars/dynamic.py
  52. 2 2
      misago/users/avatars/gravatar.py
  53. 5 3
      misago/users/avatars/store.py
  54. 5 3
      misago/users/credentialchange.py
  55. 1 1
      misago/users/models/user.py
  56. 2 3
      misago/users/tests/test_activation_views.py
  57. 24 39
      misago/users/tests/test_auth_api.py
  58. 4 4
      misago/users/tests/test_auth_views.py
  59. 6 6
      misago/users/tests/test_banadmin_views.py
  60. 2 1
      misago/users/tests/test_bansmaintenance.py
  61. 2 1
      misago/users/tests/test_captcha_api.py
  62. 1 2
      misago/users/tests/test_decorators.py
  63. 4 4
      misago/users/tests/test_djangoadmin_auth.py
  64. 4 8
      misago/users/tests/test_forgottenpassword_views.py
  65. 2 1
      misago/users/tests/test_lists_views.py
  66. 4 8
      misago/users/tests/test_options_views.py
  67. 16 15
      misago/users/tests/test_profile_views.py
  68. 11 12
      misago/users/tests/test_rankadmin_views.py
  69. 7 7
      misago/users/tests/test_testutils.py
  70. 24 36
      misago/users/tests/test_user_avatar_api.py
  71. 4 8
      misago/users/tests/test_user_changeemail_api.py
  72. 3 7
      misago/users/tests/test_user_changepassword_api.py
  73. 11 15
      misago/users/tests/test_user_create_api.py
  74. 7 9
      misago/users/tests/test_user_signature_api.py
  75. 23 27
      misago/users/tests/test_user_username_api.py
  76. 26 24
      misago/users/tests/test_useradmin_views.py
  77. 5 7
      misago/users/tests/test_usernamechanges_api.py
  78. 29 35
      misago/users/tests/test_users_api.py
  79. 7 8
      misago/users/tests/test_warningadmin_views.py
  80. 8 4
      misago/users/tokens.py
  81. 1 1
      misago/users/utils.py
  82. 2 1
      misago/users/validators.py
  83. 1 1
      misago/users/views/avatarserver.py
  84. 3 1
      misago/users/views/lists.py
  85. 2 1
      misago/users/views/options.py
  86. 2 1
      misago/users/views/profile.py
  87. 1 1
      misago/users/warnings.py

+ 3 - 2
misago/acl/tests/test_providers.py

@@ -3,6 +3,7 @@ from types import ModuleType
 
 from django.conf import settings
 from django.test import TestCase
+from django.utils import six
 
 from ..providers import PermissionProviders
 
@@ -46,7 +47,7 @@ class PermissionProvidersTests(TestCase):
         self.assertEqual(len(providers_list), len(providers_setting))
 
         for extension, module in providers_list:
-            self.assertTrue(isinstance(extension, basestring))
+            self.assertTrue(isinstance(extension, six.string_types))
             self.assertEqual(type(module), ModuleType)
 
     def test_dict(self):
@@ -58,7 +59,7 @@ class PermissionProvidersTests(TestCase):
         self.assertEqual(len(providers_dict), len(providers_setting))
 
         for extension, module in providers_dict.items():
-            self.assertTrue(isinstance(extension, basestring))
+            self.assertTrue(isinstance(extension, six.string_types))
             self.assertEqual(type(module), ModuleType)
 
     def test_annotators(self):

+ 6 - 6
misago/acl/tests/test_roleadmin_views.py

@@ -14,7 +14,7 @@ class RoleAdminViewsTests(AdminTestCase):
     def test_link_registered(self):
         """admin nav contains user roles link"""
         response = self.client.get(reverse('misago:admin:permissions:users:index'))
-        self.assertIn(reverse('misago:admin:permissions:users:index'), response.content)
+        self.assertContains(response, reverse('misago:admin:permissions:users:index'))
 
     def test_list_view(self):
         """roles list view returns 200"""
@@ -35,7 +35,7 @@ class RoleAdminViewsTests(AdminTestCase):
         test_role = Role.objects.get(name='Test Role')
         response = self.client.get(reverse('misago:admin:permissions:users:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
+        self.assertContains(response, test_role.name)
 
     def test_edit_view(self):
         """edit role view has no showstoppers"""
@@ -48,7 +48,7 @@ class RoleAdminViewsTests(AdminTestCase):
 
         response = self.client.get(reverse('misago:admin:permissions:users:edit', kwargs={'pk': test_role.pk}))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Role', response.content)
+        self.assertContains(response, 'Test Role')
 
         response = self.client.post(
             reverse('misago:admin:permissions:users:edit', kwargs={'pk': test_role.pk}),
@@ -59,7 +59,7 @@ class RoleAdminViewsTests(AdminTestCase):
         test_role = Role.objects.get(name='Top Lel')
         response = self.client.get(reverse('misago:admin:permissions:users:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
+        self.assertContains(response, test_role.name)
 
     def test_users_view(self):
         """users with this role view has no showstoppers"""
@@ -83,7 +83,7 @@ class RoleAdminViewsTests(AdminTestCase):
         response = self.client.post(reverse('misago:admin:permissions:users:delete', kwargs={'pk': test_role.pk}))
         self.assertEqual(response.status_code, 302)
 
+        # Get the page twice so no alert is renderered on second request
         self.client.get(reverse('misago:admin:permissions:users:index'))
         response = self.client.get(reverse('misago:admin:permissions:users:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(test_role.name not in response.content)
+        self.assertNotContains(response, test_role.name)

+ 1 - 1
misago/acl/testutils.py

@@ -30,7 +30,7 @@ def override_acl(user, new_acl):
 
     if user.is_authenticated():
         user._acl_cache = final_cache
-        user.acl_key = md5(unicode(user.pk)).hexdigest()[:8]
+        user.acl_key = md5(str(user.pk).encode()).hexdigest()[:8]
         user.save(update_fields=['acl_key'])
 
         threadstore.set('acl_%s' % user.acl_key, final_cache)

+ 1 - 1
misago/admin/auth.py

@@ -13,7 +13,7 @@ KEY_UPDATED = 'misago_admin_session_updated'
 
 def make_user_admin_token(user):
     formula = (str(user.pk), user.email, user.password, settings.SECRET_KEY)
-    return md5(':'.join(formula)).hexdigest()
+    return md5(':'.join(formula).encode()).hexdigest()
 
 
 # Admin session state controls

+ 12 - 19
misago/admin/tests/test_admin_views.py

@@ -44,10 +44,9 @@ class AdminLoginViewTests(TestCase):
         """unauthenticated request to admin index produces login form"""
         response = self.client.get(reverse('misago:admin:index'))
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Sign in', response.content)
-        self.assertIn('Username or e-mail', response.content)
-        self.assertIn('Password', response.content)
+        self.assertContains(response, 'Sign in')
+        self.assertContains(response, 'Username or e-mail')
+        self.assertContains(response, 'Password')
 
     def test_login_returns_200_on_invalid_post(self):
         """form handles invalid data gracefully"""
@@ -55,11 +54,10 @@ class AdminLoginViewTests(TestCase):
             reverse('misago:admin:index'),
             data={'username': 'Nope', 'password': 'Nope'})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Login or password is incorrect.', response.content)
-        self.assertIn('Sign in', response.content)
-        self.assertIn('Username or e-mail', response.content)
-        self.assertIn('Password', response.content)
+        self.assertContains(response, 'Login or password is incorrect.')
+        self.assertContains(response, 'Sign in')
+        self.assertContains(response, 'Username or e-mail')
+        self.assertContains(response, 'Password')
 
     def test_login_returns_200_on_valid_post(self):
         """form handles valid data correctly"""
@@ -80,12 +78,10 @@ class AdminLogoutTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Your admin session has been closed.", response.content)
+        self.assertContains(response, "Your admin session has been closed.")
 
         response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
     def test_complete_logout(self):
         """complete logout logged from both admin and site"""
@@ -93,12 +89,10 @@ class AdminLogoutTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
 
         response = self.client.get(reverse('misago:admin:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Sign in", response.content)
+        self.assertContains(response, "Sign in")
 
         response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Sign in", response.content)
+        self.assertContains(response, "Sign in")
 
 
 class AdminIndexViewTests(AdminTestCase):
@@ -106,5 +100,4 @@ class AdminIndexViewTests(AdminTestCase):
         """admin index view returns 200"""
         response = self.client.get(reverse('misago:admin:index'))
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)

+ 4 - 1
misago/admin/views/generic/formsbuttons.py

@@ -16,7 +16,10 @@ class TargetedView(AdminView):
             select_for_update = self.get_model().objects
             if self.is_atomic:
                 select_for_update = select_for_update.select_for_update()
-            return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
+            # Does not work on Python 3:
+            # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
+            (pk,) = kwargs.values()
+            return select_for_update.get(pk=pk)
         else:
             return self.get_model()()
 

+ 2 - 2
misago/admin/views/generic/list.py

@@ -1,4 +1,4 @@
-from urllib import urlencode
+from six.moves.urllib.parse import urlencode
 
 from django.contrib import messages
 from django.core.paginator import EmptyPage, Paginator
@@ -106,7 +106,7 @@ class ListView(AdminView):
         return self.clean_filtering_data(form.cleaned_data)
 
     def clean_filtering_data(self, data):
-        for key, value in data.items():
+        for key, value in list(data.items()):
             if not value:
                 del data[key]
         return data

+ 2 - 1
misago/admin/views/index.py

@@ -4,6 +4,7 @@ import requests
 from requests.exceptions import RequestException
 
 from django.http import Http404, JsonResponse
+from django.utils.six.moves import range
 from django.utils.translation import ugettext as _
 
 from misago import __version__
@@ -48,7 +49,7 @@ def check_version(request):
             latest = [int(v) for v in latest_version.split(".")]
             current = [int(v) for v in __version__.split(".")]
 
-            for i in xrange(3):
+            for i in range(3):
                 if latest[i] > current[i]:
                     message = _("Outdated: %(current)s < %(latest)s")
                     formats = {

+ 5 - 6
misago/categories/models.py

@@ -1,11 +1,9 @@
-from urlparse import urlparse
-
 from mptt.managers import TreeManager
 from mptt.models import MPTTModel, TreeForeignKey
 
-from django.core.urlresolvers import reverse
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils import six
+from django.utils.encoding import python_2_unicode_compatible
 
 from misago.acl import version as acl_version
 from misago.acl.models import BaseRole
@@ -59,6 +57,7 @@ class CategoryManager(TreeManager):
         cache.delete(CACHE_NAME)
 
 
+@python_2_unicode_compatible
 class Category(MPTTModel):
     parent = TreeForeignKey(
         'self',
@@ -109,8 +108,8 @@ class Category(MPTTModel):
     def thread_type(self):
         return trees_map.get_type_for_tree_id(self.tree_id)
 
-    def __unicode__(self):
-        return unicode(self.thread_type.get_category_name(self))
+    def __str__(self):
+        return six.text_type(self.thread_type.get_category_name(self))
 
     def lock(self):
         return Category.objects.select_for_update().get(id=self.id)

+ 15 - 21
misago/categories/tests/test_categories_admin_views.py

@@ -11,16 +11,14 @@ class CategoryAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
 
-        self.assertIn(reverse('misago:admin:categories:nodes:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:categories:nodes:index'))
 
     def test_list_view(self):
         """categories list view returns 200"""
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('First category', response.content)
+        self.assertContains(response, 'First category')
 
         # Now test that empty categories list contains message
         root = Category.objects.root_category()
@@ -31,7 +29,7 @@ class CategoryAdminViewsTests(AdminTestCase):
             reverse('misago:admin:categories:nodes:index'))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('No categories', response.content)
+        self.assertContains(response, 'No categories')
 
     def test_new_view(self):
         """new category view has no showstoppers"""
@@ -54,8 +52,7 @@ class CategoryAdminViewsTests(AdminTestCase):
 
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category', response.content)
+        self.assertContains(response, 'Test Category')
 
         test_category = Category.objects.get(slug='test-category')
 
@@ -72,8 +69,7 @@ class CategoryAdminViewsTests(AdminTestCase):
 
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Subcategory', response.content)
+        self.assertContains(response, 'Test Subcategory')
 
     def test_edit_view(self):
         """edit category view has no showstoppers"""
@@ -109,8 +105,7 @@ class CategoryAdminViewsTests(AdminTestCase):
                 'pk': test_category.pk
             }))
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category', response.content)
+        self.assertContains(response, 'Test Category')
 
         response = self.client.post(
             reverse('misago:admin:categories:nodes:edit', kwargs={
@@ -127,8 +122,7 @@ class CategoryAdminViewsTests(AdminTestCase):
 
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category Edited', response.content)
+        self.assertContains(response, 'Test Category Edited')
 
     def test_move_views(self):
         """move up/down views have no showstoppers"""
@@ -161,8 +155,8 @@ class CategoryAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
         self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
+        position_a = response.content.find(b'Category A')
+        position_b = response.content.find(b'Category B')
         self.assertTrue(position_a > position_b)
 
         response = self.client.post(
@@ -175,8 +169,8 @@ class CategoryAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
         self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
+        position_a = response.content.find(b'Category A')
+        position_b = response.content.find(b'Category B')
         self.assertTrue(position_a > position_b)
 
         response = self.client.post(
@@ -189,8 +183,8 @@ class CategoryAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
         self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
+        position_a = response.content.find(b'Category A')
+        position_b = response.content.find(b'Category B')
         self.assertTrue(position_a > position_b)
 
         response = self.client.post(
@@ -203,8 +197,8 @@ class CategoryAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:index'))
         self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
+        position_a = response.content.find(b'Category A')
+        position_b = response.content.find(b'Category B')
         self.assertTrue(position_a < position_b)
 
 

+ 2 - 2
misago/categories/tests/test_category_model.py

@@ -114,7 +114,7 @@ class CategoryModelTests(MisagoTestCase):
 
     def test_delete_content(self):
         """delete_content empties category"""
-        for i in xrange(10):
+        for i in range(10):
             self.create_thread()
 
         self.category.synchronize()
@@ -131,7 +131,7 @@ class CategoryModelTests(MisagoTestCase):
 
     def test_move_content(self):
         """move_content moves category threads and posts to other category"""
-        for i in xrange(10):
+        for i in range(10):
             self.create_thread()
         self.category.synchronize()
 

+ 16 - 24
misago/categories/tests/test_permissions_admin_views.py

@@ -17,8 +17,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:permissions:categories:index'))
 
-        self.assertIn(reverse('misago:admin:permissions:categories:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:permissions:categories:index'))
 
     def test_list_view(self):
         """roles list view returns 200"""
@@ -41,8 +40,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         test_role = CategoryRole.objects.get(name='Test CategoryRole')
         response = self.client.get(
             reverse('misago:admin:permissions:categories:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
+        self.assertContains(response, test_role.name)
 
     def test_edit_view(self):
         """edit role view has no showstoppers"""
@@ -56,8 +54,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             reverse('misago:admin:permissions:categories:edit', kwargs={
                 'pk': test_role.pk
             }))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test CategoryRole', response.content)
+        self.assertContains(response, 'Test CategoryRole')
 
         response = self.client.post(
             reverse('misago:admin:permissions:categories:edit', kwargs={
@@ -69,8 +66,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         test_role = CategoryRole.objects.get(name='Top Lel')
         response = self.client.get(
             reverse('misago:admin:permissions:categories:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
+        self.assertContains(response, test_role.name)
 
     def test_delete_view(self):
         """delete role view has no showstoppers"""
@@ -88,8 +84,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         self.client.get(reverse('misago:admin:permissions:categories:index'))
         response = self.client.get(
             reverse('misago:admin:permissions:categories:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(test_role.name not in response.content)
+        self.assertNotContains(response, test_role.name)
 
     def test_change_category_roles_view(self):
         """change category roles perms view works"""
@@ -146,12 +141,11 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:categories:nodes:permissions',
                     kwargs={'pk': test_category.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_category.name, response.content)
-        self.assertIn(test_role_a.name, response.content)
-        self.assertIn(test_role_b.name, response.content)
-        self.assertIn(role_comments.name, response.content)
-        self.assertIn(role_full.name, response.content)
+        self.assertContains(response, test_category.name)
+        self.assertContains(response, test_role_a.name)
+        self.assertContains(response, test_role_b.name)
+        self.assertContains(response, role_comments.name)
+        self.assertContains(response, role_full.name)
 
         # Assign roles to categories
         response = self.client.post(
@@ -239,11 +233,10 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             reverse('misago:admin:permissions:users:categories', kwargs={
                 'pk': test_role.pk
             }))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(category_a.name, response.content)
-        self.assertIn(category_b.name, response.content)
-        self.assertIn(category_c.name, response.content)
-        self.assertIn(category_d.name, response.content)
+        self.assertContains(response, category_a.name)
+        self.assertContains(response, category_b.name)
+        self.assertContains(response, category_c.name)
+        self.assertContains(response, category_d.name)
 
         # Set test roles
         self.client.post(
@@ -261,9 +254,8 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             reverse('misago:admin:permissions:users:categories', kwargs={
                 'pk': test_role.pk
             }))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(role_comments.name, response.content)
-        self.assertIn(role_full.name, response.content)
+        self.assertContains(response, role_comments.name)
+        self.assertContains(response, role_full.name)
 
         # Assign roles to categories
         response = self.client.post(

+ 9 - 8
misago/categories/tests/test_prunecategories.py

@@ -3,6 +3,7 @@ from datetime import timedelta
 from django.test import TestCase
 from django.utils import timezone
 from django.utils.six import StringIO
+from django.utils.six.moves import range
 
 from misago.threads import testutils
 
@@ -21,12 +22,12 @@ class PruneCategoriesTests(TestCase):
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         posted_on = timezone.now()
-        for t in xrange(10):
+        for t in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread, posted_on=posted_on)
 
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
 
         category.synchronize()
         self.assertEqual(category.threads, 20)
@@ -57,12 +58,12 @@ class PruneCategoriesTests(TestCase):
 
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
-        for t in xrange(10):
+        for t in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread)
 
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
 
         category.synchronize()
         self.assertEqual(category.threads, 20)
@@ -103,12 +104,12 @@ class PruneCategoriesTests(TestCase):
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
         posted_on = timezone.now()
-        for t in xrange(10):
+        for t in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread, posted_on=posted_on)
 
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
 
         category.synchronize()
         self.assertEqual(category.threads, 20)
@@ -152,12 +153,12 @@ class PruneCategoriesTests(TestCase):
 
         # post old threads with recent replies
         started_on = timezone.now() - timedelta(days=30)
-        for t in xrange(10):
+        for t in range(10):
             thread = testutils.post_thread(category, started_on=started_on)
             testutils.reply_thread(thread)
 
         # post recent threads that will be preserved
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
 
         category.synchronize()
         self.assertEqual(category.threads, 20)

+ 3 - 2
misago/categories/tests/test_synchronizecategories.py

@@ -1,5 +1,6 @@
 from django.test import TestCase
 from django.utils.six import StringIO
+from django.utils.six.moves import range
 
 from misago.threads import testutils
 
@@ -12,9 +13,9 @@ class SynchronizeCategoriesTests(TestCase):
         """command synchronizes categories"""
         category = Category.objects.all_categories()[:1][0]
 
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
         for thread in threads:
-            [testutils.reply_thread(thread) for r in xrange(5)]
+            [testutils.reply_thread(thread) for r in range(5)]
 
         category.threads = 0
         category.posts = 0

+ 13 - 13
misago/categories/tests/test_views.py

@@ -13,9 +13,9 @@ class CategoryViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:categories'))
 
         for node in get_categories_tree(self.user):
-            self.assertIn(node.name, response.content)
+            self.assertContains(response, node.name)
             if node.level > 1:
-                self.assertIn(node.get_absolute_url(), response.content)
+                self.assertContains(response, node.get_absolute_url())
 
     def test_index_renders_for_guest(self):
         """categories list renders for guest"""
@@ -24,9 +24,9 @@ class CategoryViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:categories'))
 
         for node in get_categories_tree(self.user):
-            self.assertIn(node.name, response.content)
+            self.assertContains(response, node.name)
             if node.level > 1:
-                self.assertIn(node.get_absolute_url(), response.content)
+                self.assertContains(response, node.get_absolute_url())
 
     def test_index_no_perms_renders(self):
         """categories list renders no visible categories for authenticated"""
@@ -36,7 +36,7 @@ class CategoryViewsTests(AuthenticatedUserTestCase):
         for node in get_categories_tree(self.user):
             self.assertNotIn(node.name, response.content)
             if node.level > 1:
-                self.assertNotIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())
 
     def test_index_no_perms_renders_for_guest(self):
         """categories list renders no visible categories for guest"""
@@ -48,7 +48,7 @@ class CategoryViewsTests(AuthenticatedUserTestCase):
         for node in get_categories_tree(self.user):
             self.assertNotIn(node.name, response.content)
             if node.level > 1:
-                self.assertNotIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())
 
 
 class CategoryAPIViewsTests(AuthenticatedUserTestCase):
@@ -57,9 +57,9 @@ class CategoryAPIViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:api:categories'))
 
         for node in get_categories_tree(self.user):
-            self.assertIn(node.name, response.content)
+            self.assertContains(response, node.name)
             if node.level > 1:
-                self.assertIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())
 
     def test_index_renders_for_guest(self):
         """api returns categories for guest"""
@@ -68,9 +68,9 @@ class CategoryAPIViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:api:categories'))
 
         for node in get_categories_tree(self.user):
-            self.assertIn(node.name, response.content)
+            self.assertContains(response, node.name)
             if node.level > 1:
-                self.assertIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())
 
     def test_index_no_perms_renders(self):
         """api returns no categories for authenticated"""
@@ -80,7 +80,7 @@ class CategoryAPIViewsTests(AuthenticatedUserTestCase):
         for node in get_categories_tree(self.user):
             self.assertNotIn(node.name, response.content)
             if node.level > 1:
-                self.assertNotIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())
 
     def test_index_no_perms_renders_for_guest(self):
         """api returns no categories for guest"""
@@ -90,6 +90,6 @@ class CategoryAPIViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:api:categories'))
 
         for node in get_categories_tree(self.user):
-            self.assertNotIn(node.name, response.content)
+            self.assertNotContains(response, node.name)
             if node.level > 1:
-                self.assertNotIn(node.get_absolute_url(), response.content)
+                self.assertNotContains(response, node.get_absolute_url())

+ 4 - 2
misago/conf/hydrators.py

@@ -1,5 +1,7 @@
+import six
+
 def hydrate_string(dry_value):
-    return unicode(dry_value) if dry_value else ''
+    return six.text_type(dry_value) if dry_value else ''
 
 
 def dehydrate_string(wet_value):
@@ -19,7 +21,7 @@ def hydrate_int(dry_value):
 
 
 def dehydrate_int(wet_value):
-    return unicode(wet_value)
+    return six.text_type(wet_value)
 
 
 def hydrate_list(dry_value):

+ 2 - 2
misago/conf/migrations/0001_initial.py

@@ -21,10 +21,10 @@ class Migration(migrations.Migration):
                 ('order', models.IntegerField(default=0, db_index=True)),
                 ('dry_value', models.TextField(null=True, blank=True)),
                 ('default_value', models.TextField(null=True, blank=True)),
-                ('python_type', models.CharField(default=b'string', max_length=255)),
+                ('python_type', models.CharField(default='string', max_length=255)),
                 ('is_public', models.BooleanField(default=False)),
                 ('is_lazy', models.BooleanField(default=False)),
-                ('form_field', models.CharField(default=b'text', max_length=255)),
+                ('form_field', models.CharField(default='text', max_length=255)),
                 ('pickled_field_extra', models.TextField(null=True, blank=True)),
             ],
             options={

+ 5 - 5
misago/conf/tests/test_admin_views.py

@@ -10,7 +10,7 @@ class AdminSettingsViewsTests(AdminTestCase):
         """admin index view contains settings link"""
         response = self.client.get(reverse('misago:admin:index'))
 
-        self.assertIn(reverse('misago:admin:settings:index'), response.content)
+        self.assertContains(response, reverse('misago:admin:settings:index'))
 
     def test_groups_list_view(self):
         """settings group view returns 200 and contains all settings groups"""
@@ -21,8 +21,8 @@ class AdminSettingsViewsTests(AdminTestCase):
             group_link = reverse('misago:admin:settings:group', kwargs={
                 'key': group.key
             })
-            self.assertIn(group.name, response.content)
-            self.assertIn(group_link, response.content)
+            self.assertContains(response, group.name)
+            self.assertContains(response, group_link)
 
     def test_groups_views(self):
         """
@@ -35,12 +35,12 @@ class AdminSettingsViewsTests(AdminTestCase):
             response = self.client.get(group_link)
 
             self.assertEqual(response.status_code, 200)
-            self.assertIn(group.name, response.content)
+            self.assertContains(response, group.name)
 
             values = {}
             for setting in group.setting_set.all():
                 values[setting.setting] = setting.value
-                self.assertIn(setting.name, response.content)
+                self.assertContains(response, setting.name)
 
             response = self.client.post(group_link, data=values)
             self.assertEqual(response.status_code, 302)

+ 1 - 1
misago/conf/tests/test_context_processors.py

@@ -25,4 +25,4 @@ class ContextProcessorsTests(TestCase):
         """site configuration is preloaded by middleware"""
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn('"SETTINGS": {"', response.content)
+        self.assertContains(response, '"SETTINGS": {"')

+ 2 - 1
misago/core/exceptionhandler.py

@@ -1,6 +1,7 @@
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse
 from django.http import Http404, HttpResponsePermanentRedirect, JsonResponse
+from django.utils import six
 from django.utils.translation import gettext as _
 
 from rest_framework.views import exception_handler as rest_exception_handler
@@ -18,7 +19,7 @@ def is_misago_exception(exception):
 
 
 def handle_ajax_error(request, exception):
-    json = {'is_error': 1, 'message': unicode(exception.message)}
+    json = {'is_error': 1, 'message': six.text_type(exception.message)}
     return JsonResponse(json, status=exception.code)
 
 

+ 3 - 3
misago/core/management/progressbar.py

@@ -2,8 +2,8 @@ import time
 
 
 def show_progress(command, step, total, since=None):
-    progress = step * 100 / total
-    filled = progress / 2
+    progress = step * 100 // total
+    filled = progress // 2
     blank = 50 - filled
 
     line = '\r%s%% [%s%s]'
@@ -11,7 +11,7 @@ def show_progress(command, step, total, since=None):
 
     if since:
         if step > 0:
-            estimated_time = ((time.time() - since) / step) * (total - step)
+            estimated_time = ((time.time() - since) // step) * (total - step)
             clock = time.strftime('%H:%M:%S', time.gmtime(estimated_time))
             rendered_line = '%s %s est.' % (rendered_line, clock)
         else:

+ 3 - 3
misago/core/serializer.py

@@ -12,7 +12,7 @@ except ImportError:
 
 
 def _checksum(base):
-    return sha256('%s+%s' % (settings.SECRET_KEY, base)).hexdigest()[:14]
+    return sha256(('%s+%s' % (settings.SECRET_KEY, base)).encode()).hexdigest()[:14]
 
 
 def loads(dry):
@@ -20,13 +20,13 @@ def loads(dry):
     base = dry[14:]
 
     if _checksum(base) == checksum:
-        return pickle.loads(base64.decodestring(base))
+        return pickle.loads(base64.decodestring(base.encode()))
     else:
         raise ValueError("pickle checksum is invalid")
 
 
 def dumps(wet):
-    base = base64.encodestring(pickle.dumps(wet, pickle.HIGHEST_PROTOCOL))
+    base = base64.encodestring(pickle.dumps(wet, pickle.HIGHEST_PROTOCOL)).decode()
     checksum = _checksum(base)
     return '%s%s' % (checksum, base)
 

+ 3 - 1
misago/core/shortcuts.py

@@ -3,6 +3,8 @@ from collections import OrderedDict
 from django.http import Http404
 from django.shortcuts import *  # noqa
 
+import six
+
 
 def paginate(object_list, page, per_page, orphans=0,
              allow_empty_first_page=True,
@@ -75,7 +77,7 @@ def validate_slug(model, slug):
 
 
 def get_int_or_404(value):
-    if unicode(value).isdigit():
+    if six.text_type(value).isdigit():
         return int(value)
     else:
         raise Http404()

+ 4 - 4
misago/core/tests/test_apipaginator.py

@@ -1,5 +1,5 @@
 from django.test import TestCase
-
+from django.utils.six.moves import range
 from ..apipaginator import ApiPaginator
 
 
@@ -36,7 +36,7 @@ class PaginatorTests(TestCase):
     def test_first_page(self):
         """pagination works for first page of queryset"""
         paginator = ApiPaginator(6, 2)()
-        querset = [i for i in xrange(20)]
+        querset = [i for i in range(20)]
 
         results = paginator.paginate_queryset(querset, MockRequest())
         self.assertEqual(results, [0, 1, 2, 3, 4, 5])
@@ -57,7 +57,7 @@ class PaginatorTests(TestCase):
     def test_next_page(self):
         """pagination works for next page of queryset"""
         paginator = ApiPaginator(6, 2)()
-        querset = [i for i in xrange(20)]
+        querset = [i for i in range(20)]
 
         results = paginator.paginate_queryset(querset, MockRequest(2))
         self.assertEqual(results, [6, 7, 8, 9, 10, 11])
@@ -78,7 +78,7 @@ class PaginatorTests(TestCase):
     def test_last_page(self):
         """pagination works for last page of queryset"""
         paginator = ApiPaginator(6, 2)()
-        querset = [i for i in xrange(20)]
+        querset = [i for i in range(20)]
 
         results = paginator.paginate_queryset(querset, MockRequest(3))
         self.assertEqual(results, [12, 13, 14, 15, 16, 17, 18, 19])

+ 2 - 4
misago/core/tests/test_decorators.py

@@ -9,11 +9,9 @@ class RequirePostTests(TestCase):
     def test_require_POST_success(self):
         """require_POST decorator allowed POST request"""
         response = self.client.post(reverse('test-require-post'))
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.content, 'Request method: POST')
+        self.assertContains(response, 'Request method: POST')
 
     def test_require_POST_fail_GET(self):
         """require_POST decorator failed on GET request"""
         response = self.client.get(reverse('test-require-post'))
-        self.assertEqual(response.status_code, 405)
-        self.assertIn("Wrong way", response.content)
+        self.assertContains(response, "Wrong way", status_code=405)

+ 11 - 18
misago/core/tests/test_errorpages.py

@@ -13,8 +13,7 @@ class CSRFErrorViewTests(TestCase):
         csrf_client = Client(enforce_csrf_checks=True)
         response = csrf_client.post(reverse('misago:index'),
                                     data={'eric': 'fish'})
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("Request blocked", response.content)
+        self.assertContains(response, "Request blocked", status_code=403)
 
 
 class ErrorPageViewsTests(TestCase):
@@ -23,26 +22,22 @@ class ErrorPageViewsTests(TestCase):
     def test_banned_returns_403(self):
         """banned error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-banned'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("<p>Banned for test!</p>", response.content)
+        self.assertContains(response, "<p>Banned for test!</p>", status_code=403)
 
     def test_permission_denied_returns_403(self):
         """permission_denied error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-403'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("Page not available", response.content)
+        self.assertContains(response, "Page not available", status_code=403)
 
     def test_page_not_found_returns_404(self):
         """page_not_found error page has no show-stoppers"""
         response = self.client.get(reverse('raise-misago-404'))
-        self.assertEqual(response.status_code, 404)
-        self.assertIn("Page not found", response.content)
+        self.assertContains(response, "Page not found", status_code=404)
 
     def test_not_allowed_returns_405(self):
         """not allowed error page has no showstoppers"""
         response = self.client.get(reverse('raise-misago-405'))
-        self.assertEqual(response.status_code, 405)
-        self.assertIn("Wrong way", response.content)
+        self.assertContains(response, "Wrong way", status_code=405)
 
 
 class CustomErrorPagesTests(TestCase):
@@ -63,23 +58,21 @@ class CustomErrorPagesTests(TestCase):
         response = self.client.get(reverse('raise-misago-403'))
         self.assertEqual(response.status_code, 403)
         response = self.client.get(reverse('raise-403'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("Custom 403", response.content)
+        self.assertContains(response, "Custom 403", status_code=403)
 
         response = mock_custom_403_error_page(self.misago_request)
-        self.assertNotIn("Custom 403", response.content)
+        self.assertNotContains(response, "Custom 403", status_code=403)
         response = mock_custom_403_error_page(self.site_request)
-        self.assertIn("Custom 403", response.content)
+        self.assertContains(response, "Custom 403", status_code=403)
 
     def test_shared_404_decorator(self):
         """shared_404_decorator calls correct error handler"""
         response = self.client.get(reverse('raise-misago-404'))
         self.assertEqual(response.status_code, 404)
         response = self.client.get(reverse('raise-404'))
-        self.assertEqual(response.status_code, 404)
-        self.assertIn("Custom 404", response.content)
+        self.assertContains(response, "Custom 404", status_code=404)
 
         response = mock_custom_404_error_page(self.misago_request)
-        self.assertNotIn("Custom 404", response.content)
+        self.assertNotContains(response, "Custom 404", status_code=404)
         response = mock_custom_404_error_page(self.site_request)
-        self.assertIn("Custom 404", response.content)
+        self.assertContains(response, "Custom 404", status_code=404)

+ 2 - 1
misago/core/tests/test_serializer.py

@@ -1,3 +1,4 @@
+from django.utils.six.moves import range
 from django.test import TestCase
 
 from .. import serializer
@@ -17,7 +18,7 @@ class SerializerTests(TestCase):
 
     def test_serializer_handles_paddings(self):
         """serializer handles missing paddings"""
-        for i in xrange(100):
+        for i in range(100):
             wet = 'Lorem ipsum %s' % ('a' * i)
             dry = serializer.dumps(wet)
             self.assertFalse(dry.endswith('='))

+ 3 - 2
misago/core/tests/test_setup.py

@@ -1,5 +1,6 @@
 import os
 
+from django.utils.encoding import smart_str
 from django.test import TestCase
 
 from .. import setup
@@ -36,5 +37,5 @@ class SetupTests(TestCase):
             os.path.dirname(os.path.dirname(__file__)))
         test_project_path = os.path.join(misago_path, 'project_template')
 
-        self.assertEqual(unicode(setup.get_misago_project_template()),
-                         unicode(test_project_path))
+        self.assertEqual(smart_str(setup.get_misago_project_template()),
+                         smart_str(test_project_path))

+ 3 - 3
misago/core/tests/test_shortcuts.py

@@ -12,7 +12,7 @@ class PaginateTests(TestCase):
         """Valid page number causes no errors"""
         response = self.client.get(
             reverse('test-pagination', kwargs={'page': 2}))
-        self.assertEqual("5,6,7,8,9", response.content)
+        self.assertEqual("5,6,7,8,9", response.content.decode())
 
     def test_invalid_page_handling(self):
         """Invalid page number results in 404 error"""
@@ -24,7 +24,7 @@ class PaginateTests(TestCase):
         """Implicit page number causes no errors"""
         response = self.client.get(
             reverse('test-pagination'))
-        self.assertEqual("0,1,2,3,4", response.content)
+        self.assertEqual("0,1,2,3,4", response.content.decode())
 
     def test_explicit_page_handling(self):
         """Explicit page number results in redirect"""
@@ -43,7 +43,7 @@ class ValidateSlugTests(TestCase):
             'slug': 'eric-the-fish',
             'pk': 1,
         }))
-        self.assertIn("Allright", response.content)
+        self.assertContains(response, "Allright")
 
     def test_invalid_slug_handling(self):
         """Invalid slug returns in redirect to valid page"""

+ 4 - 5
misago/core/tests/test_views.py

@@ -5,15 +5,14 @@ from django.test import TestCase
 class MomentJSCatalogViewTests(TestCase):
     def test_moment_js_catalog_view_returns_200(self):
         """moment.js catalog view has no show-stoppers"""
-        with self.settings(LANGUAGE_CODE='en_us'):
+        with self.settings(LANGUAGE_CODE='en-us'):
             response = self.client.get('/moment-i18n.js')
             self.assertEqual(response.status_code, 200)
-            self.assertEqual(response.content, "")
+            self.assertEqual(response.content, b"")
 
-        with self.settings(LANGUAGE_CODE='pl_pl'):
+        with self.settings(LANGUAGE_CODE='pl-pl'):
             response = self.client.get('/moment-i18n.js')
-            self.assertEqual(response.status_code, 200)
-            self.assertIn(response.content, "// locale : polish (pl)")
+            self.assertContains(response, "// locale : polish (pl)")
 
 
 class PreloadJSDataViewTests(TestCase):

+ 3 - 1
misago/core/utils.py

@@ -9,9 +9,11 @@ from django.utils import html, timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ungettext_lazy
 
+import six
+
 
 def slugify(string):
-    string = unicode(string)
+    string = six.text_type(string)
     string = unidecode(string)
     return django_slugify(string.replace('_', ' ').strip())
 

+ 2 - 1
misago/faker/management/commands/createfakebans.py

@@ -6,6 +6,7 @@ from faker import Factory
 
 from django.core.management.base import BaseCommand
 from django.utils import timezone
+from django.utils.six.moves import range
 
 from misago.core.management.progressbar import show_progress
 from misago.users.models import BAN_EMAIL, BAN_IP, BAN_USERNAME, Ban
@@ -97,7 +98,7 @@ class Command(BaseCommand):
 
         created_count = 0
         show_progress(self, created_count, fake_bans_to_create)
-        for i in xrange(fake_bans_to_create):
+        for i in range(fake_bans_to_create):
             ban = Ban(check_type=random.randint(BAN_USERNAME, BAN_IP))
             ban.banned_value = create_fake_test(fake, ban.check_type)
 

+ 2 - 1
misago/faker/management/commands/createfakecategories.py

@@ -2,6 +2,7 @@ import random
 import sys
 import time
 
+from django.utils.six.moves import range
 from faker import Factory
 
 from django.core.management.base import BaseCommand
@@ -46,7 +47,7 @@ class Command(BaseCommand):
         created_count = 0
         start_time = time.time()
         show_progress(self, created_count, fake_cats_to_create)
-        for i in xrange(fake_cats_to_create):
+        for i in range(fake_cats_to_create):
             parent = random.choice(categories)
 
             new_category = Category()

+ 4 - 3
misago/faker/management/commands/createfakethreads.py

@@ -8,6 +8,7 @@ from django.core.management.base import BaseCommand
 from django.db.transaction import atomic
 from django.template.defaultfilters import linebreaks_filter
 from django.utils import timezone
+from django.utils.six.moves import range
 
 from misago.categories.models import Category
 from misago.core.management.progressbar import show_progress
@@ -41,7 +42,7 @@ class Command(BaseCommand):
         created_threads = 0
         start_time = time.time()
         show_progress(self, created_threads, fake_threads_to_create)
-        for i in xrange(fake_threads_to_create):
+        for i in range(fake_threads_to_create):
             with atomic():
                 datetime = timezone.now()
                 category = random.choice(categories)
@@ -98,7 +99,7 @@ class Command(BaseCommand):
                 else:
                     thread_replies = random.randint(0, 10)
 
-                for x in xrange(thread_replies):
+                for x in range(thread_replies):
                     datetime = timezone.now()
                     user = User.objects.order_by('?')[:1][0]
                     fake_message = "\n\n".join(fake.paragraphs())
@@ -137,7 +138,7 @@ class Command(BaseCommand):
 
         pinned_threads = random.randint(0, int(created_threads * 0.025)) or 1
         self.stdout.write('\nPinning %s threads...' % pinned_threads)
-        for i in xrange(0, pinned_threads):
+        for i in range(0, pinned_threads):
             thread = Thread.objects.order_by('?')[:1][0]
             if random.randint(0, 100) > 75:
                 thread.weight = 2

+ 2 - 1
misago/faker/management/commands/createfakeusers.py

@@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 from django.core.management.base import BaseCommand
 from django.db import IntegrityError
+from django.utils.six.moves import range
 
 from misago.core.management.progressbar import show_progress
 from misago.users.avatars import dynamic, gallery, get_avatar_hash
@@ -39,7 +40,7 @@ class Command(BaseCommand):
         created_count = 0
         start_time = time.time()
         show_progress(self, created_count, fake_users_to_create)
-        for i in xrange(fake_users_to_create):
+        for i in range(fake_users_to_create):
             try:
                 kwargs = {
                     'rank': random.choice(ranks),

+ 4 - 4
misago/legal/tests.py

@@ -43,8 +43,8 @@ class PrivacyPolicyTests(TestCase):
 
         response = self.client.get(reverse('misago:privacy-policy'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Policy', response.content)
-        self.assertIn('Lorem ipsum dolor', response.content)
+        self.assertContains(response, 'Test Policy')
+        self.assertContains(response, 'Lorem ipsum dolor')
 
     def test_context_processor_no_policy(self):
         """context processor has no TOS link"""
@@ -110,8 +110,8 @@ class TermsOfServiceTests(TestCase):
 
         response = self.client.get(reverse('misago:terms-of-service'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Test ToS', response.content)
-        self.assertIn('Lorem ipsum dolor', response.content)
+        self.assertContains(response, 'Test ToS')
+        self.assertContains(response, 'Lorem ipsum dolor')
 
     def test_context_processor_no_tos(self):
         """context processor has no TOS link"""

+ 2 - 1
misago/legal/views.py

@@ -2,6 +2,7 @@ from hashlib import md5
 
 from django.http import Http404
 from django.shortcuts import redirect, render
+from django.utils.encoding import force_bytes
 from django.utils.translation import gettext as _
 
 from misago.conf import settings
@@ -15,7 +16,7 @@ def get_parsed_content(request, setting_name):
 
     unparsed_content = settings.get_lazy_setting(setting_name)
 
-    checksum_source = '%s:%s' % (unparsed_content, settings.SECRET_KEY)
+    checksum_source = force_bytes('%s:%s' % (unparsed_content, settings.SECRET_KEY))
     unparsed_checksum = md5(checksum_source).hexdigest()
 
     if cached_content and cached_content.get('checksum') == unparsed_checksum:

+ 2 - 1
misago/markup/checksums.py

@@ -23,12 +23,13 @@ in char fields with max_length=64
 from hashlib import sha256
 
 from django.conf import settings
+from django.utils import six
 
 
 def make_checksum(parsed, unique_values=None):
     unique_values = unique_values or []
     seeds = [parsed, settings.SECRET_KEY]
-    seeds.extend([unicode(v) for v in unique_values])
+    seeds.extend([six.text_type(v) for v in unique_values])
 
     return sha256('+'.join(seeds).encode("utf-8")).hexdigest()
 

+ 2 - 1
misago/markup/pipeline.py

@@ -3,6 +3,7 @@ from importlib import import_module
 from bs4 import BeautifulSoup
 
 from django.conf import settings
+from django.utils import six
 
 
 class MarkupPipeline(object):
@@ -25,7 +26,7 @@ class MarkupPipeline(object):
                 hook = getattr(module, 'clean_parsed')
                 hook.process_result(result, soup)
 
-        souped_text = unicode(soup.body).strip()[6:-7]
+        souped_text = six.text_type(soup.body).strip()[6:-7]
         result['parsed_text'] = souped_text.strip()
         return result
 

+ 1 - 1
misago/threads/api/threadendpoints/merge.py

@@ -55,7 +55,7 @@ def threads_merge_endpoint(request):
 
 def clean_threads_for_merge(request):
     try:
-        threads_ids = map(int, request.data.get('threads', []))
+        threads_ids = list(map(int, request.data.get('threads', [])))
     except (ValueError, TypeError):
         raise MergeError(_("One or more thread ids received were invalid."))
 

+ 2 - 1
misago/threads/checksums.py

@@ -1,4 +1,5 @@
 from misago.markup import checksums
+from django.utils import six
 
 
 def is_post_valid(post):
@@ -7,7 +8,7 @@ def is_post_valid(post):
 
 
 def make_post_checksum(post):
-    post_seeds = [unicode(v) for v in (post.id, post.poster_ip)]
+    post_seeds = [six.text_type(v) for v in (post.id, post.poster_ip)]
     return checksums.make_checksum(post.parsed, post_seeds)
 
 

+ 5 - 4
misago/threads/tests/test_subscriptions.py

@@ -3,6 +3,7 @@ from datetime import timedelta
 from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.utils import timezone
+from django.utils.six.moves import range
 
 from misago.categories.models import Category
 from misago.users.models import AnonymousUser
@@ -34,7 +35,7 @@ class SubscriptionsTests(TestCase):
     def test_anon_threads_subscription(self):
         """make multiple threads list sub aware for anon"""
         threads = []
-        for i in xrange(10):
+        for i in range(10):
             threads.append(
                 self.post_thread(timezone.now() - timedelta(days=10)))
 
@@ -51,7 +52,7 @@ class SubscriptionsTests(TestCase):
     def test_threads_no_subscription(self):
         """make mulitple threads sub aware for authenticated"""
         threads = []
-        for i in xrange(10):
+        for i in range(10):
             threads.append(
                 self.post_thread(timezone.now() - timedelta(days=10)))
 
@@ -76,7 +77,7 @@ class SubscriptionsTests(TestCase):
     def test_threads_no_subscription(self):
         """make mulitple threads sub aware for authenticated"""
         threads = []
-        for i in xrange(10):
+        for i in range(10):
             threads.append(
                 self.post_thread(timezone.now() - timedelta(days=10)))
 
@@ -99,7 +100,7 @@ class SubscriptionsTests(TestCase):
 
         make_subscription_aware(self.user, threads)
 
-        for i in xrange(10):
+        for i in range(10):
             if i % 3 == 0:
                 self.assertFalse(threads[i].subscription.send_email)
             elif i % 2 == 0:

+ 3 - 2
misago/threads/tests/test_synchronizethreads.py

@@ -1,5 +1,6 @@
 from django.test import TestCase
 from django.utils.six import StringIO
+from django.utils.six.moves import range
 
 from misago.categories.models import Category
 
@@ -22,9 +23,9 @@ class SynchronizeThreadsTests(TestCase):
         """command synchronizes threads"""
         category = Category.objects.all_categories()[:1][0]
 
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(category) for t in range(10)]
         for i, thread in enumerate(threads):
-            [testutils.reply_thread(thread) for r in xrange(i)]
+            [testutils.reply_thread(thread) for r in range(i)]
             thread.replies = 0
             thread.save()
 

+ 18 - 16
misago/threads/tests/test_thread_patch_api.py

@@ -1,4 +1,6 @@
 import json
+from django.utils import six
+from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
@@ -55,7 +57,7 @@ class ThreadPinGloballyApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to pin this thread globally.")
 
@@ -80,7 +82,7 @@ class ThreadPinGloballyApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to change this thread's weight.")
 
@@ -137,7 +139,7 @@ class ThreadPinLocallyApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to change this thread's weight.")
 
@@ -162,7 +164,7 @@ class ThreadPinLocallyApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to change this thread's weight.")
 
@@ -223,7 +225,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['category']['id'], self.category_b.pk)
 
-        reponse_json = json.loads(response.content)
+        reponse_json = json.loads(smart_str(response.content))
         self.assertEqual(reponse_json['category'], self.category_b.pk)
         self.assertEqual(reponse_json['top_category'], None)
 
@@ -251,7 +253,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         thread_json = self.get_thread_json()
         self.assertEqual(thread_json['category']['id'], self.category_b.pk)
 
-        reponse_json = json.loads(response.content)
+        reponse_json = json.loads(smart_str(response.content))
         self.assertEqual(reponse_json['category'], self.category_b.pk)
         self.assertEqual(reponse_json['top_category'], self.category.pk)
 
@@ -268,7 +270,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to move this thread.")
 
@@ -292,7 +294,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0], 'NOT FOUND')
 
         self.override_other_acl({})
@@ -315,7 +317,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             'You don\'t have permission to browse "Category B" contents.')
 
@@ -332,7 +334,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['category'], self.category.pk)
 
     def test_thread_top_flatten_categories(self):
@@ -353,7 +355,7 @@ class ThreadMoveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['top_category'], self.category.pk)
         self.assertEqual(response_json['category'], self.category_b.pk)
 
@@ -407,7 +409,7 @@ class ThreadCloseApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to close this thread.")
 
@@ -432,7 +434,7 @@ class ThreadCloseApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to open this thread.")
 
@@ -471,7 +473,7 @@ class ThreadApproveApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "Content approval can't be reversed.")
 
@@ -537,7 +539,7 @@ class ThreadHideApiTests(ThreadsApiTestCase):
         content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'][0],
             "You don't have permission to hide this thread.")
 
@@ -626,7 +628,7 @@ class ThreadSubscribeApiTests(ThreadsApiTestCase):
     def test_subscribe_nonexistant_thread(self):
         """api makes it impossible to subscribe nonexistant thread"""
         bad_api_link = self.api_link.replace(
-            unicode(self.thread.pk), unicode(self.thread.pk + 9))
+            six.text_type(self.thread.pk), six.text_type(self.thread.pk + 9))
 
         response = self.client.patch(bad_api_link, json.dumps([
             {'op': 'replace', 'path': 'subscription', 'value': 'email'}

+ 5 - 3
misago/threads/tests/test_threads_api.py

@@ -1,5 +1,7 @@
 import json
 
+from django.utils.encoding import smart_str
+
 from misago.acl.testutils import override_acl
 from misago.categories.models import THREADS_ROOT_NAME, Category
 from misago.users.testutils import AuthenticatedUserTestCase
@@ -47,7 +49,7 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
         response = self.client.get(self.thread.get_api_url())
         self.assertEqual(response.status_code, 200)
 
-        return json.loads(response.content)
+        return json.loads(smart_str(response.content))
 
 
 class ThreadRetrieveApiTests(ThreadsApiTestCase):
@@ -68,7 +70,7 @@ class ThreadRetrieveApiTests(ThreadsApiTestCase):
             response = self.client.get(link)
             self.assertEqual(response.status_code, 200)
 
-            response_json = json.loads(response.content)
+            response_json = json.loads(smart_str(response.content))
             self.assertEqual(response_json['id'], self.thread.pk)
             self.assertEqual(response_json['title'], self.thread.title)
 
@@ -141,7 +143,7 @@ class ThreadDeleteApiTests(ThreadsApiTestCase):
             'can_hide_threads': 0
         })
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['detail'],
             "You don't have permission to delete this thread.")
 

+ 24 - 22
misago/threads/tests/test_threads_merge_api.py

@@ -1,6 +1,8 @@
 import json
 
 from django.core.urlresolvers import reverse
+from django.utils.encoding import smart_str
+from django.utils.six.moves import range
 
 from misago.acl import add_acl
 from misago.acl.testutils import override_acl
@@ -29,7 +31,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         response = self.client.post(self.api_link, content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "You have to select at least two threads to merge."
         })
@@ -41,7 +43,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "You have to select at least two threads to merge."
         })
@@ -53,7 +55,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "One or more thread ids received were invalid."
         })
@@ -63,7 +65,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "One or more thread ids received were invalid."
         })
@@ -75,7 +77,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "You have to select at least two threads to merge."
         })
@@ -89,7 +91,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "One or more threads to merge could not be found."
         })
@@ -103,7 +105,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "One or more threads to merge could not be found."
         })
@@ -117,7 +119,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, [
             {
                 'id': thread.pk,
@@ -138,7 +140,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
     def test_merge_too_many_threads(self):
         """api rejects too many threads to merge"""
         threads = []
-        for i in xrange(MERGE_LIMIT + 1):
+        for i in range(MERGE_LIMIT + 1):
             threads.append(testutils.post_thread(category=self.category).pk)
 
         self.override_acl({
@@ -153,7 +155,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 403)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'detail': "No more than %s threads can be merged at single time." % MERGE_LIMIT
         })
@@ -174,7 +176,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'title': ['This field is required.'],
             'category': ['This field is required.'],
@@ -198,7 +200,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'title': ["Thread title should be at least 5 characters long."]
         })
@@ -221,7 +223,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'category': ["Requested category could not be found."]
         })
@@ -245,7 +247,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'weight': ["Ensure this value is less than or equal to 2."]
         })
@@ -269,7 +271,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'weight': [
                 "You don't have permission to pin threads globally in this category."
@@ -295,7 +297,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'weight': [
                 "You don't have permission to pin threads in this category."
@@ -322,7 +324,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'title': ["Thread title should be at least 5 characters long."]
         })
@@ -347,7 +349,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'title': ["Thread title should be at least 5 characters long."]
         })
@@ -371,7 +373,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'is_closed': [
                 "You don't have permission to close threads in this category."
@@ -399,7 +401,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         }), content_type="application/json")
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json, {
             'title': ["Thread title should be at least 5 characters long."]
         })
@@ -425,7 +427,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         # is response json with new thread?
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
 
         new_thread = Thread.objects.get(pk=response_json['id'])
         new_thread.is_read = False
@@ -466,7 +468,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(response.status_code, 200)
 
         # is response json with new thread?
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
 
         new_thread = Thread.objects.get(pk=response_json['id'])
         new_thread.is_read = False

+ 177 - 155
misago/threads/tests/test_threadslists.py

@@ -3,6 +3,8 @@ from json import loads as json_loads
 
 from django.conf import settings
 from django.utils import timezone
+from django.utils.encoding import smart_str
+from django.utils.six.moves import range
 
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
@@ -173,21 +175,21 @@ class AllThreadsListTests(ThreadsListTestCase):
 
             response = self.client.get('/' + url)
             self.assertEqual(response.status_code, 200)
-            self.assertIn("empty-message", response.content)
+            self.assertContains(response, "empty-message")
 
             self.access_all_categories()
 
             response = self.client.get(self.category_b.get_absolute_url() + url)
             self.assertEqual(response.status_code, 200)
-            self.assertIn(self.category_b.name, response.content)
-            self.assertIn("empty-message", response.content)
+            self.assertContains(response, self.category_b.name)
+            self.assertContains(response, "empty-message")
 
             self.access_all_categories()
 
             response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all'))
             self.assertEqual(response.status_code, 200)
 
-            response_json = json_loads(response.content)
+            response_json = json_loads(smart_str(response.content))
             self.assertEqual(len(response_json['results']), 0)
 
     def test_list_authenticated_only_views(self):
@@ -202,7 +204,7 @@ class AllThreadsListTests(ThreadsListTestCase):
 
             response = self.client.get(self.category_b.get_absolute_url() + url)
             self.assertEqual(response.status_code, 200)
-            self.assertIn(self.category_b.name, response.content)
+            self.assertContains(response, self.category_b.name)
 
             self.access_all_categories()
 
@@ -249,22 +251,28 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
 
-        self.assertIn('subcategory-%s' % self.category_a.css_class, response.content)
+        self.assertContains(response,
+            'subcategory-%s' % self.category_a.css_class)
 
         # readable categories, but non-accessible directly
-        self.assertNotIn('subcategory-%s' % self.category_b.css_class, response.content)
-        self.assertNotIn('subcategory-%s' % self.category_c.css_class, response.content)
-        self.assertNotIn('subcategory-%s' % self.category_d.css_class, response.content)
-        self.assertNotIn('subcategory-%s' % self.category_f.css_class, response.content)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_b.css_class)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_c.css_class)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_d.css_class)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_f.css_class)
 
         # hidden category
-        self.assertNotIn('subcategory-%s' % test_category.css_class, response.content)
+        self.assertNotContains(response,
+            'subcategory-%s' % test_category.css_class)
 
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertIn(self.category_a.pk, response_json['subcategories'])
         self.assertNotIn(self.category_b.pk, response_json['subcategories'])
 
@@ -274,18 +282,22 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get(self.category_a.get_absolute_url())
         self.assertEqual(response.status_code, 200)
 
-        self.assertIn('subcategory-%s' % self.category_b.css_class, response.content)
+        self.assertContains(response,
+            'subcategory-%s' % self.category_b.css_class)
 
         # readable categories, but non-accessible directly
-        self.assertNotIn('subcategory-%s' % self.category_c.css_class, response.content)
-        self.assertNotIn('subcategory-%s' % self.category_d.css_class, response.content)
-        self.assertNotIn('subcategory-%s' % self.category_f.css_class, response.content)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_c.css_class)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_d.css_class)
+        self.assertNotContains(response,
+            'subcategory-%s' % self.category_f.css_class)
 
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(
             response_json['subcategories'][0], self.category_b.pk)
 
@@ -311,10 +323,11 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
 
+        content = smart_str(response.content)
         positions = {
-            'g': response.content.find(globally.get_absolute_url()),
-            'l': response.content.find(locally.get_absolute_url()),
-            's': response.content.find(standard.get_absolute_url()),
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
         }
 
         # global announcement before others
@@ -333,10 +346,11 @@ class AllThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/api/threads/')
         self.assertEqual(response.status_code, 200)
 
+        content = smart_str(response.content)
         positions = {
-            'g': response.content.find(globally.get_absolute_url()),
-            'l': response.content.find(locally.get_absolute_url()),
-            's': response.content.find(standard.get_absolute_url()),
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
         }
 
         # global announcement before others
@@ -354,7 +368,7 @@ class AllThreadsListTests(ThreadsListTestCase):
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
         threads = []
-        for i in xrange(settings.MISAGO_THREADS_PER_PAGE * 3):
+        for i in range(settings.MISAGO_THREADS_PER_PAGE * 3):
             threads.append(testutils.post_thread(
                 category=self.first_category
             ))
@@ -364,26 +378,26 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.assertEqual(response.status_code, 200)
 
         for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
+            self.assertNotContains(response, thread.get_absolute_url())
         for thread in threads[settings.MISAGO_THREADS_PER_PAGE:settings.MISAGO_THREADS_PER_PAGE * 2]:
-            self.assertIn(thread.get_absolute_url(), response.content)
+            self.assertContains(response, thread.get_absolute_url())
         for thread in threads[settings.MISAGO_THREADS_PER_PAGE * 2:]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
+            self.assertNotContains(response, thread.get_absolute_url())
 
-        self.assertNotIn('/?page=1', response.content)
-        self.assertIn('/?page=3', response.content)
+        self.assertNotContains(response, '/?page=1')
+        self.assertContains(response, '/?page=3')
 
         # third page renders
         response = self.client.get('/?page=3')
         self.assertEqual(response.status_code, 200)
 
         for thread in threads[settings.MISAGO_THREADS_PER_PAGE:]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
+            self.assertNotContains(response, thread.get_absolute_url())
         for thread in threads[:settings.MISAGO_THREADS_PER_PAGE]:
-            self.assertIn(thread.get_absolute_url(), response.content)
+            self.assertContains(response, thread.get_absolute_url())
 
-        self.assertIn('/?page=2', response.content)
-        self.assertNotIn('/?page=4', response.content)
+        self.assertContains(response, '/?page=2')
+        self.assertNotContains(response, '/?page=4')
 
         # excessive page gives 404
         response = self.client.get('/?page=4')
@@ -469,10 +483,11 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         response = self.client.get(self.first_category.get_absolute_url())
         self.assertEqual(response.status_code, 200)
 
+        content = smart_str(response.content)
         positions = {
-            'g': response.content.find(globally.get_absolute_url()),
-            'l': response.content.find(locally.get_absolute_url()),
-            's': response.content.find(standard.get_absolute_url()),
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
         }
 
         # global announcement before others
@@ -491,10 +506,11 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         response = self.client.get('/api/threads/?category=%s' % self.first_category.pk)
         self.assertEqual(response.status_code, 200)
 
+        content = smart_str(response.content)
         positions = {
-            'g': response.content.find(globally.get_absolute_url()),
-            'l': response.content.find(locally.get_absolute_url()),
-            's': response.content.find(standard.get_absolute_url()),
+            'g': content.find(globally.get_absolute_url()),
+            'l': content.find(locally.get_absolute_url()),
+            's': content.find(standard.get_absolute_url()),
         }
 
         # global announcement before others
@@ -520,19 +536,23 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
 
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
-        self.assertIn('subcategory-%s' % self.category_a.css_class, response.content)
-        self.assertIn('subcategory-%s' % self.category_e.css_class, response.content)
-        self.assertIn('thread-category-%s' % self.category_a.css_class, response.content)
-        self.assertIn('thread-category-%s' % self.category_c.css_class, response.content)
+        self.assertContains(response,
+            'subcategory-%s' % self.category_a.css_class)
+        self.assertContains(response,
+            'subcategory-%s' % self.category_e.css_class)            
+        self.assertContains(response,
+            'thread-category-%s' % self.category_a.css_class)
+        self.assertContains(response,
+            'thread-category-%s' % self.category_c.css_class)
 
         # api displays same data
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 3)
         self.assertIn(self.category_a.pk, response_json['subcategories'])
@@ -543,17 +563,19 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.assertEqual(response.status_code, 200)
 
         # thread displays
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
-        self.assertNotIn('thread-category-%s' % self.category_b.css_class, response.content)
-        self.assertIn('thread-category-%s' % self.category_c.css_class, response.content)
+        self.assertNotContains(response,
+            'thread-category-%s' % self.category_b.css_class)
+        self.assertContains(response,
+            'thread-category-%s' % self.category_c.css_class)
 
         # api displays same data
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 2)
         self.assertEqual(response_json['subcategories'][0], self.category_c.pk)
@@ -572,7 +594,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
     def test_api_hides_hidden_thread(self):
         """api returns empty due to no permission to see thread"""
@@ -589,7 +611,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_see_own_unapproved_thread(self):
@@ -602,14 +624,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_cant_see_unapproved_thread(self):
@@ -621,14 +643,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_cant_see_hidden_thread(self):
@@ -640,14 +662,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_cant_see_own_hidden_thread(self):
@@ -660,14 +682,14 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_user_can_see_own_hidden_thread(self):
@@ -684,7 +706,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories({
@@ -694,7 +716,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_can_see_hidden_thread(self):
@@ -712,7 +734,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories({
@@ -722,7 +744,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
     def test_list_user_can_see_unapproved_thread(self):
@@ -740,7 +762,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
 
         response = self.client.get('/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories({
@@ -750,7 +772,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
 
@@ -761,26 +783,26 @@ class MyThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/my/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'my/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_test_thread(self):
@@ -798,22 +820,22 @@ class MyThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/my/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
-        self.assertNotIn(other_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
+        self.assertNotContains(response, other_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'my/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
-        self.assertNotIn(other_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
+        self.assertNotContains(response, other_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -821,7 +843,7 @@ class MyThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -833,26 +855,26 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_new_thread(self):
@@ -865,20 +887,20 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -886,7 +908,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -908,20 +930,20 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -929,7 +951,7 @@ class NewThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -949,27 +971,27 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_cutoff_thread(self):
@@ -986,27 +1008,27 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_read_thread(self):
@@ -1025,27 +1047,27 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_category_read_thread(self):
@@ -1066,27 +1088,27 @@ class NewThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'new/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
 
@@ -1097,27 +1119,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn("empty-message", response.content)
+        self.assertContains(response, "empty-message")
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_renders_unread_thread(self):
@@ -1138,20 +1160,20 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -1159,7 +1181,7 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
         self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
 
@@ -1176,27 +1198,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_read_thread(self):
@@ -1215,27 +1237,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_global_cutoff_thread(self):
@@ -1259,27 +1281,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_user_cutoff_thread(self):
@@ -1301,27 +1323,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
     def test_list_hides_category_cutoff_thread(self):
@@ -1349,27 +1371,27 @@ class UnreadThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'unread/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
         self.access_all_categories()
         response = self.client.get('%s?list=unread&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
 
 
@@ -1389,30 +1411,30 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/subscribed/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'subscribed/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 1)
-        self.assertIn(test_thread.get_absolute_url(), response.content)
+        self.assertContains(response, test_thread.get_absolute_url())
 
     def test_list_hides_unsubscribed_thread(self):
         """list shows subscribed thread"""
@@ -1424,30 +1446,30 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
 
         response = self.client.get('/subscribed/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
 
         response = self.client.get(self.category_a.get_absolute_url() + 'subscribed/')
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk))
         self.assertEqual(response.status_code, 200)
 
-        response_json = json_loads(response.content)
+        response_json = json_loads(smart_str(response.content))
         self.assertEqual(len(response_json['results']), 0)
-        self.assertNotIn(test_thread.get_absolute_url(), response.content)
+        self.assertNotContains(response, test_thread.get_absolute_url())
 
 
 class UnapprovedListTests(ThreadsListTestCase):
@@ -1503,8 +1525,8 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         self.access_all_categories({
             'can_approve_content': True
@@ -1513,8 +1535,8 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         # test api
         self.access_all_categories({
@@ -1524,8 +1546,8 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
     def test_list_shows_owned_threads_for_unapproving_user(self):
         """
@@ -1547,16 +1569,16 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
         response = self.client.get('/unapproved/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         self.access_all_categories(base_acl={
             'can_see_unapproved_content_lists': True
         })
         response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())
 
         # test api
         self.access_all_categories(base_acl={
@@ -1564,5 +1586,5 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
         response = self.client.get('%s?list=unapproved' % self.api_link)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(visible_thread.get_absolute_url(), response.content)
-        self.assertNotIn(hidden_thread.get_absolute_url(), response.content)
+        self.assertContains(response, visible_thread.get_absolute_url())
+        self.assertNotContains(response, hidden_thread.get_absolute_url())

+ 3 - 2
misago/threads/tests/test_typestree.py

@@ -1,4 +1,5 @@
 from django.test import TestCase
+from django.utils import six
 
 from misago.categories.models import Category
 
@@ -75,7 +76,7 @@ class TreesMapTests(TestCase):
             trees_map.get_type_for_tree_id(tree_id + 1000)
             self.fail("invalid tree id should cause KeyError being raised")
         except KeyError as e:
-            self.assertIn("tree id has no type defined", unicode(e), "invalid exception message as given")
+            self.assertIn("tree id has no type defined", six.text_type(e), "invalid exception message as given")
 
     def test_get_tree_id_for_root(self):
         """TreesMap().get_tree_id_for_root() returns tree id for valid type name"""
@@ -91,4 +92,4 @@ class TreesMapTests(TestCase):
             trees_map.get_tree_id_for_root('hurr_durr')
             self.fail("invalid root name should cause KeyError being raised")
         except KeyError as e:
-            self.assertIn('"hurr_durr" root has no tree defined', unicode(e), "invalid exception message as given")
+            self.assertIn('"hurr_durr" root has no tree defined', six.text_type(e), "invalid exception message as given")

+ 2 - 2
misago/threads/viewmodels/category.py

@@ -13,8 +13,8 @@ class ViewModel(object):
         map(lambda c: add_acl(request.user, c), self.categories)
 
         self.category = self.get_category(request, self.categories, **kwargs)
-        self.subcategories = filter(self.category.has_child, self.categories)
-        self.children = filter(lambda s: s.parent_id == self.category.pk, self.subcategories)
+        self.subcategories = list(filter(self.category.has_child, self.categories))
+        self.children = list(filter(lambda s: s.parent_id == self.category.pk, self.subcategories))
 
     def get_categories(self, request):
         raise NotImplementedError('Category view model has to implement get_categories(request)')

+ 1 - 1
misago/threads/views/goto.py

@@ -41,7 +41,7 @@ class GotoView(View):
             return 1 # no chance for post to be on other page than only page
 
         # compute total count of thread pages
-        thread_pages = thread_len / settings.MISAGO_POSTS_PER_PAGE
+        thread_pages = thread_len // settings.MISAGO_POSTS_PER_PAGE
         thread_tail = thread_len - thread_pages * settings.MISAGO_POSTS_PER_PAGE
         if thread_tail > settings.MISAGO_POSTS_TAIL:
             thread_pages += 1

+ 1 - 1
misago/users/avatars/dynamic.py

@@ -44,7 +44,7 @@ COLOR_WHEEL_LEN = len(COLOR_WHEEL)
 def draw_avatar_bg(user, image):
     image_size = image.size
 
-    color_index = user.pk - COLOR_WHEEL_LEN * (user.pk / COLOR_WHEEL_LEN)
+    color_index = user.pk - COLOR_WHEEL_LEN * (user.pk // COLOR_WHEEL_LEN)
     main_color = COLOR_WHEEL[color_index]
 
     rgb = ImageColor.getrgb(main_color)

+ 2 - 2
misago/users/avatars/gravatar.py

@@ -1,4 +1,4 @@
-from StringIO import StringIO
+from io import BytesIO
 
 import requests
 from PIL import Image
@@ -27,7 +27,7 @@ def set_avatar(user):
             raise NoGravatarAvailable(
                 'gravatar is not available for this e-mail')
 
-        image = Image.open(StringIO(r.content))
+        image = Image.open(BytesIO(r.content))
         store.store_new_avatar(user, image)
     except requests.exceptions.RequestException:
         raise GravatarError('failed to connect to gravatar servers')

+ 5 - 3
misago/users/avatars/store.py

@@ -1,6 +1,8 @@
 import os
 from hashlib import md5
 
+from django.utils.encoding import force_bytes
+
 from path import Path
 from PIL import Image
 
@@ -60,8 +62,8 @@ def get_user_avatar_tokens(user):
     token_seeds = (user.email, user.avatar_hash, settings.SECRET_KEY)
 
     tokens = {
-        'org': md5('org:%s:%s:%s' % token_seeds).hexdigest()[:8],
-        'tmp': md5('tmp:%s:%s:%s' % token_seeds).hexdigest()[:8],
+        'org': md5(force_bytes('org:%s:%s:%s' % token_seeds)).hexdigest()[:8],
+        'tmp': md5(force_bytes('tmp:%s:%s:%s' % token_seeds)).hexdigest()[:8],
     }
 
     tokens.update({
@@ -112,7 +114,7 @@ def get_avatars_dir_path(user=None):
         except AttributeError:
             user_pk = user
 
-        dir_hash = md5(str(user_pk)).hexdigest()
+        dir_hash = md5(str(user_pk).encode()).hexdigest()
         hash_path = [dir_hash[0:1], dir_hash[2:3]]
         return Path(os.path.join(AVATARS_STORE, *hash_path))
     else:

+ 5 - 3
misago/users/credentialchange.py

@@ -6,7 +6,8 @@ Stores new e-mail and password in cache
 from hashlib import sha256
 
 from django.conf import settings
-
+from django.utils import six
+from django.utils.encoding import force_bytes
 from misago.core import serializer
 
 
@@ -52,7 +53,8 @@ def _make_change_token(user, token_type):
         user.password,
         user.last_login.replace(microsecond=0, tzinfo=None),
         settings.SECRET_KEY,
-        unicode(token_type)
+        six.text_type(token_type)
     )
 
-    return sha256('+'.join([unicode(s) for s in seeds])).hexdigest()
+    return sha256(
+        force_bytes('+'.join([six.text_type(s) for s in seeds]))).hexdigest()

+ 1 - 1
misago/users/models/user.py

@@ -416,7 +416,7 @@ class User(AbstractBaseUser, PermissionsMixin):
             else:
                 roles_pks.append('%s:%s' % (self.rank.pk, role.pk))
 
-        self.acl_key = md5(','.join(roles_pks)).hexdigest()[:12]
+        self.acl_key = md5(','.join(roles_pks).encode()).hexdigest()[:12]
 
     def email_user(self, subject, message, from_email=None, **kwargs):
         """

+ 2 - 3
misago/users/tests/test_activation_views.py

@@ -29,8 +29,7 @@ class ActivationViewsTests(TestCase):
             'pk': test_user.pk,
             'token': activation_token,
         }))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("<p>Nope!</p>", response.content)
+        self.assertContains(response, "<p>Nope!</p>", status_code=403)
 
         test_user = User.objects.get(pk=test_user.pk)
         self.assertEqual(test_user.requires_activation, 1)
@@ -81,7 +80,7 @@ class ActivationViewsTests(TestCase):
             'token': activation_token,
         }))
         self.assertEqual(response.status_code, 200)
-        self.assertIn("your account has been activated!", response.content)
+        self.assertContains(response, "your account has been activated!")
 
         test_user = User.objects.get(pk=test_user.pk)
         self.assertEqual(test_user.requires_activation, 0)

+ 24 - 39
misago/users/tests/test_auth_api.py

@@ -2,6 +2,7 @@ import json
 
 from django.contrib.auth import get_user_model
 from django.core import mail
+from django.utils.encoding import smart_str
 from django.test import TestCase
 
 from ..models import BAN_USERNAME, Ban
@@ -15,13 +16,12 @@ class GatewayTests(TestCase):
             '/api/auth/',
             data={'username': 'nope', 'password': 'nope'})
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn("Login or password is incorrect.", response.content)
+        self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
     def test_login(self):
@@ -39,15 +39,14 @@ class GatewayTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertEqual(user_json['id'], user.id)
         self.assertEqual(user_json['username'], user.username)
 
     def test_submit_empty(self):
         """login api errors for no body"""
         response = self.client.post('/api/auth/')
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('empty_data', response.content)
+        self.assertContains(response, 'empty_data', status_code=400)
 
     def test_login_banned(self):
         """login api fails to sign banned user in"""
@@ -66,7 +65,7 @@ class GatewayTests(TestCase):
         })
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['code'], 'banned')
         self.assertEqual(response_json['detail']['message']['plain'],
                          ban.user_message)
@@ -76,7 +75,7 @@ class GatewayTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
     def test_login_inactive_admin(self):
@@ -91,13 +90,13 @@ class GatewayTests(TestCase):
         })
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['code'], 'inactive_user')
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
     def test_login_inactive_user(self):
@@ -112,13 +111,13 @@ class GatewayTests(TestCase):
         })
         self.assertEqual(response.status_code, 400)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['code'], 'inactive_admin')
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
 
@@ -154,16 +153,14 @@ class SendActivationAPITests(TestCase):
     def test_submit_empty(self):
         """request activation link api errors for no body"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('empty_email', response.content)
+        self.assertContains(response, 'empty_email', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
     def test_submit_invalid(self):
         """request activation link api errors for invalid email"""
         response = self.client.post(self.link, data={'email': 'fake@mail.com'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('not_found', response.content)
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -173,9 +170,7 @@ class SendActivationAPITests(TestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Bob, your account is already active.',
-                      response.content)
+        self.assertContains(response, 'Bob, your account is already active.', status_code=400)
 
     def test_submit_inactive_user(self):
         """request activation link api errors for admin-activated users"""
@@ -183,8 +178,7 @@ class SendActivationAPITests(TestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_admin', response.content)
+        self.assertContains(response, 'inactive_admin', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -228,16 +222,14 @@ class SendPasswordFormAPITests(TestCase):
     def test_submit_empty(self):
         """request change password form link api errors for no body"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('empty_email', response.content)
+        self.assertContains(response, 'empty_email', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
     def test_submit_invalid(self):
         """request change password form link api errors for invalid email"""
         response = self.client.post(self.link, data={'email': 'fake@mail.com'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('not_found', response.content)
+        self.assertContains(response, 'not_found', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -247,15 +239,13 @@ class SendPasswordFormAPITests(TestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_user', response.content)
+        self.assertContains(response, 'inactive_user', status_code=400)
 
         self.user.requires_activation = 2
         self.user.save()
 
         response = self.client.post(self.link, data={'email': self.user.email})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('inactive_admin', response.content)
+        self.assertContains(response, 'inactive_admin', status_code=400)
 
         self.assertTrue(not mail.outbox)
 
@@ -285,8 +275,7 @@ class ChangePasswordAPITests(TestCase):
                 'asda7ad89sa7d9s789as'
             ))
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Form link is invalid.', response.content)
+        self.assertContains(response, 'Form link is invalid.', status_code=400)
 
     def test_banned_user_link(self):
         """request errors because user is banned"""
@@ -300,8 +289,7 @@ class ChangePasswordAPITests(TestCase):
                 self.user.pk,
                 make_password_change_token(self.user)
             ))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Your link has expired.', response.content)
+        self.assertContains(response, 'Your link has expired.', status_code=400)
 
     def test_inactive_user(self):
         """request change password form link api errors for inactive users"""
@@ -312,8 +300,7 @@ class ChangePasswordAPITests(TestCase):
                 self.user.pk,
                 make_password_change_token(self.user)
             ))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Your link has expired.', response.content)
+        self.assertContains(response, 'Your link has expired.', status_code=400)
 
         self.user.requires_activation = 2
         self.user.save()
@@ -322,8 +309,7 @@ class ChangePasswordAPITests(TestCase):
                 self.user.pk,
                 make_password_change_token(self.user)
             ))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Your link has expired.', response.content)
+        self.assertContains(response, 'Your link has expired.', status_code=400)
 
     def test_submit_empty(self):
         """submit change password form api errors for empty body"""
@@ -331,5 +317,4 @@ class ChangePasswordAPITests(TestCase):
                 self.user.pk,
                 make_password_change_token(self.user)
             ))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Valid password must', response.content)
+        self.assertContains(response, 'Valid password must', status_code=400)

+ 4 - 4
misago/users/tests/test_auth_views.py

@@ -2,6 +2,7 @@ import json
 
 from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
+from django.utils.encoding import smart_str
 from django.test import TestCase
 
 
@@ -43,13 +44,12 @@ class AuthViewsTests(TestCase):
         response = self.client.post(
             '/api/auth/', data={'username': 'nope', 'password': 'nope'})
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn("Login or password is incorrect.", response.content)
+        self.assertContains(response, "Login or password is incorrect.", status_code=400)
 
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
         response = self.client.post(reverse('misago:logout'))
@@ -58,5 +58,5 @@ class AuthViewsTests(TestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])

+ 6 - 6
misago/users/tests/test_banadmin_views.py

@@ -1,6 +1,7 @@
 from datetime import date, datetime, timedelta
 
 from django.core.urlresolvers import reverse
+from django.utils.six.moves import range
 
 from misago.admin.testutils import AdminTestCase
 
@@ -14,8 +15,7 @@ class BanAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:accounts:index'))
 
         response = self.client.get(response['location'])
-        self.assertIn(reverse('misago:admin:users:bans:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:users:bans:index'))
 
     def test_list_view(self):
         """bans list view returns 200"""
@@ -29,7 +29,7 @@ class BanAdminViewsTests(AdminTestCase):
         """adminview deletes multiple bans"""
         test_date = datetime.now() + timedelta(days=180)
 
-        for i in xrange(10):
+        for i in range(10):
             response = self.client.post(
                 reverse('misago:admin:users:bans:new'),
                 data={
@@ -75,7 +75,7 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(reverse('misago:admin:users:bans:index'))
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
-        self.assertIn('test@test.com', response.content)
+        self.assertContains(response, 'test@test.com')
 
     def test_edit_view(self):
         """edit ban view has no showstoppers"""
@@ -101,7 +101,7 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(reverse('misago:admin:users:bans:index'))
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
-        self.assertIn('test@test.com', response.content)
+        self.assertContains(response, 'test@test.com')
 
     def test_delete_view(self):
         """delete ban view has no showstoppers"""
@@ -125,4 +125,4 @@ class BanAdminViewsTests(AdminTestCase):
         response = self.client.get(response['location'])
 
         self.assertEqual(response.status_code, 200)
-        self.assertTrue(test_ban.banned_value not in response.content)
+        self.assertNotContains(response, test_ban.banned_value)

+ 2 - 1
misago/users/tests/test_bansmaintenance.py

@@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.utils import timezone
 from django.utils.six import StringIO
+from django.utils.six.moves import range
 
 from .. import bans
 from ..management.commands import bansmaintenance
@@ -14,7 +15,7 @@ class BansMaintenanceTests(TestCase):
     def test_expired_bans_handling(self):
         """expired bans are flagged as such"""
         # create 5 bans then update their valid date to past one
-        for i in xrange(5):
+        for i in range(5):
             Ban.objects.create(banned_value="abcd")
         expired_date = (timezone.now() - timedelta(days=10))
         Ban.objects.all().update(expires_on=expired_date, is_checked=True)

+ 2 - 1
misago/users/tests/test_captcha_api.py

@@ -1,6 +1,7 @@
 import json
 
 from django.core.urlresolvers import reverse
+from django.utils.encoding import smart_str
 from django.test import TestCase
 
 from misago.conf import settings
@@ -28,6 +29,6 @@ class AuthenticateAPITests(TestCase):
         response = self.client.get(self.api_link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['question'], 'Do you like pies?')
         self.assertEqual(response_json['help_text'], 'Type in "yes".')

+ 1 - 2
misago/users/tests/test_decorators.py

@@ -51,5 +51,4 @@ class DenyBannedIPTests(UserTestCase):
             user_message='Ya got banned!')
 
         response = self.client.post(reverse('misago:request-activation'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('<p>Ya got banned!</p>', response.content)
+        self.assertContains(response, '<p>Ya got banned!</p>', status_code=403)

+ 4 - 4
misago/users/tests/test_djangoadmin_auth.py

@@ -24,20 +24,20 @@ class DjangoAdminAuthTests(AdminTestCase):
 
         response = self.client.get(reverse('admin:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
     def test_logout(self):
         """its possible to sign out from django admin"""
         response = self.client.get(reverse('admin:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
         # assert there's no showstopper on signout page
         response = self.client.get(reverse('admin:logout'))
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(self.user.username, response.content)
+        self.assertNotContains(response, self.user.username)
 
         # user was signed out
         response = self.client.get(reverse('admin:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(self.user.username, response.content)
+        self.assertNotContains(response, self.user.username)

+ 4 - 8
misago/users/tests/test_forgottenpassword_views.py

@@ -37,8 +37,7 @@ class ForgottenPasswordViewsTests(UserTestCase):
                 'pk': test_user.pk,
                 'token': password_token,
             }))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('<p>Nope!</p>', response.content)
+        self.assertContains(response, '<p>Nope!</p>', status_code=403)
 
     def test_change_password_on_other_user(self):
         """change other user password errors"""
@@ -54,8 +53,7 @@ class ForgottenPasswordViewsTests(UserTestCase):
                 'pk': test_user.pk,
                 'token': password_token,
             }))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('your link has expired', response.content)
+        self.assertContains(response, 'your link has expired', status_code=400)
 
     def test_change_password_invalid_token(self):
         """invalid form token errors"""
@@ -69,8 +67,7 @@ class ForgottenPasswordViewsTests(UserTestCase):
                 'pk': test_user.pk,
                 'token': 'abcdfghqsads',
             }))
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('your link is invalid', response.content)
+        self.assertContains(response, 'your link is invalid', status_code=400)
 
     def test_change_password_form(self):
         """change user password form displays for valid token"""
@@ -84,5 +81,4 @@ class ForgottenPasswordViewsTests(UserTestCase):
                 'pk': test_user.pk,
                 'token': password_token,
             }))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(password_token, response.content)
+        self.assertContains(response, password_token)

+ 2 - 1
misago/users/tests/test_lists_views.py

@@ -1,5 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
+from django.utils.six.moves import range
 
 from misago.acl.testutils import override_acl
 
@@ -43,7 +44,7 @@ class ActivePostersTests(UsersListTestCase):
 
         # Create 200 test users and see if errors appeared
         User = get_user_model()
-        for i in xrange(200):
+        for i in range(200):
             User.objects.create_user(
                 'Bob%s' % i, 'm%s@te.com' % i, 'Pass.123', posts=12345)
 

+ 4 - 8
misago/users/tests/test_options_views.py

@@ -41,15 +41,13 @@ class ConfirmChangeEmailTests(AuthenticatedUserTestCase):
                 'token': 'invalid'
             }))
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn("Change confirmation link is invalid.", response.content)
+        self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
 
     def test_change_email(self):
         """valid token changes email"""
         response = self.client.get(self.link)
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("your e-mail has been changed", response.content)
+        self.assertContains(response, "your e-mail has been changed")
 
         self.reload_user()
         self.assertEqual(self.user.email, 'n3w@email.com')
@@ -78,15 +76,13 @@ class ConfirmChangePasswordTests(AuthenticatedUserTestCase):
                 'token': 'invalid'
             }))
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn("Change confirmation link is invalid.", response.content)
+        self.assertContains(response, "Change confirmation link is invalid.", status_code=400)
 
     def test_change_password(self):
         """valid token changes password"""
         response = self.client.get(self.link)
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("your password has been changed", response.content)
+        self.assertContains(response, "your password has been changed")
 
         self.reload_user()
         self.assertFalse(self.user.check_password(self.USER_PASSWORD))

+ 16 - 15
misago/users/tests/test_profile_views.py

@@ -1,5 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
+from django.utils.six.moves import range
 
 from misago.acl.testutils import override_acl
 
@@ -29,7 +30,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
                                            kwargs=self.link_kwargs))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('no messages posted', response.content)
+        self.assertContains(response, 'no messages posted')
 
     def test_user_threads_list(self):
         """user profile threads list has no showstoppers"""
@@ -37,7 +38,7 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
                                            kwargs=self.link_kwargs))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('no started threads', response.content)
+        self.assertContains(response, 'no started threads')
 
     def test_user_followers(self):
         """user profile followers list has no showstoppers"""
@@ -47,10 +48,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
                                            kwargs=self.link_kwargs))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('You have no followers.', response.content)
+        self.assertContains(response, 'You have no followers.')
 
         followers = []
-        for i in xrange(10):
+        for i in range(10):
             user_data = ("Follower%s" % i, "foll%s@test.com" % i, "Pass.123")
             followers.append(User.objects.create_user(*user_data))
             self.user.followed_by.add(followers[-1])
@@ -58,8 +59,8 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:user-followers',
                                            kwargs=self.link_kwargs))
         self.assertEqual(response.status_code, 200)
-        for i in xrange(10):
-            self.assertIn("Follower%s" % i, response.content)
+        for i in range(10):
+            self.assertContains(response, "Follower%s" % i)
 
     def test_user_follows(self):
         """user profile follows list has no showstoppers"""
@@ -69,10 +70,10 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
                                            kwargs=self.link_kwargs))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('You are not following any users.', response.content)
+        self.assertContains(response, 'You are not following any users.')
 
         followers = []
-        for i in xrange(10):
+        for i in range(10):
             user_data = ("Follower%s" % i, "foll%s@test.com" % i, "Pass.123")
             followers.append(User.objects.create_user(*user_data))
             followers[-1].followed_by.add(self.user)
@@ -80,15 +81,15 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:user-follows',
                                            kwargs=self.link_kwargs))
         self.assertEqual(response.status_code, 200)
-        for i in xrange(10):
-            self.assertIn("Follower%s" % i, response.content)
+        for i in range(10):
+            self.assertContains(response, "Follower%s" % i)
 
     def test_username_history_list(self):
         """user name changes history list has no showstoppers"""
         response = self.client.get(reverse('misago:username-history',
                                            kwargs=self.link_kwargs))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Your username was never changed.', response.content)
+        self.assertContains(response, 'Your username was never changed.')
 
         self.user.set_username('RenamedAdmin')
         self.user.save()
@@ -98,8 +99,8 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:username-history',
                                            kwargs=self.link_kwargs))
         self.assertEqual(response.status_code, 200)
-        self.assertIn("TestUser", response.content)
-        self.assertIn("RenamedAdmin", response.content)
+        self.assertContains(response, "TestUser")
+        self.assertContains(response, "RenamedAdmin")
 
     def test_user_ban_details(self):
         """user ban details page has no showstoppers"""
@@ -136,5 +137,5 @@ class UserProfileViewsTests(AuthenticatedUserTestCase):
         response = self.client.get(reverse('misago:user-ban',
                                            kwargs=link_kwargs))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('User m3ss4ge', response.content)
-        self.assertIn('Staff m3ss4ge', response.content)
+        self.assertContains(response, 'User m3ss4ge')
+        self.assertContains(response, 'Staff m3ss4ge')

+ 11 - 12
misago/users/tests/test_rankadmin_views.py

@@ -13,15 +13,14 @@ class RankAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:accounts:index'))
 
         response = self.client.get(response['location'])
-        self.assertIn(reverse('misago:admin:users:ranks:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:users:ranks:index'))
 
     def test_list_view(self):
         """ranks list view returns 200"""
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Team', response.content)
+        self.assertContains(response, 'Team')
 
     def test_new_view(self):
         """new rank view has no showstoppers"""
@@ -47,8 +46,8 @@ class RankAdminViewsTests(AdminTestCase):
 
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Rank', response.content)
-        self.assertIn('Test Title', response.content)
+        self.assertContains(response, 'Test Rank')
+        self.assertContains(response, 'Test Title')
 
         test_rank = Rank.objects.get(slug='test-rank')
         self.assertIn(test_role_a, test_rank.roles.all())
@@ -78,8 +77,8 @@ class RankAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:ranks:edit',
                     kwargs={'pk': test_rank.pk}))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_rank.name, response.content)
-        self.assertIn(test_rank.title, response.content)
+        self.assertContains(response, test_rank.name)
+        self.assertContains(response, test_rank.title)
 
         response = self.client.post(
             reverse('misago:admin:users:ranks:edit',
@@ -93,7 +92,7 @@ class RankAdminViewsTests(AdminTestCase):
         test_rank = Rank.objects.get(slug='top-lel')
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_rank.name, response.content)
+        self.assertContains(response, test_rank.name)
         self.assertTrue('Test Rank' not in test_rank.roles.all())
         self.assertTrue('Test Title' not in test_rank.roles.all())
 
@@ -214,8 +213,8 @@ class RankAdminViewsTests(AdminTestCase):
         response = self.client.get(reverse('misago:admin:users:ranks:index'))
         self.assertEqual(response.status_code, 200)
 
-        self.assertTrue(test_rank.name not in response.content)
-        self.assertTrue(test_rank.title not in response.content)
+        self.assertNotContains(response, test_rank.name)
+        self.assertNotContains(response, test_rank.title)
 
     def test_uniquess(self):
         """rank slug uniqueness is enforced by admin forms"""
@@ -233,7 +232,7 @@ class RankAdminViewsTests(AdminTestCase):
             })
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn("This name collides with other rank.", response.content)
+        self.assertContains(response, "This name collides with other rank.")
 
         self.client.post(
             reverse('misago:admin:users:ranks:new'),
@@ -256,4 +255,4 @@ class RankAdminViewsTests(AdminTestCase):
                 'roles': [test_role_a.pk],
             })
         self.assertEqual(response.status_code, 200)
-        self.assertIn("This name collides with other rank.", response.content)
+        self.assertContains(response, "This name collides with other rank.")

+ 7 - 7
misago/users/tests/test_testutils.py

@@ -1,7 +1,7 @@
 import json
 
 from django.core.urlresolvers import reverse
-
+from django.utils.encoding import smart_str
 from ..testutils import AuthenticatedUserTestCase, SuperUserTestCase, UserTestCase
 
 
@@ -36,7 +36,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertEqual(user_json['id'], user.id)
 
     def test_login_superuser(self):
@@ -47,7 +47,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertEqual(user_json['id'], user.id)
 
     def test_logout_user(self):
@@ -59,7 +59,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
     def test_logout_superuser(self):
@@ -71,7 +71,7 @@ class UserTestCaseTests(UserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertIsNone(user_json['id'])
 
 
@@ -79,7 +79,7 @@ class AuthenticatedUserTestCaseTests(AuthenticatedUserTestCase):
     def test_setup(self):
         """setup executed correctly"""
         response = self.client.get(reverse('misago:index'))
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
     def test_reload_user(self):
         """reload_user reloads user"""
@@ -98,5 +98,5 @@ class SuperUserTestCaseTests(SuperUserTestCase):
         response = self.client.get('/api/auth/')
         self.assertEqual(response.status_code, 200)
 
-        user_json = json.loads(response.content)
+        user_json = json.loads(smart_str(response.content))
         self.assertEqual(user_json['id'], self.user.id)

+ 24 - 36
misago/users/tests/test_user_avatar_api.py

@@ -4,6 +4,7 @@ from path import Path
 
 from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
+from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
 from misago.conf import settings
@@ -26,7 +27,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             response = self.client.get(self.link)
             self.assertEqual(response.status_code, 200)
 
-            options = json.loads(response.content)
+            options = json.loads(smart_str(response.content))
             self.assertTrue(options['generated'])
             self.assertFalse(options['gravatar'])
             self.assertFalse(options['crop_org'])
@@ -40,7 +41,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             response = self.client.get(self.link)
             self.assertEqual(response.status_code, 200)
 
-            options = json.loads(response.content)
+            options = json.loads(smart_str(response.content))
             self.assertTrue(options['generated'])
             self.assertTrue(options['gravatar'])
             self.assertFalse(options['crop_org'])
@@ -55,30 +56,26 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('Your avatar is pwnt', response.content)
+        self.assertContains(response, 'Your avatar is pwnt', status_code=403)
 
     def test_other_user_avatar(self):
         """requests to api error if user tries to access other user"""
         self.logout_user();
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('You have to sign in', response.content)
+        self.assertContains(response, 'You have to sign in', status_code=403)
 
         User = get_user_model()
         self.login_user(User.objects.create_user(
             "BobUser", "bob@bob.com", self.USER_PASSWORD))
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('can\'t change other users avatars', response.content)
+        self.assertContains(response, 'can\'t change other users avatars', status_code=403)
 
     def test_empty_requests(self):
         """empty request errors with code 400"""
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Unknown avatar type.', response.content)
+        self.assertContains(response, 'Unknown avatar type.', status_code=400)
 
     def test_failed_gravatar_request(self):
         """no gravatar RPC fails"""
@@ -86,8 +83,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('No Gravatar is associated', response.content)
+        self.assertContains(response, 'No Gravatar is associated', status_code=400)
 
     def test_successful_gravatar_request(self):
         """gravatar RPC fails"""
@@ -95,14 +91,12 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Gravatar was downloaded and set', response.content)
+        self.assertContains(response, 'Gravatar was downloaded and set')
 
     def test_generation_request(self):
         """generated avatar is set"""
         response = self.client.post(self.link, data={'avatar': 'generated'})
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('New avatar based on your account', response.content)
+        self.assertContains(response, 'New avatar based on your account')
 
     def test_avatar_upload_and_crop(self):
         """avatar can be uploaded and cropped"""
@@ -110,11 +104,10 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
 
         response = self.client.post(self.link, data={'avatar': 'upload'})
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('No file was sent.', response.content)
+        self.assertContains(response, 'No file was sent.', status_code=400)
 
         avatar_path = (settings.MEDIA_ROOT, 'avatars', 'blank.png')
-        with open('/'.join(avatar_path)) as avatar:
+        with open('/'.join(avatar_path), 'rb') as avatar:
             response = self.client.post(self.link,
                                         data={
                                             'avatar': 'upload',
@@ -122,7 +115,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                                         })
             self.assertEqual(response.status_code, 200)
 
-            response_json = json.loads(response.content)
+            response_json = json.loads(smart_str(response.content))
             self.assertTrue(response_json['options']['crop_tmp'])
 
             avatar_dir = store.get_existing_avatars_dir(self.user)
@@ -150,10 +143,10 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                     }
                 }),
                 content_type="application/json")
-            response_json = json.loads(response.content)
+            response_json = json.loads(smart_str(response.content))
 
             self.assertEqual(response.status_code, 200)
-            self.assertIn('Uploaded avatar was set.', response.content)
+            self.assertContains(response, 'Uploaded avatar was set.')
 
             avatar_dir = store.get_existing_avatars_dir(self.user)
             avatar = Path('%s/%s_tmp.png' % (avatar_dir, self.user.pk))
@@ -183,8 +176,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                     }
                 }),
                 content_type="application/json")
-            self.assertEqual(response.status_code, 400)
-            self.assertIn('This avatar type is not allowed.', response.content)
+            self.assertContains(response, 'This avatar type is not allowed.', status_code=400)
 
             response = self.client.post(self.link, json.dumps({
                     'avatar': 'crop_org',
@@ -196,15 +188,14 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                     }
                 }),
                 content_type="application/json")
-            self.assertEqual(response.status_code, 200)
-            self.assertIn('Avatar was re-cropped.', response.content)
+            self.assertContains(response, 'Avatar was re-cropped.')
 
     def test_gallery(self):
         """its possible to set avatar from gallery"""
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertTrue(options['galleries'])
 
         for gallery in options['galleries']:
@@ -214,8 +205,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                     'image': image
                 })
 
-                self.assertEqual(response.status_code, 200)
-                self.assertIn('Avatar from gallery was set.', response.content)
+                self.assertContains(response, 'Avatar from gallery was set.')
 
 
 class UserAvatarModerationTests(AuthenticatedUserTestCase):
@@ -238,16 +228,14 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't moderate avatars", response.content)
+        self.assertContains(response, "can't moderate avatars", status_code=403)
 
         override_acl(self.user, {
             'can_moderate_avatars': 0,
         })
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't moderate avatars", response.content)
+        self.assertContains(response, "can't moderate avatars", status_code=403)
 
     def test_moderate_avatar(self):
         """moderate avatar"""
@@ -258,7 +246,7 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertEqual(options['is_avatar_locked'],
                          self.other_user.is_avatar_locked)
         self.assertEqual(options['avatar_lock_user_message'],
@@ -281,7 +269,7 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         User = get_user_model()
         other_user = User.objects.get(pk=self.other_user.pk)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertEqual(other_user.is_avatar_locked, True)
         self.assertEqual(
             other_user.avatar_lock_user_message, "Test user message.")
@@ -311,7 +299,7 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
 
         other_user = User.objects.get(pk=self.other_user.pk)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertEqual(options['avatar_hash'],
                          other_user.avatar_hash)
         self.assertEqual(options['is_avatar_locked'],

+ 4 - 8
misago/users/tests/test_user_changeemail_api.py

@@ -39,8 +39,7 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
             'new_email': 'new@email.com',
             'password': 'Lor3mIpsum'
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('password is invalid', response.content)
+        self.assertContains(response, 'password is invalid', status_code=400)
 
     def test_invalid_input(self):
         """api errors correctly for invalid input"""
@@ -48,15 +47,13 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
             'new_email': '',
             'password': self.USER_PASSWORD
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('new_email":["This field is required', response.content)
+        self.assertContains(response, 'new_email":["This field is required', status_code=400)
 
         response = self.client.post(self.link, data={
             'new_email': 'newmail',
             'password': self.USER_PASSWORD
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('valid email address', response.content)
+        self.assertContains(response, 'valid email address', status_code=400)
 
     def test_email_taken(self):
         """api validates email usage"""
@@ -67,5 +64,4 @@ class UserChangeEmailTests(AuthenticatedUserTestCase):
             'new_email': 'new@email.com',
             'password': self.USER_PASSWORD
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('not available', response.content)
+        self.assertContains(response, 'not available', status_code=400)

+ 3 - 7
misago/users/tests/test_user_changepassword_api.py

@@ -39,8 +39,7 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
             'new_password': 'N3wP@55w0rd',
             'password': 'Lor3mIpsum'
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('password is invalid', response.content)
+        self.assertContains(response, 'password is invalid', status_code=400)
 
     def test_invalid_input(self):
         """api errors correctly for invalid input"""
@@ -48,13 +47,10 @@ class UserChangePasswordTests(AuthenticatedUserTestCase):
             'new_password': '',
             'password': self.USER_PASSWORD
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('new_password":["This field is required',
-                      response.content)
+        self.assertContains(response, 'new_password":["This field is required', status_code=400)
 
         response = self.client.post(self.link, data={
             'new_password': 'n',
             'password': self.USER_PASSWORD
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('password must be', response.content)
+        self.assertContains(response, 'password must be', status_code=400)

+ 11 - 15
misago/users/tests/test_user_create_api.py

@@ -28,8 +28,7 @@ class UserCreateTests(UserTestCase):
         settings.override_setting('account_activation', 'closed')
 
         response = self.client.post('/api/users/')
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('closed', response.content)
+        self.assertContains(response, 'closed', status_code=403)
 
     def test_registration_creates_active_user(self):
         """api creates active and signed in user on POST"""
@@ -40,10 +39,9 @@ class UserCreateTests(UserTestCase):
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('active', response.content)
-        self.assertIn('Bob', response.content)
-        self.assertIn('bob@bob.com', response.content)
+        self.assertContains(response, 'active')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
 
         User = get_user_model()
         User.objects.get_by_username('Bob')
@@ -52,7 +50,7 @@ class UserCreateTests(UserTestCase):
         self.assertEqual(Online.objects.filter(user=test_user).count(), 1)
 
         response = self.client.get(reverse('misago:index'))
-        self.assertIn('Bob', response.content)
+        self.assertContains(response, 'Bob')
 
         self.assertIn('Welcome', mail.outbox[0].subject)
 
@@ -65,10 +63,9 @@ class UserCreateTests(UserTestCase):
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('user', response.content)
-        self.assertIn('Bob', response.content)
-        self.assertIn('bob@bob.com', response.content)
+        self.assertContains(response, 'user')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
 
         User = get_user_model()
         User.objects.get_by_username('Bob')
@@ -85,10 +82,9 @@ class UserCreateTests(UserTestCase):
                                           'email': 'bob@bob.com',
                                           'password': 'pass123'})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('admin', response.content)
-        self.assertIn('Bob', response.content)
-        self.assertIn('bob@bob.com', response.content)
+        self.assertContains(response, 'admin')
+        self.assertContains(response, 'Bob')
+        self.assertContains(response, 'bob@bob.com')
 
         User = get_user_model()
         User.objects.get_by_username('Bob')

+ 7 - 9
misago/users/tests/test_user_signature_api.py

@@ -1,6 +1,7 @@
 import json
 
 from django.contrib.auth import get_user_model
+from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
 from misago.conf import settings
@@ -23,8 +24,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("You don't have permission to change", response.content)
+        self.assertContains(response, "You don't have permission to change", status_code=403)
 
     def test_signature_locked(self):
         """locked edit signature returns 403"""
@@ -37,8 +37,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         self.user.save()
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn('Your siggy is banned', response.content)
+        self.assertContains(response, 'Your siggy is banned', status_code=403)
 
     def test_get_signature(self):
         """GET to api returns json with no signature"""
@@ -52,7 +51,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertFalse(response_json['signature'])
 
     def test_post_empty_signature(self):
@@ -67,7 +66,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={'signature': ''})
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertFalse(response_json['signature'])
 
     def test_post_too_long_signature(self):
@@ -82,8 +81,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={
             'signature': 'abcd' * 1000
         })
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('too long', response.content)
+        self.assertContains(response, 'too long', status_code=400)
 
     def test_post_good_signature(self):
         """POST with good signature changes user signature"""
@@ -99,7 +97,7 @@ class UserSignatureTests(AuthenticatedUserTestCase):
         })
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['signature']['html'],
                          '<p>Hello, <strong>bros</strong>!</p>')
         self.assertEqual(response_json['signature']['plain'],

+ 23 - 27
misago/users/tests/test_user_username_api.py

@@ -1,6 +1,8 @@
 import json
 
 from django.contrib.auth import get_user_model
+from django.utils.encoding import smart_str
+from django.utils.six.moves import range
 
 from misago.acl.testutils import override_acl
 from misago.conf import settings
@@ -21,7 +23,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
 
         self.assertIsNotNone(response_json['changes_left'])
         self.assertEqual(response_json['length_min'],
@@ -30,13 +32,13 @@ class UserUsernameTests(AuthenticatedUserTestCase):
                          settings.username_length_max)
         self.assertIsNone(response_json['next_on'])
 
-        for i in xrange(response_json['changes_left']):
+        for i in range(response_json['changes_left']):
             self.user.set_username('NewName%s' % i, self.user)
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['changes_left'], 0)
         self.assertIsNotNone(response_json['next_on'])
 
@@ -45,28 +47,26 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        response_json = json.loads(response.content)
-        for i in xrange(response_json['changes_left']):
+        response_json = json.loads(smart_str(response.content))
+        for i in range(response_json['changes_left']):
             self.user.set_username('NewName%s' % i, self.user)
 
         response = self.client.get(self.link)
-        response_json = json.loads(response.content)
+        response_json = json.loads(smart_str(response.content))
         self.assertEqual(response_json['changes_left'], 0)
 
         response = self.client.post(self.link, data={
             'username': 'Pointless'
         })
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('change your username now', response.content)
+        self.assertContains(response, 'change your username now', status_code=400)
         self.assertTrue(self.user.username != 'Pointless')
 
     def test_change_username_no_input(self):
         """api returns error 400 if new username is empty"""
         response = self.client.post(self.link, data={})
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('Enter new username.', response.content)
+        self.assertContains(response, 'Enter new username.', status_code=400)
 
     def test_change_username_invalid_name(self):
         """api returns error 400 if new username is wrong"""
@@ -74,13 +74,12 @@ class UserUsernameTests(AuthenticatedUserTestCase):
             'username': '####'
         })
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn('can only contain latin', response.content)
+        self.assertContains(response, 'can only contain latin', status_code=400)
 
     def test_change_username(self):
         """api changes username and records change"""
         response = self.client.get(self.link)
-        changes_left = json.loads(response.content)['changes_left']
+        changes_left = json.loads(smart_str(response.content))['changes_left']
 
         username = self.user.username
         new_username = 'NewUsernamu'
@@ -90,7 +89,7 @@ class UserUsernameTests(AuthenticatedUserTestCase):
         })
 
         self.assertEqual(response.status_code, 200)
-        options = json.loads(response.content)['options']
+        options = json.loads(smart_str(response.content))['options']
         self.assertEqual(changes_left, options['changes_left'] + 1)
 
         self.reload_user()
@@ -120,16 +119,14 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't rename users", response.content)
+        self.assertContains(response, "can't rename users", status_code=403)
 
         override_acl(self.user, {
             'can_rename_users': 0,
         })
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't rename users", response.content)
+        self.assertContains(response, "can't rename users", status_code=403)
 
     def test_moderate_username(self):
         """moderate username"""
@@ -140,7 +137,7 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertEqual(options['length_min'],
                          settings.username_length_min)
         self.assertEqual(options['length_max'],
@@ -155,8 +152,7 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type="application/json")
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn("Enter new username", response.content)
+        self.assertContains(response, "Enter new username", status_code=400)
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -167,10 +163,9 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             }),
             content_type="application/json")
 
-        self.assertEqual(response.status_code, 400)
-        self.assertIn(
+        self.assertContains(response,
             "Username can only contain latin alphabet letters and digits.",
-            response.content)
+            status_code=400)
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -182,8 +177,9 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
             content_type="application/json")
 
         self.assertEqual(response.status_code, 400)
-        self.assertIn(
-            "Username must be at least 3 characters long.", response.content)
+        self.assertContains(response,
+            "Username must be at least 3 characters long.",
+            status_code=400)
 
         override_acl(self.user, {
             'can_rename_users': 1,
@@ -202,7 +198,7 @@ class UserUsernameModerationTests(AuthenticatedUserTestCase):
         self.assertEqual('BobBoberson', other_user.username)
         self.assertEqual('bobboberson', other_user.slug)
 
-        options = json.loads(response.content)
+        options = json.loads(smart_str(response.content))
         self.assertEqual(options['username'], other_user.username)
         self.assertEqual(options['slug'], other_user.slug)
 

+ 26 - 24
misago/users/tests/test_useradmin_views.py

@@ -3,6 +3,9 @@ import json
 from django.contrib.auth import get_user_model
 from django.core import mail
 from django.core.urlresolvers import reverse
+from django.utils import six
+from django.utils.six.moves import range
+from django.utils.encoding import smart_str
 
 from misago.acl.models import Role
 from misago.admin.testutils import AdminTestCase
@@ -17,8 +20,7 @@ class UserAdminViewsTests(AdminTestCase):
         """admin index view contains users link"""
         response = self.client.get(reverse('misago:admin:index'))
 
-        self.assertIn(reverse('misago:admin:users:accounts:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:users:accounts:index'))
 
     def test_list_view(self):
         """users list view returns 200"""
@@ -28,7 +30,7 @@ class UserAdminViewsTests(AdminTestCase):
 
         response = self.client.get(response['location'])
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
     def test_list_search(self):
         """users list is searchable"""
@@ -47,27 +49,27 @@ class UserAdminViewsTests(AdminTestCase):
         # Search both
         response = self.client.get(link_base + '&username=tyr')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(user_a.username, response.content)
-        self.assertIn(user_b.username, response.content)
+        self.assertContains(response, user_a.username)
+        self.assertContains(response, user_b.username)
 
         # Search tyrion
         response = self.client.get(link_base + '&username=tyrion')
         self.assertEqual(response.status_code, 200)
-        self.assertFalse(user_a.username in response.content)
-        self.assertIn(user_b.username, response.content)
+        self.assertNotContains(response, user_a.username)
+        self.assertContains(response, user_b.username)
 
         # Search tyrael
         response = self.client.get(link_base + '&email=t123@test.com')
         self.assertEqual(response.status_code, 200)
-        self.assertIn(user_a.username, response.content)
-        self.assertFalse(user_b.username in response.content)
+        self.assertContains(response, user_a.username)
+        self.assertNotContains(response, user_b.username)
 
     def test_mass_activation(self):
         """users list activates multiple users"""
         User = get_user_model()
 
         user_pks = []
-        for i in xrange(10):
+        for i in range(10):
             test_user = User.objects.create_user(
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
@@ -91,7 +93,7 @@ class UserAdminViewsTests(AdminTestCase):
         User = get_user_model()
 
         user_pks = []
-        for i in xrange(10):
+        for i in range(10):
             test_user = User.objects.create_user(
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
@@ -124,7 +126,7 @@ class UserAdminViewsTests(AdminTestCase):
         User = get_user_model()
 
         user_pks = []
-        for i in xrange(10):
+        for i in range(10):
             test_user = User.objects.create_user(
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
@@ -144,7 +146,7 @@ class UserAdminViewsTests(AdminTestCase):
         User = get_user_model()
 
         user_pks = []
-        for i in xrange(10):
+        for i in range(10):
             test_user = User.objects.create_user(
                 'Bob%s' % i,
                 'bob%s@test.com' % i,
@@ -171,8 +173,8 @@ class UserAdminViewsTests(AdminTestCase):
         response = self.client.post(reverse('misago:admin:users:accounts:new'),
             data={
                 'username': 'Bawww',
-                'rank': unicode(default_rank.pk),
-                'roles': unicode(authenticated_role.pk),
+                'rank': six.text_type(default_rank.pk),
+                'roles': six.text_type(authenticated_role.pk),
                 'email': 'reg@stered.com',
                 'new_password': 'pass123',
                 'staff_level': '0'
@@ -194,8 +196,8 @@ class UserAdminViewsTests(AdminTestCase):
 
         response = self.client.post(test_link, data={
             'username': 'Bawww',
-            'rank': unicode(test_user.rank_id),
-            'roles': unicode(test_user.roles.all()[0].pk),
+            'rank': six.text_type(test_user.rank_id),
+            'roles': six.text_type(test_user.roles.all()[0].pk),
             'email': 'reg@stered.com',
             'new_password': 'pass123',
             'staff_level': '0',
@@ -223,19 +225,19 @@ class UserAdminViewsTests(AdminTestCase):
                             kwargs={'pk': test_user.pk})
 
         category = Category.objects.all_categories()[:1][0]
-        [post_thread(category, poster=test_user) for i in xrange(10)]
+        [post_thread(category, poster=test_user) for i in range(10)]
 
         response = self.client.post(test_link, **self.ajax_header)
         self.assertEqual(response.status_code, 200)
 
-        response_dict = json.loads(response.content)
+        response_dict = json.loads(smart_str(response.content))
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertFalse(response_dict['is_completed'])
 
         response = self.client.post(test_link, **self.ajax_header)
         self.assertEqual(response.status_code, 200)
 
-        response_dict = json.loads(response.content)
+        response_dict = json.loads(smart_str(response.content))
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertTrue(response_dict['is_completed'])
 
@@ -248,19 +250,19 @@ class UserAdminViewsTests(AdminTestCase):
 
         category = Category.objects.all_categories()[:1][0]
         thread = post_thread(category)
-        [reply_thread(thread, poster=test_user) for i in xrange(10)]
+        [reply_thread(thread, poster=test_user) for i in range(10)]
 
         response = self.client.post(test_link, **self.ajax_header)
         self.assertEqual(response.status_code, 200)
 
-        response_dict = json.loads(response.content)
+        response_dict = json.loads(smart_str(response.content))
         self.assertEqual(response_dict['deleted_count'], 10)
         self.assertFalse(response_dict['is_completed'])
 
         response = self.client.post(test_link, **self.ajax_header)
         self.assertEqual(response.status_code, 200)
 
-        response_dict = json.loads(response.content)
+        response_dict = json.loads(smart_str(response.content))
         self.assertEqual(response_dict['deleted_count'], 0)
         self.assertTrue(response_dict['is_completed'])
 
@@ -274,5 +276,5 @@ class UserAdminViewsTests(AdminTestCase):
         response = self.client.post(test_link, **self.ajax_header)
         self.assertEqual(response.status_code, 200)
 
-        response_dict = json.loads(response.content)
+        response_dict = json.loads(smart_str(response.content))
         self.assertTrue(response_dict['is_completed'])

+ 5 - 7
misago/users/tests/test_usernamechanges_api.py

@@ -18,7 +18,7 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get('%s?user=%s' % (self.link, self.user.pk))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
     def test_list_handles_invalid_filter(self):
         """list raises 404 for invalid filter"""
@@ -48,13 +48,13 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
             '%s?user=%s&search=new' % (self.link, self.user.pk))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
         response = self.client.get(
             '%s?user=%s&search=usernew' % (self.link, self.user.pk))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('[]', response.content)
+        self.assertContains(response, '[]')
 
     def test_list_denies_permission(self):
         """list denies permission for other user (or all) if no access"""
@@ -62,9 +62,7 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
 
         response = self.client.get(
             '%s?user=%s' % (self.link, self.user.pk + 1))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("don't have permission to", response.content)
+        self.assertContains(response, "don't have permission to", status_code=403)
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("don't have permission to", response.content)
+        self.assertContains(response, "don't have permission to", status_code=403)

+ 29 - 35
misago/users/tests/test_users_api.py

@@ -2,6 +2,7 @@ import json
 from datetime import timedelta
 
 from django.contrib.auth import get_user_model
+from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
@@ -34,11 +35,11 @@ class ActivePostersListTests(AuthenticatedUserTestCase):
         """empty list is served"""
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(self.user.username, response.content)
+        self.assertNotContains(response, self.user.username)
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertNotIn(self.user.username, response.content)
+        self.assertNotContains(response, self.user.username)
 
     def test_filled_list(self):
         """filled list is served"""
@@ -50,18 +51,18 @@ class ActivePostersListTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
-        self.assertIn('"is_online":true', response.content)
-        self.assertIn('"is_offline":false', response.content)
+        self.assertContains(response, self.user.username)
+        self.assertContains(response, '"is_online":true')
+        self.assertContains(response, '"is_offline":false')
 
         self.logout_user()
         build_active_posters_ranking()
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
-        self.assertIn('"is_online":false', response.content)
-        self.assertIn('"is_offline":true', response.content)
+        self.assertContains(response, self.user.username)
+        self.assertContains(response, '"is_online":false')
+        self.assertContains(response, '"is_offline":true')
 
 
 class FollowersListTests(AuthenticatedUserTestCase):
@@ -91,7 +92,7 @@ class FollowersListTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link % self.user.pk)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_follower.username, response.content)
+        self.assertContains(response, test_follower.username)
 
 
 class FollowsListTests(AuthenticatedUserTestCase):
@@ -121,7 +122,7 @@ class FollowsListTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link % self.user.pk)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_follower.username, response.content)
+        self.assertContains(response, test_follower.username)
 
 
 class RankListTests(AuthenticatedUserTestCase):
@@ -163,7 +164,7 @@ class RankListTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link % self.user.rank.pk)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
 
 class SearchNamesListTests(AuthenticatedUserTestCase):
@@ -183,7 +184,7 @@ class SearchNamesListTests(AuthenticatedUserTestCase):
         """filled list returns 200"""
         response = self.client.get(self.link + self.user.slug)
         self.assertEqual(response.status_code, 200)
-        self.assertIn(self.user.username, response.content)
+        self.assertContains(response, self.user.username)
 
 
 class UserCategoriesOptionsTests(AuthenticatedUserTestCase):
@@ -206,7 +207,7 @@ class UserCategoriesOptionsTests(AuthenticatedUserTestCase):
         )
 
         for field in fields:
-            self.assertIn('"%s"' % field, response.content)
+            self.assertContains(response, '"%s"' % field, status_code=400)
 
     def test_change_forum_options(self):
         """forum options are changed"""
@@ -243,14 +244,12 @@ class UserFollowTests(AuthenticatedUserTestCase):
         self.logout_user()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("action is not available to guests", response.content)
+        self.assertContains(response, "action is not available to guests", status_code=403)
 
     def test_follow_myself(self):
         """you can't follow yourself"""
         response = self.client.post('/api/users/%s/follow/' % self.user.pk)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't add yourself to followed", response.content)
+        self.assertContains(response, "can't add yourself to followed", status_code=403)
 
     def test_cant_follow(self):
         """no permission to follow users"""
@@ -259,8 +258,7 @@ class UserFollowTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't follow other users", response.content)
+        self.assertContains(response, "can't follow other users", status_code=403)
 
     def test_follow(self):
         """follow and unfollow other user"""
@@ -317,8 +315,7 @@ class UserBanTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't see users bans details", response.content)
+        self.assertContains(response, "can't see users bans details", status_code=403)
 
     def test_no_ban(self):
         """api returns empty json"""
@@ -328,7 +325,7 @@ class UserBanTests(AuthenticatedUserTestCase):
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.content, '{}')
+        self.assertEqual(smart_str(response.content), '{}')
 
     def test_ban_details(self):
         """api returns ban json"""
@@ -343,7 +340,7 @@ class UserBanTests(AuthenticatedUserTestCase):
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)
 
-        ban_json = json.loads(response.content)
+        ban_json = json.loads(smart_str(response.content))
         self.assertEqual(ban_json['user_message']['plain'], 'Nope!')
         self.assertEqual(ban_json['user_message']['html'], '<p>Nope!</p>')
 
@@ -380,7 +377,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete users", response.content)
+        self.assertContains(response, "can't delete users", status_code=403)
 
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
@@ -394,7 +391,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete users", response.content)
+        self.assertContains(response, "can't delete users", status_code=403)
 
     def test_delete_too_many_posts(self):
         """raises 403 error when user has too many posts"""
@@ -408,8 +405,8 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete users", response.content)
-        self.assertIn("made more than 5 posts", response.content)
+        self.assertContains(response, "can't delete users", status_code=403)
+        self.assertContains(response, "made more than 5 posts", status_code=403)
 
     def test_delete_too_old_member(self):
         """raises 403 error when user is too old"""
@@ -423,8 +420,8 @@ class UserDeleteTests(AuthenticatedUserTestCase):
 
         response = self.client.post(self.link)
         self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete users", response.content)
-        self.assertIn("members for more than 5 days", response.content)
+        self.assertContains(response, "can't delete users", status_code=403)
+        self.assertContains(response, "members for more than 5 days", status_code=403)
 
     def test_delete_self(self):
         """raises 403 error when attempting to delete oneself"""
@@ -434,8 +431,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         })
 
         response = self.client.post('/api/users/%s/delete/' % self.user.pk)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete yourself", response.content)
+        self.assertContains(response, "can't delete yourself", status_code=403)
 
     def test_delete_admin(self):
         """raises 403 error when attempting to delete admin"""
@@ -448,8 +444,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete administrators", response.content)
+        self.assertContains(response, "can't delete administrators", status_code=403)
 
     def test_delete_superadmin(self):
         """raises 403 error when attempting to delete superadmin"""
@@ -462,8 +457,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.other_user.save()
 
         response = self.client.post(self.link)
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("can't delete administrators", response.content)
+        self.assertContains(response, "can't delete administrators", status_code=403)
 
     def test_delete_with_content(self):
         """returns 200 and deletes user with content"""

+ 7 - 8
misago/users/tests/test_warningadmin_views.py

@@ -12,8 +12,7 @@ class WarningsAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:accounts:index'))
 
         response = self.client.get(response['location'])
-        self.assertIn(reverse('misago:admin:users:warnings:index'),
-                      response.content)
+        self.assertContains(response, reverse('misago:admin:users:warnings:index'))
 
     def test_list_view(self):
         """warning levels list view returns 200"""
@@ -21,7 +20,7 @@ class WarningsAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:warnings:index'))
 
         self.assertEqual(response.status_code, 200)
-        self.assertIn('No warning levels', response.content)
+        self.assertContains(response, 'No warning levels')
 
     def test_new_view(self):
         """new warning level view has no showstoppers"""
@@ -42,7 +41,7 @@ class WarningsAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:users:warnings:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Level', response.content)
+        self.assertContains(response, 'Test Level')
 
     def test_edit_view(self):
         """edit warning level view has no showstoppers"""
@@ -61,7 +60,7 @@ class WarningsAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:warnings:edit',
                     kwargs={'pk': test_level.pk}))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_level.name, response.content)
+        self.assertContains(response, test_level.name)
 
         response = self.client.post(
             reverse('misago:admin:users:warnings:edit',
@@ -78,8 +77,8 @@ class WarningsAdminViewsTests(AdminTestCase):
         response = self.client.get(
             reverse('misago:admin:users:warnings:index'))
         self.assertEqual(response.status_code, 200)
-        self.assertIn(test_level.name, response.content)
-        self.assertTrue('Test Level' not in response.content)
+        self.assertContains(response, test_level.name)
+        self.assertNotContains(response, 'Test Level')
 
     def test_move_up_view(self):
         """move warning level up view has no showstoppers"""
@@ -168,4 +167,4 @@ class WarningsAdminViewsTests(AdminTestCase):
             reverse('misago:admin:users:warnings:index'))
         self.assertEqual(response.status_code, 200)
 
-        self.assertTrue(test_level.name not in response.content)
+        self.assertNotContains(response, test_level.name)

+ 8 - 4
misago/users/tokens.py

@@ -3,6 +3,8 @@ from hashlib import sha256
 from time import time
 
 from django.conf import settings
+from django.utils import six
+from django.utils.encoding import force_bytes
 
 
 """
@@ -18,7 +20,7 @@ def make(user, token_type):
     user_hash = _make_hash(user, token_type)
     creation_day = _days_since_epoch()
 
-    obfuscated = base64.b64encode('%s%s' % (user_hash, creation_day))
+    obfuscated = base64.b64encode(force_bytes('%s%s' % (user_hash, creation_day))).decode()
     obfuscated = obfuscated.rstrip('=')
     checksum = _make_checksum(obfuscated)
 
@@ -32,7 +34,7 @@ def is_valid(user, token_type, token):
     if checksum != _make_checksum(obfuscated):
         return False
 
-    unobfuscated = base64.b64decode(obfuscated + '=' * (-len(obfuscated) % 4))
+    unobfuscated = base64.b64decode(obfuscated + '=' * (-len(obfuscated) % 4)).decode()
     user_hash = unobfuscated[:8]
 
     if user_hash != _make_hash(user, token_type):
@@ -52,7 +54,8 @@ def _make_hash(user, token_type):
         settings.SECRET_KEY,
     )
 
-    return sha256('+'.join([unicode(s) for s in seeds])).hexdigest()[:8]
+    return sha256(force_bytes(
+        '+'.join([six.text_type(s) for s in seeds]))).hexdigest()[:8]
 
 
 def _days_since_epoch():
@@ -60,7 +63,8 @@ def _days_since_epoch():
 
 
 def _make_checksum(obfuscated):
-    return sha256('%s:%s' % (settings.SECRET_KEY, obfuscated)).hexdigest()[:8]
+    return sha256(force_bytes(
+        '%s:%s' % (settings.SECRET_KEY, obfuscated))).hexdigest()[:8]
 
 
 """

+ 1 - 1
misago/users/utils.py

@@ -2,4 +2,4 @@ import hashlib
 
 
 def hash_email(email):
-    return hashlib.md5(email.lower()).hexdigest()
+    return hashlib.md5(email.lower().encode()).hexdigest()

+ 2 - 1
misago/users/validators.py

@@ -7,6 +7,7 @@ import requests
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.validators import validate_email as validate_email_content
+from django.utils.encoding import force_str
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ungettext
 
@@ -131,7 +132,7 @@ def _real_validate_with_sfs(ip, email):
     try:
         r = requests.get(SFS_API_URL % {'email': email, 'ip': ip},
                          timeout=3)
-        api_response = json.loads(r.content)
+        api_response = json.loads(force_str(r.content))
         ip_score = api_response.get('ip', {}).get('confidence', 0)
         email_score = api_response.get('email', {}).get('confidence', 0)
 

+ 1 - 1
misago/users/views/avatarserver.py

@@ -43,7 +43,7 @@ def serve_user_avatar_source(request, pk, secret, hash):
     fallback_avatar = get_blank_avatar_file(min(settings.MISAGO_AVATARS_SIZES))
     User = get_user_model()
 
-    if pk > 0:
+    if int(pk) > 0:
         try:
             user = User.objects.get(pk=pk)
 

+ 3 - 1
misago/users/views/lists.py

@@ -4,6 +4,8 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import render as django_render
 from django.shortcuts import redirect
 
+import six
+
 from misago.core.shortcuts import get_object_or_404, paginate, pagination_dict
 from misago.core.utils import format_plaintext_for_html
 
@@ -22,7 +24,7 @@ def render(request, template, context):
     for page in context['pages']:
         page['reversed_link'] = reverse(page['link'])
         request.frontend_context['USERS_LISTS'].append({
-            'name': unicode(page['name']),
+            'name': six.text_type(page['name']),
             'component': page['component'],
         })
 

+ 2 - 1
misago/users/views/options.py

@@ -2,6 +2,7 @@ from django.contrib.auth import update_session_auth_hash
 from django.core.urlresolvers import reverse
 from django.db import IntegrityError
 from django.shortcuts import render
+from django.utils import six
 from django.utils.translation import ugettext as _
 
 from ..credentialchange import read_new_credential
@@ -14,7 +15,7 @@ def index(request, *args, **kwargs):
     user_options = []
     for section in usercp.get_sections(request):
         user_options.append({
-            'name': unicode(section['name']),
+            'name': six.text_type(section['name']),
             'icon': section['icon'],
             'component': section['component'],
         })

+ 2 - 1
misago/users/views/profile.py

@@ -7,6 +7,7 @@ from django.db.transaction import atomic
 from django.http import Http404, JsonResponse
 from django.shortcuts import render as django_render
 from django.shortcuts import redirect
+from django.utils import six
 from django.utils.translation import ugettext as _
 
 from misago.acl import add_acl
@@ -62,7 +63,7 @@ def render(request, template, context):
 
     for section in context['sections']:
         request.frontend_context['PROFILE_PAGES'].append({
-            'name': unicode(section['name']),
+            'name': six.text_type(section['name']),
             'icon': section['icon'],
             'meta': section.get('metadata'),
             'component': section['component'],

+ 1 - 1
misago/users/warnings.py

@@ -25,7 +25,7 @@ def fetch_user_valid_warnings(user):
 
     # expire levels
     active_warnings = []
-    for length, level in enumerate(levels.values()[1:]):
+    for length, level in enumerate(list(levels.values())[1:]):
         length += 1
         level_warnings = []
         if level.length_in_minutes: