[MERGE] forward port of branch saas-1 up to revid 4899 chs@openerp.com-20130821094758-1ae0d1ml5obufzxv

bzr revid: odo@openerp.com-20130820160346-qcdy4kavgm5cb9t6
bzr revid: chs@openerp.com-20130821103139-zqqbrt8dq6iixrny
This commit is contained in:
Christophe Simonis 2013-08-21 12:31:39 +02:00
commit 208b697239
13 changed files with 127 additions and 45 deletions

View File

@ -31,5 +31,15 @@ Changelog
``openerp.exceptions.RedirectWarning``. ``openerp.exceptions.RedirectWarning``.
- Give a pair of new methods to ``res.config.settings`` and a helper to make - Give a pair of new methods to ``res.config.settings`` and a helper to make
them easier to use: ``get_config_warning()``. them easier to use: ``get_config_warning()``.
- Path to webkit report files (field ``report_file``) must be writen with the - Path to webkit report files (field ``report_file``) must be written the
Unix way (with ``/`` and not ``\``) Unix way (with ``/`` and not ``\``)
`7.0`
-----
- Modules may now include an ``i18n_extra`` directory that will be treated like the
default ``i18n`` directory. This is typically useful for manual translation files
that are not managed by Launchpad's translation system. An example is l10n modules
that depend on ``l10n_multilang``.

View File

@ -195,7 +195,8 @@ class ir_model(osv.osv):
ctx = dict(context, ctx = dict(context,
field_name=vals['name'], field_name=vals['name'],
field_state='manual', field_state='manual',
select=vals.get('select_level', '0')) select=vals.get('select_level', '0'),
update_custom_fields=True)
self.pool[vals['model']]._auto_init(cr, ctx) self.pool[vals['model']]._auto_init(cr, ctx)
openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
return res return res

View File

@ -445,6 +445,13 @@ class ir_translation(osv.osv):
tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context) tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later context['overwrite'] = True # make sure the requested translation will override the base terms later
# i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
base_trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
if base_trans_extra_file:
_logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language # Step 2: then load the main translation file, possibly overriding the terms coming from the base language
trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po') trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file: if trans_file:
@ -452,6 +459,11 @@ class ir_translation(osv.osv):
tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context) tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
elif lang_code != 'en_US': elif lang_code != 'en_US':
_logger.warning('module %s: no translation for language %s', module_name, lang_code) _logger.warning('module %s: no translation for language %s', module_name, lang_code)
trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
if trans_extra_file:
_logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
return True return True

View File

@ -187,7 +187,7 @@ class view(osv.osv):
if self.pool._init: if self.pool._init:
# Module init currently in progress, only consider views from modules whose code was already loaded # Module init currently in progress, only consider views from modules whose code was already loaded
query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE v.inherit_id=%s AND v.model=%s AND md.module in %s WHERE v.inherit_id=%s AND v.model=%s AND (md.module IS NULL or md.module in %s)
ORDER BY priority""" ORDER BY priority"""
query_params = (view_id, model, tuple(self.pool._init_modules)) query_params = (view_id, model, tuple(self.pool._init_modules))
else: else:

View File

@ -30,11 +30,13 @@ from openerp.tools.translate import _
CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
class res_currency(osv.osv): class res_currency(osv.osv):
def _current_rate(self, cr, uid, ids, name, arg, context=None): def _current_rate(self, cr, uid, ids, name, arg, context=None):
return self._get_current_rate(cr, uid, ids, name, arg, context=context) return self._get_current_rate(cr, uid, ids, context=context)
def _get_current_rate(self, cr, uid, ids, name, arg, context=None): def _current_rate_silent(self, cr, uid, ids, name, arg, context=None):
return self._get_current_rate(cr, uid, ids, raise_on_no_rate=False, context=context)
def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
if context is None: if context is None:
context = {} context = {}
res = {} res = {}
@ -52,9 +54,12 @@ class res_currency(osv.osv):
if cr.rowcount: if cr.rowcount:
id, rate = cr.fetchall()[0] id, rate = cr.fetchall()[0]
res[id] = rate res[id] = rate
elif not raise_on_no_rate:
res[id] = 0
else: else:
raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id))) raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id)))
return res return res
_name = "res.currency" _name = "res.currency"
_description = "Currency" _description = "Currency"
_columns = { _columns = {
@ -63,6 +68,10 @@ class res_currency(osv.osv):
'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."), 'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."),
'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6), 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
help='The rate of the currency to the currency of rate 1.'), help='The rate of the currency to the currency of rate 1.'),
# Do not use for computation ! Same as rate field with silent failing
'rate_silent': fields.function(_current_rate_silent, string='Current Rate', digits=(12,6),
help='The rate of the currency to the currency of rate 1 (0 if no rate defined).'),
'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'), 'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
'accuracy': fields.integer('Computational Accuracy'), 'accuracy': fields.integer('Computational Accuracy'),
'rounding': fields.float('Rounding Factor', digits=(12,6)), 'rounding': fields.float('Rounding Factor', digits=(12,6)),

View File

@ -22,7 +22,7 @@
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="rate_ids" invisible="1"/> <field name="rate_ids" invisible="1"/>
<field name="date"/> <field name="date"/>
<field name="rate"/> <field name="rate_silent"/>
<field name="rounding"/> <field name="rounding"/>
<field name="accuracy"/> <field name="accuracy"/>
<field name="position"/> <field name="position"/>
@ -37,7 +37,7 @@
<form string="Currency" version="7.0"> <form string="Currency" version="7.0">
<group col="4"> <group col="4">
<field name="name"/> <field name="name"/>
<field name="rate"/> <field name="rate_silent"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
</group> </group>

View File

@ -208,6 +208,8 @@ class res_partner(osv.osv, format_address):
return result return result
def _display_name_compute(self, cr, uid, ids, name, args, context=None): def _display_name_compute(self, cr, uid, ids, name, args, context=None):
context = dict(context or {})
context.pop('show_address', None)
return dict(self.name_get(cr, uid, ids, context=context)) return dict(self.name_get(cr, uid, ids, context=context))
# indirections to avoid passing a copy of the overridable method when declaring the function field # indirections to avoid passing a copy of the overridable method when declaring the function field

View File

@ -398,7 +398,9 @@ def is_leaf(element, internal=False):
INTERNAL_OPS += ('inselect',) INTERNAL_OPS += ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \ return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \ and len(element) == 3 \
and element[1] in INTERNAL_OPS and element[1] in INTERNAL_OPS \
and ((isinstance(element[0], basestring) and element[0])
or element in (TRUE_LEAF, FALSE_LEAF))
# -------------------------------------------------- # --------------------------------------------------

View File

@ -383,16 +383,18 @@ class browse_record(object):
raise KeyError(error_msg) raise KeyError(error_msg)
# if the field is a classic one or a many2one, we'll fetch all classic and many2one fields # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
if col._prefetch: if col._prefetch and not col.groups:
# gen the list of "local" (ie not inherited) fields which are classic or many2one # gen the list of "local" (ie not inherited) fields which are classic or many2one
fields_to_fetch = filter(lambda x: x[1]._classic_write and x[1]._prefetch, self._table._columns.items()) field_filter = lambda x: x[1]._classic_write and x[1]._prefetch and not x[1].groups
fields_to_fetch = filter(field_filter, self._table._columns.items())
# gen the list of inherited fields # gen the list of inherited fields
inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items()) inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items())
# complete the field list with the inherited fields which are classic or many2one # complete the field list with the inherited fields which are classic or many2one
fields_to_fetch += filter(lambda x: x[1]._classic_write and x[1]._prefetch, inherits) fields_to_fetch += filter(field_filter, inherits)
# otherwise we fetch only that field # otherwise we fetch only that field
else: else:
fields_to_fetch = [(name, col)] fields_to_fetch = [(name, col)]
ids = filter(lambda id: name not in self._data[id], self._data.keys()) ids = filter(lambda id: name not in self._data[id], self._data.keys())
# read the results # read the results
field_names = map(lambda x: x[0], fields_to_fetch) field_names = map(lambda x: x[0], fields_to_fetch)

View File

@ -109,7 +109,7 @@ class _date_format(str, _format):
if self.val: if self.val:
if getattr(self,'name', None): if getattr(self,'name', None):
date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT) date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT)
return date.strftime(str(self.lang_obj.date_format)) return date.strftime(self.lang_obj.date_format.encode('utf-8'))
return self.val return self.val
class _dttime_format(str, _format): class _dttime_format(str, _format):
@ -120,8 +120,8 @@ class _dttime_format(str, _format):
def __str__(self): def __str__(self):
if self.val and getattr(self,'name', None): if self.val and getattr(self,'name', None):
return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\ return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\
.strftime("%s %s"%(str(self.lang_obj.date_format), .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'),
str(self.lang_obj.time_format))) (self.lang_obj.time_format).encode('utf-8')))
return self.val return self.val
@ -313,7 +313,7 @@ class rml_parse(object):
date = datetime_field.context_timestamp(self.cr, self.uid, date = datetime_field.context_timestamp(self.cr, self.uid,
timestamp=date, timestamp=date,
context=self.localcontext) context=self.localcontext)
return date.strftime(date_format) return date.strftime(date_format.encode('utf-8'))
res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
if currency_obj: if currency_obj:

View File

@ -9,6 +9,7 @@ import common
# test group that demo user should not have # test group that demo user should not have
GROUP_TECHNICAL_FEATURES = 'base.group_no_one' GROUP_TECHNICAL_FEATURES = 'base.group_no_one'
class TestACL(common.TransactionCase): class TestACL(common.TransactionCase):
def setUp(self): def setUp(self):
@ -22,25 +23,25 @@ class TestACL(common.TransactionCase):
def test_field_visibility_restriction(self): def test_field_visibility_restriction(self):
"""Check that model-level ``groups`` parameter effectively restricts access to that """Check that model-level ``groups`` parameter effectively restricts access to that
field for users who do not belong to one of the explicitly allowed groups""" field for users who do not belong to one of the explicitly allowed groups"""
# Verify the test environment first # Verify the test environment first
original_fields = self.res_currency.fields_get(self.cr, self.demo_uid, []) original_fields = self.res_currency.fields_get(self.cr, self.demo_uid, [])
form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form') form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form')
view_arch = etree.fromstring(form_view.get('arch')) view_arch = etree.fromstring(form_view.get('arch'))
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group before the test") self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group before the test")
self.assertTrue('rate' in original_fields, "'rate' field must be properly visible before the test") self.assertTrue('accuracy' in original_fields, "'accuracy' field must be properly visible before the test")
self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [], self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [],
"Field 'rate' must be found in view definition before the test") "Field 'accuracy' must be found in view definition before the test")
# Restrict access to the field and check it's gone # Restrict access to the field and check it's gone
self.res_currency._columns['rate'].groups = GROUP_TECHNICAL_FEATURES self.res_currency._columns['accuracy'].groups = GROUP_TECHNICAL_FEATURES
fields = self.res_currency.fields_get(self.cr, self.demo_uid, []) fields = self.res_currency.fields_get(self.cr, self.demo_uid, [])
form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form') form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form')
view_arch = etree.fromstring(form_view.get('arch')) view_arch = etree.fromstring(form_view.get('arch'))
self.assertFalse('rate' in fields, "'rate' field should be gone") self.assertFalse('accuracy' in fields, "'accuracy' field should be gone")
self.assertEquals(view_arch.xpath("//field[@name='rate']"), [], self.assertEquals(view_arch.xpath("//field[@name='accuracy']"), [],
"Field 'rate' must not be found in view definition") "Field 'accuracy' must not be found in view definition")
# Make demo user a member of the restricted group and check that the field is back # Make demo user a member of the restricted group and check that the field is back
self.tech_group.write({'users': [(4, self.demo_uid)]}) self.tech_group.write({'users': [(4, self.demo_uid)]})
@ -50,13 +51,13 @@ class TestACL(common.TransactionCase):
view_arch = etree.fromstring(form_view.get('arch')) view_arch = etree.fromstring(form_view.get('arch'))
#import pprint; pprint.pprint(fields); pprint.pprint(form_view) #import pprint; pprint.pprint(fields); pprint.pprint(form_view)
self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group") self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group")
self.assertTrue('rate' in fields, "'rate' field must be properly visible again") self.assertTrue('accuracy' in fields, "'accuracy' field must be properly visible again")
self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [], self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [],
"Field 'rate' must be found in view definition again") "Field 'accuracy' must be found in view definition again")
#cleanup #cleanup
self.tech_group.write({'users': [(3, self.demo_uid)]}) self.tech_group.write({'users': [(3, self.demo_uid)]})
self.res_currency._columns['rate'].groups = False self.res_currency._columns['accuracy'].groups = False
@mute_logger('openerp.osv.orm') @mute_logger('openerp.osv.orm')
def test_field_crud_restriction(self): def test_field_crud_restriction(self):
@ -65,7 +66,7 @@ class TestACL(common.TransactionCase):
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group") self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group")
self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids'])) self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids']))
self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
# Now restrict access to the field and check it's forbidden # Now restrict access to the field and check it's forbidden
self.res_partner._columns['bank_ids'].groups = GROUP_TECHNICAL_FEATURES self.res_partner._columns['bank_ids'].groups = GROUP_TECHNICAL_FEATURES
@ -79,12 +80,30 @@ class TestACL(common.TransactionCase):
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES) has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group") self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group")
self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids'])) self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids']))
self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []})) self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
#cleanup #cleanup
self.tech_group.write({'users': [(3, self.demo_uid)]}) self.tech_group.write({'users': [(3, self.demo_uid)]})
self.res_partner._columns['bank_ids'].groups = False self.res_partner._columns['bank_ids'].groups = False
def test_fields_browse_restriction(self):
"""Test access to records having restricted fields"""
self.res_partner._columns['email'].groups = GROUP_TECHNICAL_FEATURES
try:
P = self.res_partner
pid = P.search(self.cr, self.demo_uid, [], limit=1)[0]
part = P.browse(self.cr, self.demo_uid, pid)
# accessing fields must no raise exceptions...
part.name
# ... except they are restricted
with self.assertRaises(openerp.osv.orm.except_orm) as cm:
part.email
self.assertEqual(cm.exception.args[0], 'Access Denied')
finally:
self.res_partner._columns['email'].groups = False
if __name__ == '__main__': if __name__ == '__main__':
unittest2.main() unittest2.main()

View File

@ -2,7 +2,7 @@
############################################################################## ##############################################################################
# #
# OpenERP, Open Source Business Applications # OpenERP, Open Source Business Applications
# Copyright (C) 2012 OpenERP S.A. (<http://openerp.com>). # Copyright (C) 2012-2013 OpenERP S.A. (<http://openerp.com>).
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -44,29 +44,53 @@ tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "if
tags_to_remove = ['html', 'body', 'font'] tags_to_remove = ['html', 'body', 'font']
def html_sanitize(src): def html_sanitize(src, silent=True):
if not src: if not src:
return src return src
src = ustr(src, errors='replace') src = ustr(src, errors='replace')
logger = _logger.getChild('html_sanitize')
# html encode email tags # html encode email tags
part = re.compile(r"(<(([^a<>]|a[^<>\s])[^<>]*)@[^<>]+>)", re.IGNORECASE | re.DOTALL) part = re.compile(r"(<(([^a<>]|a[^<>\s])[^<>]*)@[^<>]+>)", re.IGNORECASE | re.DOTALL)
src = part.sub(lambda m: cgi.escape(m.group(1)), src) src = part.sub(lambda m: cgi.escape(m.group(1)), src)
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail) kwargs = {
'page_structure': True,
'style': False, # do not remove style attributes
'forms': True, # remove form tags
}
if etree.LXML_VERSION >= (2, 3, 1):
# kill_tags attribute has been added in version 2.3.1
kwargs.update({
'kill_tags': tags_to_kill,
'remove_tags': tags_to_remove,
})
else:
kwargs['remove_tags'] = tags_to_kill + tags_to_remove
if etree.LXML_VERSION >= (3, 1, 0):
kwargs.update({
'safe_attrs_only': True,
'safe_attrs': clean.defs.safe_attrs | set(['style']),
})
else:
# lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
kwargs['safe_attrs_only'] = False
try: try:
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, kill_tags=tags_to_kill, remove_tags=tags_to_remove) # some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
cleaner = clean.Cleaner(**kwargs)
cleaned = cleaner.clean_html(src) cleaned = cleaner.clean_html(src)
except TypeError, e: except etree.ParserError:
# lxml.clean version < 2.3.1 does not have a kill_tags attribute if not silent:
# to remove in 2014 raise
cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, remove_tags=tags_to_kill + tags_to_remove) logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
cleaned = cleaner.clean_html(src)
except etree.ParserError, e:
_logger.warning('html_sanitize: ParserError "%s" obtained when sanitizing "%s"' % (e, src))
cleaned = '<p>ParserError when sanitizing</p>' cleaned = '<p>ParserError when sanitizing</p>'
except Exception, e: except Exception:
_logger.warning('html_sanitize: unknown error "%s" obtained when sanitizing "%s"' % (e, src)) if not silent:
raise
logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
cleaned = '<p>Unknown error when sanitizing</p>' cleaned = '<p>Unknown error when sanitizing</p>'
return cleaned return cleaned

View File

@ -421,6 +421,7 @@ def get_iso_codes(lang):
ALL_LANGUAGES = { ALL_LANGUAGES = {
'ab_RU': u'Abkhazian / аҧсуа', 'ab_RU': u'Abkhazian / аҧсуа',
'am_ET': u'Amharic / አምሃርኛ',
'ar_SY': u'Arabic / الْعَرَبيّة', 'ar_SY': u'Arabic / الْعَرَبيّة',
'bg_BG': u'Bulgarian / български език', 'bg_BG': u'Bulgarian / български език',
'bs_BS': u'Bosnian / bosanski jezik', 'bs_BS': u'Bosnian / bosanski jezik',