diff --git a/django/contrib/localflavor/ru/__init__.py b/django/contrib/localflavor/ru/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/django/contrib/localflavor/ru/forms.py b/django/contrib/localflavor/ru/forms.py
new file mode 100644
index 00000000000..fa3c245c276
--- /dev/null
+++ b/django/contrib/localflavor/ru/forms.py
@@ -0,0 +1,68 @@
+"""
+Russian-specific forms helpers
+"""
+import re
+
+from django.core.validators import EMPTY_VALUES
+from django.forms import ValidationError
+from django.forms.fields import CharField, Select, RegexField
+from django.utils.translation import ugettext_lazy as _
+
+phone_digits_re = re.compile(r'^(?:[78]-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$')
+
+
+class RUCountySelect(Select):
+ """
+ A Select widget that uses a list of Russian Counties as its choices.
+ """
+ def __init__(self, attrs=None):
+ from ru_regions import RU_COUNTY_CHOICES
+ super(RUCountySelect, self).__init__(attrs, choices=RU_COUNTY_CHOICES)
+
+
+class RURegionSelect(Select):
+ """
+ A Select widget that uses a list of Russian Regions as its choices.
+ """
+ def __init__(self, attrs=None):
+ from ru_regions import RU_REGIONS_CHOICES
+ super(RURegionSelect, self).__init__(attrs, choices=RU_REGIONS_CHOICES)
+
+
+class RUPostalCodeField(RegexField):
+ """
+ Russian Postal code field.
+ Format: XXXXXX, where X is any digit, and first digit is not zero.
+ """
+ default_error_messages = {
+ 'invalid': _(u'Enter a postal code in the format XXXXXX.'),
+ }
+ def __init__(self, *args, **kwargs):
+ super(RUPostalCodeField, self).__init__(r'^\d{6}$',
+ max_length=None, min_length=None, *args, **kwargs)
+
+
+class RUPassportNumberField(RegexField):
+ """
+ Russian internal passport number format:
+ XXXX XXXXXX where X - any digit.
+ """
+ default_error_messages = {
+ 'invalid': _(u'Enter a passport number in the format XXXX XXXXXX.'),
+ }
+ def __init__(self, *args, **kwargs):
+ super(RUPassportNumberField, self).__init__(r'^\d{4} \d{6}$',
+ max_length=None, min_length=None, *args, **kwargs)
+
+
+class RUAlienPassportNumberField(RegexField):
+ """
+ Russian alien's passport number format:
+ XX XXXXXXX where X - any digit.
+ """
+ default_error_messages = {
+ 'invalid': _(u'Enter a passport number in the format XX XXXXXXX.'),
+ }
+ def __init__(self, *args, **kwargs):
+ super(RUAlienPassportNumberField, self).__init__(r'^\d{2} \d{7}$',
+ max_length=None, min_length=None, *args, **kwargs)
diff --git a/django/contrib/localflavor/ru/ru_regions.py b/django/contrib/localflavor/ru/ru_regions.py
new file mode 100644
index 00000000000..d07803914f6
--- /dev/null
+++ b/django/contrib/localflavor/ru/ru_regions.py
@@ -0,0 +1,104 @@
+# -*- encoding: utf-8 -*-
+"""
+Sources:
+ http://ru.wikipedia.org/wiki/Коды_субъектов_Российской_Федерации
+ http://ru.wikipedia.org/wiki/Федеральные_округа_Российской_Федерации
+"""
+from django.utils.translation import ugettext_lazy as _
+
+RU_COUNTY_CHOICES = (
+ ("Central Federal County", _("Central Federal County")),
+ ("South Federal County", _("South Federal County")),
+ ("North-West Federal County", _("North-West Federal County")),
+ ("Far-East Federal County", _("Far-East Federal County")),
+ ("Siberian Federal County", _("Siberian Federal County")),
+ ("Ural Federal County", _("Ural Federal County")),
+ ("Privolzhsky Federal County", _("Privolzhsky Federal County")),
+ ("North-Caucasian Federal County", _("North-Caucasian Federal County"))
+)
+
+RU_REGIONS_CHOICES = (
+ ("77", _("Moskva")),
+ ("78", _("Saint-Peterburg")),
+ ("50", _("Moskovskaya oblast'")),
+ ("01", _("Adygeya, Respublika")),
+ ("02", _("Bashkortostan, Respublika")),
+ ("03", _("Buryatia, Respublika")),
+ ("04", _("Altay, Respublika")),
+ ("05", _("Dagestan, Respublika")),
+ ("06", _("Ingushskaya Respublika")),
+ ("07", _("Kabardino-Balkarskaya Respublika")),
+ ("08", _("Kalmykia, Respublika")),
+ ("09", _("Karachaevo-Cherkesskaya Respublika")),
+ ("10", _("Karelia, Respublika")),
+ ("11", _("Komi, Respublika")),
+ ("12", _("Mariy Ehl, Respublika")),
+ ("13", _("Mordovia, Respublika")),
+ ("14", _("Sakha, Respublika (Yakutiya)")),
+ ("15", _("Severnaya Osetia, Respublika (Alania)")),
+ ("16", _("Tatarstan, Respublika")),
+ ("17", _("Tyva, Respublika (Tuva)")),
+ ("18", _("Udmurtskaya Respublika")),
+ ("19", _("Khakassiya, Respublika")),
+ ("95", _("Chechenskaya Respublika")),
+ ("21", _("Chuvashskaya Respublika")),
+ ("22", _("Altayskiy Kray")),
+ ("80", _("Zabaykalskiy Kray")),
+ ("82", _("Kamchatskiy Kray")),
+ ("23", _("Krasnodarskiy Kray")),
+ ("24", _("Krasnoyarskiy Kray")),
+ ("81", _("Permskiy Kray")),
+ ("25", _("Primorskiy Kray")),
+ ("26", _("Stavropol'siyy Kray")),
+ ("27", _("Khabarovskiy Kray")),
+ ("28", _("Amurskaya oblast'")),
+ ("29", _("Arkhangel'skaya oblast'")),
+ ("30", _("Astrakhanskaya oblast'")),
+ ("31", _("Belgorodskaya oblast'")),
+ ("32", _("Bryanskaya oblast'")),
+ ("33", _("Vladimirskaya oblast'")),
+ ("34", _("Volgogradskaya oblast'")),
+ ("35", _("Vologodskaya oblast'")),
+ ("36", _("Voronezhskaya oblast'")),
+ ("37", _("Ivanovskaya oblast'")),
+ ("38", _("Irkutskaya oblast'")),
+ ("39", _("Kaliningradskaya oblast'")),
+ ("40", _("Kaluzhskaya oblast'")),
+ ("42", _("Kemerovskaya oblast'")),
+ ("43", _("Kirovskaya oblast'")),
+ ("44", _("Kostromskaya oblast'")),
+ ("45", _("Kurganskaya oblast'")),
+ ("46", _("Kurskaya oblast'")),
+ ("47", _("Leningradskaya oblast'")),
+ ("48", _("Lipeckaya oblast'")),
+ ("49", _("Magadanskaya oblast'")),
+ ("51", _("Murmanskaya oblast'")),
+ ("52", _("Nizhegorodskaja oblast'")),
+ ("53", _("Novgorodskaya oblast'")),
+ ("54", _("Novosibirskaya oblast'")),
+ ("55", _("Omskaya oblast'")),
+ ("56", _("Orenburgskaya oblast'")),
+ ("57", _("Orlovskaya oblast'")),
+ ("58", _("Penzenskaya oblast'")),
+ ("60", _("Pskovskaya oblast'")),
+ ("61", _("Rostovskaya oblast'")),
+ ("62", _("Rjazanskaya oblast'")),
+ ("63", _("Samarskaya oblast'")),
+ ("64", _("Saratovskaya oblast'")),
+ ("65", _("Sakhalinskaya oblast'")),
+ ("66", _("Sverdlovskaya oblast'")),
+ ("67", _("Smolenskaya oblast'")),
+ ("68", _("Tambovskaya oblast'")),
+ ("69", _("Tverskaya oblast'")),
+ ("70", _("Tomskaya oblast'")),
+ ("71", _("Tul'skaya oblast'")),
+ ("72", _("Tyumenskaya oblast'")),
+ ("73", _("Ul'ianovskaya oblast'")),
+ ("74", _("Chelyabinskaya oblast'")),
+ ("76", _("Yaroslavskaya oblast'")),
+ ("79", _("Evreyskaya avtonomnaja oblast'")),
+ ("83", _("Neneckiy autonomnyy okrug")),
+ ("86", _("Khanty-Mansiyskiy avtonomnyy okrug - Yugra")),
+ ("87", _("Chukotskiy avtonomnyy okrug")),
+ ("89", _("Yamalo-Neneckiy avtonomnyy okrug"))
+)
diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt
index 9214458c3b4..a7c0b984cfb 100644
--- a/docs/ref/contrib/localflavor.txt
+++ b/docs/ref/contrib/localflavor.txt
@@ -63,6 +63,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are:
* Poland_
* Portugal_
* Romania_
+ * Russia_
* Slovakia_
* `South Africa`_
* Spain_
@@ -113,6 +114,7 @@ Here's an example of how to use them::
.. _Poland: `Poland (pl)`_
.. _Portugal: `Portugal (pt)`_
.. _Romania: `Romania (ro)`_
+.. _Russia: `Russia (ru)`_
.. _Slovakia: `Slovakia (sk)`_
.. _South Africa: `South Africa (za)`_
.. _Spain: `Spain (es)`_
@@ -190,6 +192,8 @@ Argentina (``ar``)
Australia (``au``)
=============================================
+.. versionadded:: 1.4
+
.. class:: au.forms.AUPostCodeField
A form field that validates input as an Australian postcode.
@@ -342,6 +346,8 @@ Chile (``cl``)
China (``cn``)
==============
+.. versionadded:: 1.4
+
.. class:: cn.forms.CNProvinceSelect
A ``Select`` widget that uses a list of Chinese regions as its choices.
@@ -756,6 +762,34 @@ Romania (``ro``)
A form field that validates Romanian postal codes.
+Russia (``ru``)
+===============
+
+.. versionadded:: 1.4
+
+.. class:: ru.forms.RUPostalCodeField
+
+ Russian Postal code field. The valid format is XXXXXX, where X is any
+ digit and the first digit is not zero.
+
+.. class:: ru.forms.RUCountySelect
+
+ A ``Select`` widget that uses a list of Russian Counties as its choices.
+
+.. class:: ru.forms.RURegionSelect
+
+ A ``Select`` widget that uses a list of Russian Regions as its choices.
+
+.. class:: ru.forms.RUPassportNumberField
+
+ Russian internal passport number. The valid format is XXXX XXXXXX, where X
+ is any digit.
+
+.. class:: ru.forms.RUAlienPassportNumberField
+
+ Russian alien's passport number. The valid format is XX XXXXXXX, where X
+ is any digit.
+
Slovakia (``sk``)
=================
diff --git a/tests/regressiontests/forms/localflavor/ru.py b/tests/regressiontests/forms/localflavor/ru.py
new file mode 100644
index 00000000000..c8feba6075d
--- /dev/null
+++ b/tests/regressiontests/forms/localflavor/ru.py
@@ -0,0 +1,148 @@
+from django.contrib.localflavor.ru.forms import *
+
+from utils import LocalFlavorTestCase
+
+
+class RULocalFlavorTests(LocalFlavorTestCase):
+
+ def test_RUPassportNumberField(self):
+ error = [u'Enter a passport number in the format XXXX XXXXXX.']
+ valid = {
+ '1981 211204': '1981 211204',
+ '0305 967876': '0305 967876',
+ }
+ invalid = {
+ '1981 2112044': error,
+ '1981 23220': error,
+ '9981211201': error,
+ }
+ self.assertFieldOutput(RUPassportNumberField, valid, invalid)
+
+ def test_RUAlienPassportNumberField(self):
+ error = [u'Enter a passport number in the format XX XXXXXXX.']
+ valid = {
+ '19 8111204': '19 8111204',
+ '03 0567876': '03 0567876',
+ }
+ invalid = {
+ '198 1112044': error,
+ '19 81123220': error,
+ '99 812112': error,
+ }
+ self.assertFieldOutput(RUAlienPassportNumberField, valid, invalid)
+
+ def test_RUPostalCodeField(self):
+ error = [u'Enter a postal code in the format XXXXXX.']
+ valid = {
+ '987654': '987654',
+ '123456': '123456'
+ }
+ invalid = {
+ '123 34': error,
+ '1234567': error,
+ '12345': error
+ }
+ self.assertFieldOutput(RUPostalCodeField, valid, invalid)
+
+ def test_RUCountySelect(self):
+ f = RUCountySelect()
+ out = u''''''
+ self.assertEqual(f.render('county', None), out)
+
+ def test_RURegionSelect(self):
+ f = RURegionSelect()
+ out = u''''''
+ self.assertEqual(f.render('region', '67'), out)
diff --git a/tests/regressiontests/forms/localflavortests.py b/tests/regressiontests/forms/localflavortests.py
index 94abed1ec05..7cf838193e0 100644
--- a/tests/regressiontests/forms/localflavortests.py
+++ b/tests/regressiontests/forms/localflavortests.py
@@ -24,6 +24,7 @@ from localflavor.nl import NLLocalFlavorTests
from localflavor.pl import PLLocalFlavorTests
from localflavor.pt import PTLocalFlavorTests
from localflavor.ro import ROLocalFlavorTests
+from localflavor.ru import RULocalFlavorTests
from localflavor.se import SELocalFlavorTests
from localflavor.sk import SKLocalFlavorTests
from localflavor.tr import TRLocalFlavorTests
diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
index 8d47a411683..5567da49b4c 100644
--- a/tests/regressiontests/forms/tests/__init__.py
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -38,6 +38,7 @@ from regressiontests.forms.localflavortests import (
PLLocalFlavorTests,
PTLocalFlavorTests,
ROLocalFlavorTests,
+ RULocalFlavorTests,
SELocalFlavorTests,
SKLocalFlavorTests,
TRLocalFlavorTests,