LanguageCodeDescriptor.__delete__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 2
c 0
b 0
f 0
rs 10
cc 1
1
"""
2
All fields that are attached to the models.
3
4
The core design of django-parler is to attach descriptor fields
5
to the shared model, which then proxies the get/set calls to the translated model.
6
7
The :class:`TranslatedField` objects are automatically added to the shared model,
8
but may be added explicitly as well. This also allows to set the ``any_language`` configuration option.
9
It's also useful for abstract models; add a :class:`TranslatedField` to
10
indicate that the derived model is expected to provide that translatable field.
11
"""
12
from __future__ import unicode_literals
13
14
import django
15
from django.forms.forms import pretty_name
16
17
18
# TODO: inherit RelatedField?
19
class TranslatedField(object):
20
    """
21
    Proxy field attached to a model.
22
23
    The field is automatically added to the shared model.
24
    However, this can be assigned manually to be more explicit, or to pass the ``any_language`` value.
25
    The ``any_language=True`` option causes the attribute to always return a translated value,
26
    even when the current language and fallback are missing.
27
    This can be useful for "title" attributes for example.
28
29
    Example:
30
31
    .. code-block:: python
32
33
        from django.db import models
34
        from parler.models import TranslatableModel, TranslatedFieldsModel
35
36
        class MyModel(TranslatableModel):
37
            title = TranslatedField(any_language=True)  # Add with any-fallback support
38
            slug = TranslatedField()                    # Optional, but explicitly mentioned
39
40
41
        class MyModelTranslation(TranslatedFieldsModel):
42
            # Manual model class:
43
            master = models.ForeignKey(MyModel, related_name='translations', null=True)
44
            title = models.CharField("Title", max_length=200)
45
            slug = models.SlugField("Slug")
46
    """
47
48
    def __init__(self, any_language=False):
49
        self.model = None
50
        self.name = None
51
        self.any_language = any_language
52
        self._meta = None
53
54
    def contribute_to_class(self, cls, name):
55
        #super(TranslatedField, self).contribute_to_class(cls, name)
56
        self.model = cls
57
        self.name = name
58
59
        # Add the proxy attribute
60
        setattr(cls, self.name, TranslatedFieldDescriptor(self))
61
62
    @property
63
    def meta(self):
64
        if self._meta is None:
65
            self._meta = self.model._parler_meta._get_extension_by_field(self.name)
66
        return self._meta
67
68
69
class TranslatedFieldDescriptor(object):
70
    """
71
    Descriptor for translated attributes.
72
73
    This attribute proxies all get/set calls to the translated model.
74
    """
75
76
    def __init__(self, field):
77
        """
78
        :type field: TranslatedField
79
        """
80
        self.field = field
81
82
    def __get__(self, instance, instance_type=None):
83
        if not instance:
84
            # Return the class attribute when asked for by the admin.
85
            return self
86
87
        # Auto create is useless for __get__, will return empty titles everywhere.
88
        # Better use a fallback instead, just like gettext does.
89
        translation = None
90
        meta = self.field.meta
91
        try:
92
            translation = instance._get_translated_model(use_fallback=True, meta=meta)
93
        except meta.model.DoesNotExist as e:
94
            if self.field.any_language:
95
                translation = instance._get_any_translated_model(meta=meta)  # returns None on error.
96
97
            if translation is None:
98
                # Improve error message
99
                e.args = ("{1}\nAttempted to read attribute {0}.".format(self.field.name, e.args[0]),)
100
                raise
101
102
        return getattr(translation, self.field.name)
103
104
    def __set__(self, instance, value):
105
        if instance is None:
106
            raise AttributeError("{0} must be accessed via instance".format(self.field.opts.object_name))
107
108
        # When assigning the property, assign to the current language.
109
        # No fallback is used in this case.
110
        translation = instance._get_translated_model(use_fallback=False, auto_create=True, meta=self.field.meta)
111
        setattr(translation, self.field.name, value)
112
113
    def __delete__(self, instance):
114
        # No autocreate or fallback, as this is delete.
115
        # Rather blow it all up when the attribute doesn't exist.
116
        # Similar to getting a KeyError on `del dict['UNKNOWN_KEY']`
117
        translation = instance._get_translated_model(meta=self.field.meta)
118
        delattr(translation, self.field.name)
119
120
    def __repr__(self):
121
        return "<{0} for {1}.{2}>".format(self.__class__.__name__, self.field.model.__name__, self.field.name)
122
123
    @property
124
    def short_description(self):
125
        """
126
        Ensure that the admin ``list_display`` renders the correct verbose name for translated fields.
127
128
        The :func:`~django.contrib.admin.utils.label_for_field` function
129
        uses :func:`~django.db.models.Options.get_field_by_name` to find the find and ``verbose_name``.
130
        However, for translated fields, this option does not exist,
131
        hence it falls back to reading the attribute and trying ``short_description``.
132
        Ideally, translated fields should also appear in this list, to be treated like regular fields.
133
        """
134
        translations_model = self.field.meta.model
135
        if translations_model is None:
136
            # This only happens with abstract models. The code is accessing the descriptor at the base model directly,
137
            # not the upgraded descriptor version that contribute_translations() installed.
138
            # Fallback to what the admin label_for_field() would have done otherwise.
139
            return pretty_name(self.field.name)
140
141
        if django.VERSION >= (1, 8):
142
            field = translations_model._meta.get_field(self.field.name)
143
        else:
144
            field = translations_model._meta.get_field_by_name(self.field.name)[0]
145
146
        return field.verbose_name
147
148
149
class LanguageCodeDescriptor(object):
150
    """
151
    This is the property to access the ``language_code`` in the ``TranslatableModel``.
152
    """
153
154
    def __get__(self, instance, instance_type=None):
155
        if not instance:
156
            raise AttributeError("language_code must be accessed via instance")
157
158
        return instance._current_language
159
160
    def __set__(self, instance, value):
161
        raise AttributeError("The 'language_code' attribute cannot be changed directly! Use the set_current_language() method instead.")
162
163
    def __delete__(self, instance):
164
        raise AttributeError("The 'language_code' attribute cannot be deleted!")
165
166
167
try:
168
    from south.modelsinspector import add_ignored_fields
169
except ImportError:
170
    pass
171
else:
172
    _name_re = "^" + __name__.replace(".", "\.")
173
    add_ignored_fields((
174
        _name_re + "\.TranslatedField",
175
    ))
176