Skip to content

Commit

Permalink
Merge pull request #24 from raagin/master
Browse files Browse the repository at this point in the history
Django 1.8 Fix: Django 1.8 removed _fill_fields_cache
  • Loading branch information
specialunderwear authored May 16, 2017
2 parents 9cca24e + b7378db commit 92f674b
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 149 deletions.
73 changes: 1 addition & 72 deletions easymode/i18n/__init__.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,3 @@
"""
Internationalization and localization support for django models.
"""

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

if not hasattr(settings, 'PROJECT_DIR'):
raise ImproperlyConfigured(
"""
PLEASE DEFINE PROJECT_DIR AS:
PROJECT_DIR = os.path.dirname(__file__)
in your settings.py.
Otherwise you can not use easymode.i18n.
""")

from weakref import WeakKeyDictionary

from django.db.models.signals import post_save

import easymode.i18n.gettext

__all__ = ('register', 'unregister', 'admin', 'decorators', 'gettext', 'meta')

_post_save_handlers = WeakKeyDictionary()

def register(cls, location=None):
"""
Register a model for translation
like so:
>>> from easymode import i18n
>>> i18n.register(SomeModel)
This will add entries to the gettext catalog located in the
``settings.LOCALE_DIR`` or directory.
You can also store the catalogs somewhere else. The next example stores the
catalog in the same dir as the file in which i18n.register is called:
>>> i18n.register(SomeModel, __file__)
You can also explictly give a path:
>>> i18n.register(SomeModel, '/tmp/)
Please note that the entire locale is stored in that path, so if the path
is /tmp/ the locale is: /tmp/locale/ this directory will be created if it
does not exist.
:param cls: The model class that should be translated by means of gettext.
:param location: The location of the po file. This can be either a directory\
or a file name. If left unspecified, ``settings.LOCALE_DIR`` or\
``settings.PROJECT_DIR`` will be used.
"""

create_po_for_model = easymode.i18n.gettext.MakeModelMessages(location, cls)

_post_save_handlers[cls] = create_po_for_model

post_save.connect(create_po_for_model, cls, False)

def unregister(cls):
"""
unregisters a previously registered model.
This means the po file will not be updated when the model is saved.
:param cls: The model class that should nolonger be translated by means of gettext.
"""
handler = _post_save_handlers.get(cls, None)
if handler:
post_save.disconnect(handler, cls)
"""
105 changes: 53 additions & 52 deletions easymode/i18n/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,29 @@
from django.core.management.base import CommandError
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import templatize, to_locale, get_language
from django.utils import tzinfo
try:
from django.utils.timezone import tzinfo
except ImportError:
from django.utils import tzinfo

from django.template.loader import render_to_string

import easymode.tree.introspection
from easymode.utils import mutex
from easymode.utils.languagecode import get_language_codes
from easymode.utils import polibext

if 'rosetta' in settings.INSTALLED_APPS:
from rosetta import polib
else:
try:
import polib
except ImportError:
raise ImproperlyConfigured(
"""
Please install either django-rosetta:
http://code.google.com/p/django-rosetta/
or polib:
http://bitbucket.org/izi/polib/src/
otherwise easymode.i18n.gettext won't work"""
)
try:
import polib
except ImportError:
raise ImproperlyConfigured(
"""
Please install either django-rosetta:
http://code.google.com/p/django-rosetta/
or polib:
http://bitbucket.org/izi/polib/src/
otherwise easymode.i18n.gettext won't work"""
)

__all__ = ('MakeModelMessages', 'XGETTEXT_REENCODES_UTF8')

Expand All @@ -53,14 +54,14 @@
xversion = (int(match.group('major')), int(match.group('minor')))
if xversion < (0, 15):
XGETTEXT_REENCODES_UTF8 = True


class MakeModelMessages(object):
"""
Class that can be set as a post_save signal handler.
If done so, it will create and update a django.po file
for each language in settings. But only if the model is
create newly, not when it is updated manually.
create newly, not when it is updated manually.
"""

def __init__(self, location=None, sender=None):
Expand All @@ -72,10 +73,10 @@ def __init__(self, location=None, sender=None):
self.location = os.path.dirname(location)
else:
self.location = location

self.sender = sender


def __call__(self, instance, sender=None, **kwargs):
"""
Handler for the post_save signal.
Expand All @@ -86,38 +87,38 @@ def __call__(self, instance, sender=None, **kwargs):
# no need to create translation files if not in the master site
if not getattr(settings, 'MASTER_SITE', False):
return

if hasattr(instance, 'language') and instance.language != settings.LANGUAGE_CODE:
return

if get_language() != settings.LANGUAGE_CODE:
return

po_form = self.poify(instance)
if po_form is not None:
# for each language create a po file
for language in get_language_codes():

locale_dir = os.path.join( self.location , 'locale',
to_locale(language), 'LC_MESSAGES')
locale_file = os.path.join( locale_dir, 'django.po')

# create locale dir if not available
if not os.path.isdir(locale_dir):
os.makedirs(locale_dir)

# acquire exclusive lock this should only be run
# by one process at a time
# by one process at a time
with mutex(max_wait=30):
# write proper header if the file is new
# this header contains the character set.
write_header = not os.path.exists(locale_file)

if write_header: # write header and po stuffz0r
with codecs.open(locale_file, 'w', 'utf-8') as fp:
fp.write(polibext.po_to_unicode(po_form))
else:

else:
#merge existing translation with the new one
msg = self.msgmerge(locale_file, polibext.po_to_unicode(po_form).encode('utf-8'))
if msg:
Expand All @@ -127,28 +128,28 @@ def __call__(self, instance, sender=None, **kwargs):
for entry in po_form: # this avoids printing the header
fp.write(u"\n")
fp.write(polibext.po_to_unicode(entry))
# filter away duplicates

# filter away duplicates
msg = self.msguniq(locale_file)
# save entire po file once more and overwrite with filtered shizzle
with open(locale_file, 'w') as fp:
fp.write(msg)
fp.write(msg)



def poify(self, model):
"""turn a django model into a po file."""
if not hasattr(model, 'localized_fields'):
return None

# create po stream with header
po_stream = polibext.PoStream(StringIO.StringIO(self.po_header)).parse()

for (name, field) in easymode.tree.introspection.get_default_field_descriptors(model):
occurrence = u"%s.%s.%s" % (model._meta.app_label, model.__class__.__name__, name)
value = field.value_to_string(model)

# only add empty strings
if value != "":
if value != "":
entry = polib.POEntry(msgid=value, occurrences=[(occurrence, model.pk)])
# make sure no duplicate entries in the po_stream
existing_entry = po_stream.find(entry.msgid)
Expand All @@ -157,31 +158,31 @@ def poify(self, model):
else:
# no really, existing_entry.merge does not merge the occurrences.
existing_entry.occurrences += entry.occurrences

return po_stream


def xgettext(self, template):
"""Extracts to be translated strings from template and turns it into po format."""

cmd = 'xgettext -d django -L Python --keyword=gettext_noop \
--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --from-code=UTF-8 \
--output=- -'

p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(msg, err) = p.communicate(input=templatize(template))

if err:
# dont raise exception, some stuff in stderr are just warmings
logging.warning(err)

if XGETTEXT_REENCODES_UTF8:
return msg.decode('utf-8').encode('iso-8859-1')

return msg


def po_stream(self, po_string):
"""make a po stream object from a po_string"""
return polibext.PoStream(StringIO.StringIO(po_string)).parse()
Expand All @@ -191,30 +192,30 @@ def msgmerge(self, locale_file, po_string):
"""
Runs msgmerge on a locale_file and po_string
"""

cmd = "msgmerge -q %s -" % locale_file
p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(msg, err) = p.communicate(input=po_string)

if err:
# dont raise exception, some stuff in stderr are just warmings
logging.warning("%s \nfile: %s\npostring: %s" % (err, locale_file, po_string))

return msg


def msguniq(self, locale_file):
"""
run msgunique on the locale_file
"""

# group related language strings together.
# except if no real entries where written or the header will be removed.
p = subprocess.Popen('msguniq --to-code=utf-8 %s' % (locale_file,),
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
(msg, err) = p.communicate()

if err:
# raise exception, none of the stuff in stderr are just warmings
logging.error(err)
Expand All @@ -228,13 +229,13 @@ def msguniq(self, locale_file):
)

return msg


@property
def po_header(self):
localtime = tzinfo.LocalTimezone( datetime.now() )
now = datetime.now(localtime).isoformat()
po_header = render_to_string('i18n/po_header.txt', {
po_header = render_to_string('i18n/po_header.txt', {
'creation_date': now,
'revision_date': now,
'last_translator' : u'anonymous',
Expand Down
7 changes: 6 additions & 1 deletion easymode/i18n/meta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ def localize_fields(cls, localized_fields):
cls.add_to_class(field, DefaultFieldDescriptor(field, **kwargs))

# update fields cache
cls._meta._fill_fields_cache()
try:
cls._meta._fill_fields_cache()
except AttributeError:
# Django 1.8 removed _fill_fields_cache
cls._meta._expire_cache()
cls._meta._get_fields(reverse=False)

# return the finished product
return cls
3 changes: 2 additions & 1 deletion easymode/i18n/meta/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def __get__(self, obj, typ=None):

# first check if the database contains the localized data
vo.stored_value = getattr(obj, real_field_name)

if getattr(settings, 'I18N_NOFALLBACK', False):
return vo.stored_value
# the database does not have our localized data.
# check if we have a translation, first get the msgid, as a unicode string.
vo.msgid = get_localized_property(obj, self.name, getattr(settings, 'MSGID_LANGUAGE', settings.LANGUAGE_CODE))
Expand Down
Loading

0 comments on commit 92f674b

Please sign in to comment.