[damned-lies] Make the order of the list of languages case insensitive in a user's page



commit c1ff657e0dec62fc58cc6bb5872b7e2188908d69
Author: Grégoire Détrez <gregoire detrez gu se>
Date:   Thu Aug 13 11:33:14 2015 +0200

    Make the order of the list of languages case insensitive in a user's page
    
    This adds a new util function lc_sorted which is a localized version
    of the built-in sorted function (using pyicu).
    
    The new function is used to sort the list of available languages presented
    to the user.
    trans_sort_object_list is also been refactored to use the new function.
    
    This patch adds a new dependency to the mock package, but only for the
    test suite (the mock package is included in the standard library in
    later versions of python but not yet in 2.X).
    
    https://bugzilla.gnome.org/show_bug.cgi?id=753579

 common/tests.py |   67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 common/utils.py |   24 ++++++++++---------
 people/tests.py |   19 +++++++++++++++
 people/views.py |    3 +-
 4 files changed, 101 insertions(+), 12 deletions(-)
---
diff --git a/common/tests.py b/common/tests.py
index 8d9f77e..a255769 100644
--- a/common/tests.py
+++ b/common/tests.py
@@ -19,13 +19,19 @@
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 from datetime import datetime, timedelta
+import operator
+from unittest import skipUnless
 
 from django.core.urlresolvers import reverse
 from django.test import TestCase
+from django.utils import translation
+from mock import MagicMock, patch
 
 from people.models import Person
 from teams.models import Team, Role
 
+from .utils import lc_sorted, pyicu_present, trans_sort_object_list
+
 class CommonTest(TestCase):
 
     def setUp(self):
@@ -92,3 +98,64 @@ class CommonTest(TestCase):
         jt = Person.objects.get(first_name='John', last_name='Translator')
         for role in Role.objects.filter(person=jt):
             self.assertFalse(role.is_active)
+
+
+class LcSortedTest(TestCase):
+
+    @skipUnless(pyicu_present, "PyICU package is required for this test")
+    def test_lc_sorted_en_case_insensitive(self):
+        with translation.override('en'):
+            self.assertEqual(['A','b', 'C', 'z'], lc_sorted(['z', 'C', 'b', "A"]))
+
+    @skipUnless(pyicu_present, "PyICU package is required for this test")
+    def test_lc_sorted_fr_eacute_between_d_and_f(self):
+        with translation.override('fr'):
+            self.assertEqual(['d', 'é', 'f', 'z'], lc_sorted(['é', 'z', 'd', "f"]))
+
+    @skipUnless(pyicu_present, "PyICU package is required for this test")
+    def test_lc_sorted_sv_ouml_after_z(self):
+        with translation.override('sv'):
+            self.assertEqual(['a', 'o', 'z', 'ö'], lc_sorted(['z', 'ö', 'o', "a"]))
+
+    def test_lc_sorted_with_custom_key(self):
+        mykey = operator.itemgetter(1)
+        with translation.override('fr'):
+            self.assertEqual([('z', 'a'), ('a', 'z')],
+                    lc_sorted([('a', 'z'), ('z', 'a')], key=mykey))
+
+
+class TransSortObjectListTest(TestCase):
+
+    def mkItem(self, name):
+        item = MagicMock(name=name)
+        item.name = name
+        return item
+
+    @patch('common.utils._', return_value="bar")
+    def test_translate_object_name(self, gettext):
+        """
+        The given field is translated using gettext and the translation is stored
+        in translated_name.
+        """
+        item = self.mkItem("foo")
+        trans_sort_object_list([item], 'name')
+        self.assertEqual("bar", item.translated_name)
+
+    @patch('common.utils._', side_effect=lambda s: s[::-1])
+    def test_sort_list_according_to_translation(self, _):
+        """
+        The list is sorted according to the computed translated_name
+        (here gettext reverses the string so foo (oof) comes before bar (rab)).
+        """
+        foo, bar = self.mkItem('foo'), self.mkItem('bar')
+        actual = trans_sort_object_list([foo,bar], 'name')
+        self.assertEqual([foo, bar], actual, "wrong order")
+
+    @skipUnless(pyicu_present, "PyICU package is required for this test")
+    @patch('common.utils._', side_effect=lambda x: x)
+    def test_uses_localized_ordering(self, _):
+        """Consider the current locale when ordering elements."""
+        d, eacute, f = self.mkItem('d'), self.mkItem('é'), self.mkItem('f')
+        with translation.override('fr'):
+            actual = trans_sort_object_list([f, eacute, d], 'name')
+            self.assertEqual([d, eacute, f], actual)
diff --git a/common/utils.py b/common/utils.py
index e8b4508..6e65bad 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -23,9 +23,9 @@ from django.conf import settings
 from django.utils.translation import ugettext as _, get_language
 from languages.models import Language
 try:
-    import PyICU
+    import icu
     pyicu_present = True
-except:
+except ImportError:
     pyicu_present = False
 
 MIME_TYPES = {
@@ -33,17 +33,21 @@ MIME_TYPES = {
     'xml':  'text/xml'
 }
 
+def lc_sorted(*args, **kwargs):
+    """
+    Same as the built-in function ``sorted`` but according to the current locale.
+    """
+    if pyicu_present:
+        collator = icu.Collator.createInstance(icu.Locale(str(get_language())))
+        key = kwargs.get('key', lambda x: x)
+        kwargs['key'] = lambda x: collator.getSortKey(key(x))
+    return sorted(*args, **kwargs)
+
 def trans_sort_object_list(lst, tr_field):
     """Sort an object list with translated_name"""
     for l in lst:
         l.translated_name = _(getattr(l, tr_field))
-    templist = [(obj_.translated_name.lower(), obj_) for obj_ in lst]
-    if pyicu_present:
-        collator = PyICU.Collator.createInstance(PyICU.Locale(str(get_language())))
-        templist.sort(key=operator.itemgetter(0), cmp=collator.compare)
-    else:
-        templist.sort()
-    return [obj_ for (key1, obj_) in templist]
+    return lc_sorted(lst, key=lambda o: o.translated_name.lower())
 
 def merge_sorted_by_field(object_list1, object_list2, field):
     """
@@ -112,8 +116,6 @@ def imerge_sorted_by_field(object_list1, object_list2, field):
     >>> [el.num for el in imerge_sorted_by_field(l1, l2, '-num')]
     [6, 5, 4, 4, 4, 2, 1, 1]
     """
-    import operator
-
     if field is not None and field[0] == '-':
         # Reverse the sort order
         field = field[1:]
diff --git a/people/tests.py b/people/tests.py
index 10d8ba8..d331850 100644
--- a/people/tests.py
+++ b/people/tests.py
@@ -20,11 +20,13 @@
 from __future__ import unicode_literals
 
 import datetime
+from unittest import skipUnless
 
 from django.test import TestCase
 from django.core.urlresolvers import reverse
 from django.utils.safestring import SafeData
 
+from common.utils import pyicu_present
 from people.models import Person, obfuscate_email
 from people import forms
 
@@ -116,3 +118,20 @@ class PeopleTestCase(TestCase):
             'Me&nbsp;&lt;me&nbsp;dot&nbsp;company&nbsp;at&nbsp;domain&nbsp;dot&nbsp;org&gt;\n'
             'You&nbsp;&lt;some-address&nbsp;at&nbsp;example&nbsp;dot&nbsp;com&gt;'
         )
+
+    @skipUnless(pyicu_present, "PyICU package is required for this test")
+    def test_all_languages_list_order(self):
+        """
+        The order of the languages should be case insensitive.
+        This tests that "français" appears before "Frisian".
+        """
+        Person.objects.create(username='jn', password='password')
+        self.client.login(username='jn', password='password')
+        url = reverse('person_detail_username', kwargs={'slug': 'jn'})
+        response = self.client.get(url)
+        all_languages = response.context['all_languages']
+        self.assertGreater(
+            all_languages.index(('fy-nl', 'Frisian')),
+            all_languages.index(('fr', "français")),
+            "français is not before Frisian"
+        )
diff --git a/people/views.py b/people/views.py
index ee9be3c..88bf7ae 100644
--- a/people/views.py
+++ b/people/views.py
@@ -32,6 +32,7 @@ from django.shortcuts import render, get_object_or_404
 from django.utils.translation import ugettext_lazy, ugettext as _
 from django.views.generic import ListView, DetailView, UpdateView
 
+from common.utils import lc_sorted
 from people.models import Person
 from teams.models import Team, Role
 from people.forms import TeamJoinForm, DetailForm
@@ -55,7 +56,7 @@ class PersonDetailView(DetailView):
         context = super(PersonDetailView, self).get_context_data(**kwargs)
         states = State.objects.filter(action__person=self.object).distinct()
         all_languages = [(lg[0], LANG_INFO.get(lg[0], {'name_local': lg[1]})['name_local']) for lg in 
settings.LANGUAGES]
-        all_languages.sort(key=itemgetter(1))
+        all_languages = lc_sorted(all_languages, key=itemgetter(1))
         context.update({
             'pageSection': "teams",
             'all_languages': all_languages,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]