class Poll(TranslatableModel): # Add this hack because of broken BooleanField in MySQL when using hvad app PUBLISH_CHOICES = ( ('T', 'True'), ('F', 'False'), ) date = models.DateField(auto_now_add=True) is_published = models.CharField(max_length=10, default='True', verbose_name=_('is published'), choices=PUBLISH_CHOICES) translations = TranslatedFields( title = models.CharField(max_length=250, verbose_name=_('question')), ) objects = TranslationManager() published = PublishedManager() class Meta: ordering = ['-date'] verbose_name = _('poll') verbose_name_plural = _('polls') def __unicode__(self): return self.title def get_vote_count(self): return Vote.objects.filter(poll=self).count() vote_count = property(fget=get_vote_count) def get_cookie_name(self): return str('poll_%s' % (self.pk))
class Newsletter(TranslatableModel): name = models.CharField(max_length=256, verbose_name=_('Name (internal)')) translations = TranslatedFields( title=models.CharField(max_length=256, verbose_name=_('Title (translated)')), description=models.TextField(blank=True, null=True, verbose_name=_('Description')), ) backend = models.CharField(max_length=36, default='mailchimp', choices=BACKEND_CHOICES) backend_id = models.CharField(max_length=64, blank=True, null=True) backend_api_key = models.CharField(max_length=64, blank=True, null=True) objects = TranslationManager() class Meta: app_label = 'subscription' verbose_name = _('Newsletter') #ordering = ('-publish',) def __unicode__(self): return u'%s' % self.lazy_translation_getter('name', str(self.pk)) def get_backend(self): backend = None if self.backend == 'mailchimp': if self.backend_api_key: backend = MailchimpBackend(api_key=self.backend_api_key) else: backend = MailchimpBackend() return backend def subscribe(self, email, name, language=None, channel=None): log.info(u'subscribe %s - %s [%s] %s on %s' % (email, name, language, channel, self.name)) backend = self.get_backend() backend.subscribe(list_id=self.backend_id, email=email, name=name, language=language, channel=channel) def unsubscribe(self, email): log.info(u'unsubscribe %s from %s' % (email, self.name)) backend = self.get_backend() backend.unsubscribe(list_id=self.backend_id, email=email) def save(self, *args, **kwargs): super(Newsletter, self).save(*args, **kwargs)
class EventCategory(TranslatableModel): on_site = CurrentSiteManager(field_name='sites') translations = TranslatedFields( name=models.CharField(_(u"Name"), max_length=100)) slug = models.SlugField( _('Slug'), help_text= _('Name in lowercase with no spaces which will be chown in the URL of the navigator' )) sites = models.ManyToManyField(Site) created = models.DateTimeField(auto_now_add=True, editable=False) updated = models.DateTimeField(auto_now=True, editable=False) objects = TranslationManager() def site_list(self): return self.sites.all() def __unicode__(self): return self.name class Meta: verbose_name_plural = _(u'Termin Kategorien') verbose_name = _(u'Termin Kategorie')
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} for key, value in kwargs.items(): if key in self._translated_field_names and not key in veto_names: tkwargs[key] = value else: skwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) # Create a translation if there are translated fields if tkwargs: tkwargs['language_code'] = tkwargs.get( 'language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs)) @classmethod def save_translations(cls, instance, **kwargs): 'Signal handler for post_save' translation = get_cached_translation(instance) if translation is not None: translation.master = instance translation.save() def translate(self, language_code): ''' Create a new translation for current instance. Does NOT check if the translation already exists! ''' set_cached_translation( self, self._meta.translations_model(language_code=language_code)) return self def safe_translation_getter(self, name, default=None): cache = get_cached_translation(self) if cache is None: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = getattr(self, self._meta.translations_accessor).all() # if no translation exists, bail out now if len(translations) == 0: return default # organize translations into a nice dict translation_dict = dict((t.language_code, t) for t in translations) # see if we have the right language, or any language in fallbacks for code in (get_language(), settings.LANGUAGE_CODE) + FALLBACK_LANGUAGES: try: translation = translation_dict[code] except KeyError: continue break else: # none of the fallbacks was found, pick an arbitrary translation translation = translation_dict.popitem()[1] set_cached_translation(self, translation) return getattr(translation, name, default) def get_available_languages(self): """ Get a list of all available language_code in db. """ qs = getattr(self, self._meta.translations_accessor).all() if qs._result_cache is not None: return [obj.language_code for obj in qs] return qs.values_list('language_code', flat=True) #=========================================================================== # Validation #=========================================================================== def clean_fields(self, exclude=None): super(TranslatableModel, self).clean_fields(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.clean_fields( exclude=exclude + ['id', 'master', 'master_id', 'language_code']) def validate_unique(self, exclude=None): super(TranslatableModel, self).validate_unique(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.validate_unique(exclude=exclude) #=========================================================================== # Checks - require Django 1.7 or newer #=========================================================================== if django.VERSION >= (1, 7): @classmethod def check(cls, **kwargs): errors = super(TranslatableModel, cls).check(**kwargs) errors.extend(cls._check_shared_translated_clash()) return errors @classmethod def _check_shared_translated_clash(cls): fields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.fields)) tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('id', 'master'))) return [ checks.Error( "translated field '%s' clashes with untranslated field." % field, hint=None, obj=cls, id='hvad.models.E01') for field in tfields.intersection(fields) ] @classmethod def _check_local_fields(cls, fields, option): """ Remove fields we recognize as translated fields from tests """ to_check = [] for field in fields: try: cls._meta.translations_model._meta.get_field(field) except FieldDoesNotExist: to_check.append(field) return super(TranslatableModel, cls)._check_local_fields(to_check, option) @classmethod def _check_ordering(cls): if not cls._meta.ordering: return [] if not isinstance(cls._meta.ordering, (list, tuple)): return [ checks.Error("'ordering' must be a tuple or list.", hint=None, obj=cls, id='models.E014') ] fields = [f for f in cls._meta.ordering if f != '?'] fields = [f[1:] if f.startswith('-') else f for f in fields] fields = set(f for f in fields if f not in ('_order', 'pk') and '__' not in f) valid_fields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.fields)) valid_tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('master', 'language_code'))) return [ checks.Error( "'ordering' refers to the non-existent field '%s' --hvad." % field, hint=None, obj=cls, id='models.E015') for field in fields - valid_fields - valid_tfields ] #=========================================================================== # Internals #=========================================================================== @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: opts = self._meta.translations_model._meta result = set() if django.VERSION >= (1, 8): for field in opts.get_fields(): result.add(field.name) if hasattr(field, 'attname'): result.add(field.attname) else: result = set(opts.get_all_field_names()) for name in tuple(result): try: attname = opts.get_field(name).get_attname() except (FieldDoesNotExist, AttributeError): continue if attname: result.add(attname) self._translated_field_names_cache = tuple(result) return self._translated_field_names_cache
class InvalidModel2(object): objects = TranslationManager()
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} for key, value in kwargs.items(): if key in self._translated_field_names and not key in veto_names: tkwargs[key] = value else: skwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) # Create a translation if there are translated fields if tkwargs: tkwargs['language_code'] = tkwargs.get( 'language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs)) @classmethod def save_translations(cls, instance, **kwargs): 'Signal handler for post_save' translation = get_cached_translation(instance) if translation is not None: translation.master = instance translation.save() def translate(self, language_code): ''' Create a new translation for current instance. Does NOT check if the translation already exists! ''' set_cached_translation( self, self._meta.translations_model(language_code=language_code)) return self def safe_translation_getter(self, name, default=None): cache = get_cached_translation(self) if cache is None: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = getattr(self, self._meta.translations_accessor).all() # if no translation exists, bail out now if len(translations) == 0: return default # organize translations into a nice dict translation_dict = dict((t.language_code, t) for t in translations) # see if we have the right language, or any language in fallbacks for code in (get_language(), settings.LANGUAGE_CODE) + FALLBACK_LANGUAGES: try: translation = translation_dict[code] except KeyError: continue break else: # none of the fallbacks was found, pick an arbitrary translation translation = translation_dict.popitem()[1] set_cached_translation(self, translation) return getattr(translation, name, default) def get_available_languages(self): """ Get a list of all available language_code in db. """ qs = getattr(self, self._meta.translations_accessor).all() if qs._result_cache is not None: return [obj.language_code for obj in qs] return qs.values_list('language_code', flat=True) #=========================================================================== # Internals #=========================================================================== @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: opts = self._meta.translations_model._meta result = set() if django.VERSION >= (1, 8): for field in opts.get_fields(): result.add(field.name) if hasattr(field, 'attname'): result.add(field.attname) else: result = set(opts.get_all_field_names()) for name in tuple(result): try: attname = opts.get_field(name).get_attname() except (FieldDoesNotExist, AttributeError): continue if attname: result.add(attname) self._translated_field_names_cache = tuple(result) return self._translated_field_names_cache
class Event(TranslatableModel): on_site = EventManager(field_name='sites') translations = TranslatedFields(name=models.CharField( _("Terminname"), max_length=250, help_text=_(u'Zum Beispiel: "Drittes Sommerfest"'))) slug = models.SlugField( _('Kurzname'), unique_for_date='start', help_text= _('Name in Kleinbuchstaben ohne Leerzeichen der in der URL verwendet wird.' )) description = PlaceholderField( 'event_description', blank=True, null=True, verbose_name=_('Beschreibung'), ) start = models.DateField( _(u"Datum"), help_text= _(u'Format: jjjj-mm-tt. Wann findet der Termin statt? Wenn der Termin über mehrere Tage stattfindet dann gib ein Startdatum und unten ein Enddatum an.' )) time = models.CharField( _("Zeitpunkt"), blank=True, max_length=100, help_text= _(u'* Optional. zum Beispiel: "8", "7 - 8" oder "7:30" das "Uhr" wird automatisch hinzugefügt' )) # Todo -- continue to validate that the end is after the start? end = models.DateField( _(u'Enddatum'), blank=True, null=True, help_text= _(u'* Optional. Wenn der Termin über mehrere Tage andauert dann gebe hier ein Enddatum an. Wenn freigelassen wird automatisch das Startdatum eingetragen.' )) categories = models.ManyToManyField( EventCategory, blank=True, null=True, limit_choices_to={'sites__id': settings.SITE_ID}) sites = models.ManyToManyField(Site, editable=False, default=[settings.SITE_ID]) created = models.DateTimeField(auto_now_add=True, editable=False) updated = models.DateTimeField(auto_now=True, editable=False) objects = TranslationManager() def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return ('event_detail', None, { 'year': self.start.strftime("%Y"), 'month': self.start.strftime("%m"), 'day': self.start.strftime("%d"), 'slug': self.slug, }) def has_passed(self): if self.end: return self.end < date.today() else: return self.start < date.today() def category_list(self): if self.categories.all(): return ", ".join([c.name for c in self.categories.all()]) return _(u'(No categories)') def site_list(self): return self.sites.all() def is_mutiple_days(self): return self.end is not None and self.end > self.start def get_next_upcoming(self): try: return Event.on_site.upcoming().filter( start__gte=self.start).exclude(id=self.id)[0] except IndexError: return None def get_previous_upcoming(self): try: return Event.on_site.upcoming().filter( start__lte=self.start).exclude(id=self.id)[0] except IndexError: return None def date_span(self): if self.end: s = "%s — %s" % (format(self.start), format(self.end)) else: s = format(self.start) return mark_safe(s) date_span.short_description = 'date' date_span.admin_order_field = 'start' date_span.allow_tags = True class Meta: ordering = ('-start', ) verbose_name = _(u'Termin') verbose_name_plural = _(u'Termine')
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ __metaclass__ = TranslatableModelBase # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): tkwargs = {} # translated fields skwargs = {} # shared fields if 'master' in kwargs.keys(): raise RuntimeError( "Cannot init %s class with a 'master' argument" % \ self.__class__.__name__ ) # filter out all the translated fields (including 'master' and 'language_code') primary_key_names = ('pk', self._meta.pk.name) for key in kwargs.keys(): if key in self._translated_field_names: if not key in primary_key_names: # we exclude the pk of the shared model tkwargs[key] = kwargs.pop(key) if not tkwargs.keys(): # if there where no translated options, then we assume this is a # regular init and don't want to do any funky stuff super(TranslatableModel, self).__init__(*args, **kwargs) return # there was at least one of the translated fields (or a language_code) # in kwargs. We need to do magic. # extract all the shared fields (including the pk) for key in kwargs.keys(): if key in self._shared_field_names: skwargs[key] = kwargs.pop(key) # do the regular init minus the translated fields super(TranslatableModel, self).__init__(*args, **skwargs) # prepopulate the translations model cache with an translation model tkwargs['language_code'] = tkwargs.get('language_code', get_language()) tkwargs['master'] = self translated = self._meta.translations_model(*args, **tkwargs) setattr(self, self._meta.translations_cache, translated) @classmethod def contribute_translations(cls, rel): """ Contribute translations options to the inner Meta class and set the descriptors. This get's called from TranslatableModelBase.__new__ """ opts = cls._meta opts.translations_accessor = rel.get_accessor_name() opts.translations_model = rel.model opts.translations_cache = '%s_cache' % rel.get_accessor_name() trans_opts = opts.translations_model._meta # Set descriptors ignore_fields = [ 'pk', 'master', opts.translations_model._meta.pk.name, ] for field in trans_opts.fields: if field.name in ignore_fields: continue if field.name == 'language_code': attr = LanguageCodeAttribute(opts) else: attr = TranslatedAttribute(opts, field.name) setattr(cls, field.name, attr) @classmethod def save_translations(cls, instance, **kwargs): """ When this instance is saved, also save the (cached) translation """ opts = cls._meta if hasattr(instance, opts.translations_cache): trans = getattr(instance, opts.translations_cache) if not trans.master_id: trans.master = instance trans.save() def translate(self, language_code): """ Returns an Model instance in the specified language. Does NOT check if the translation already exists! Does NOT interact with the database. This will refresh the translations cache attribute on the instance. """ tkwargs = { 'language_code': language_code, 'master': self, } translated = self._meta.translations_model(**tkwargs) setattr(self, self._meta.translations_cache, translated) return self def safe_translation_getter(self, name, default=None): cache = getattr(self, self._meta.translations_cache, None) if not cache: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ cache = getattr(self, self._meta.translations_cache, NoTranslation) trans = self._meta.translations_model.objects.filter( master__pk=self.pk) if not cache and cache != NoTranslation and not trans.exists( ): # check if there is no translations return default elif getattr( cache, name, NoTranslation) == NoTranslation and trans.exists( ): # We have translations, but no specific translation cached trans_in_own_language = trans.filter(language_code=get_language()) if trans_in_own_language.exists(): trans = trans_in_own_language[0] else: trans = trans[0] setattr(self, self._meta.translations_cache, trans) return getattr(trans, name) return getattr(cache, name) def get_available_languages(self): manager = self._meta.translations_model.objects return manager.filter(master=self).values_list('language_code', flat=True) #=========================================================================== # Internals #=========================================================================== @property def _shared_field_names(self): if getattr(self, '_shared_field_names_cache', None) is None: self._shared_field_names_cache = self._meta.get_all_field_names() return self._shared_field_names_cache @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: self._translated_field_names_cache = self._meta.translations_model._meta.get_all_field_names( ) return self._translated_field_names_cache
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ objects = TranslationManager() _plain_manager = models.Manager() class Meta: abstract = True if django.VERSION >= (1, 10): base_manager_name = '_plain_manager' def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} translations_opts = self._meta.translations_model._meta for key, value in kwargs.items(): if key in veto_names: skwargs[key] = value else: try: translations_opts.get_field(key) except FieldDoesNotExist: skwargs[key] = value else: tkwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) if tkwargs: tkwargs['language_code'] = tkwargs.get( 'language_code') or get_language() set_cached_translation(self, self._meta.translations_model(**tkwargs)) def save(self, *args, **skwargs): veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) translations_opts = self._meta.translations_model._meta translation = get_cached_translation(self) tkwargs = skwargs.copy() # split update_fields in shared/translated fields update_fields = skwargs.get('update_fields') if update_fields is not None: supdate, tupdate = [], [] for name in update_fields: if name in veto_names: supdate.append(name) else: try: translations_opts.get_field(name) except FieldDoesNotExist: supdate.append(name) else: tupdate.append(name) skwargs['update_fields'], tkwargs[ 'update_fields'] = supdate, tupdate # save share and translated model in a single transaction if update_fields is None or skwargs['update_fields']: super(TranslatableModel, self).save(*args, **skwargs) if (update_fields is None or tkwargs['update_fields']) and translation is not None: if translation.pk is None and update_fields: del tkwargs['update_fields'] # allow new translations translation.master = self translation.save(*args, **tkwargs) save.alters_data = True def translate(self, language_code): ''' Create a new translation for current instance. Does NOT check if the translation already exists! ''' set_cached_translation( self, self._meta.translations_model(language_code=language_code)) return self translate.alters_data = True def safe_translation_getter(self, name, default=None): cache = get_cached_translation(self) if cache is None: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = getattr(self, self._meta.translations_accessor).all() # if no translation exists, bail out now if len(translations) == 0: return default # organize translations into a nice dict translation_dict = dict((t.language_code, t) for t in translations) # see if we have the right language, or any language in fallbacks for code in (get_language(), ) + hvad_settings.FALLBACK_LANGUAGES: try: translation = translation_dict[code] except KeyError: continue break else: # none of the fallbacks was found, pick an arbitrary translation translation = translation_dict.popitem()[1] set_cached_translation(self, translation) return getattr(translation, name, default) def get_available_languages(self): """ Get a list of all available language_code in db. """ qs = getattr(self, self._meta.translations_accessor).all() if qs._result_cache is not None: return [obj.language_code for obj in qs] return qs.values_list('language_code', flat=True) #=========================================================================== # Validation #=========================================================================== def clean_fields(self, exclude=None): super(TranslatableModel, self).clean_fields(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.clean_fields( exclude=exclude + ['id', 'master', 'master_id', 'language_code']) def validate_unique(self, exclude=None): super(TranslatableModel, self).validate_unique(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.validate_unique(exclude=exclude) #=========================================================================== # Checks #=========================================================================== @classmethod def check(cls, **kwargs): errors = super(TranslatableModel, cls).check(**kwargs) errors.extend(cls._check_shared_translated_clash()) errors.extend(cls._check_default_manager_translation_aware()) return errors @classmethod def _check_shared_translated_clash(cls): fields = set( chain.from_iterable((f.name, f.attname) for f in cls._meta.fields)) tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('id', 'master'))) return [ checks.Error( "translated field '%s' clashes with untranslated field." % field, hint=None, obj=cls, id='hvad.models.E01') for field in tfields.intersection(fields) ] @classmethod def _check_default_manager_translation_aware(cls): errors = [] if not isinstance(cls._default_manager, TranslationManager): errors.append( checks.Error( "The default manager on a TranslatableModel must be a " "TranslationManager instance or an instance of a subclass of " "TranslationManager, the default manager of %r is not." % cls, hint=None, obj=cls, id='hvad.models.E02')) return errors @classmethod def _check_local_fields(cls, fields, option): """ Remove fields we recognize as translated fields from tests """ to_check = [] for field in fields: try: cls._meta.translations_model._meta.get_field(field) except FieldDoesNotExist: to_check.append(field) return super(TranslatableModel, cls)._check_local_fields(to_check, option) @classmethod def _check_ordering(cls): if not cls._meta.ordering: return [] if not isinstance(cls._meta.ordering, (list, tuple)): return [ checks.Error("'ordering' must be a tuple or list.", hint=None, obj=cls, id='models.E014') ] fields = [f for f in cls._meta.ordering if f != '?'] fields = [f[1:] if f.startswith('-') else f for f in fields] fields = set(f for f in fields if f not in ('_order', 'pk') and '__' not in f) valid_fields = set( chain.from_iterable((f.name, f.attname) for f in cls._meta.fields)) valid_tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('master', 'language_code'))) return [ checks.Error( "'ordering' refers to the non-existent field '%s' --hvad." % field, hint=None, obj=cls, id='models.E015') for field in fields - valid_fields - valid_tfields ]
class License(TranslatableModel, MigrationMixin, UUIDModelMixin, TimestampedModelMixin, models.Model): slug = models.SlugField(max_length=100, unique=False) name = models.CharField(max_length=200) key = models.CharField(verbose_name=_("License key"), max_length=36, blank=True, null=True, help_text=_("used e.g. for the icon-names")) restricted = models.NullBooleanField(null=True, blank=True) version = models.CharField(verbose_name=_("License version"), max_length=36, blank=True, null=True, help_text=_("e.g. 2.5 CH")) iconset = models.CharField(verbose_name=_("Iconset"), max_length=36, blank=True, null=True, help_text=_("e.g. cc-by, cc-nc, cc-sa")) link = models.URLField(null=True, blank=True) parent = models.ForeignKey('self', null=True, blank=True, related_name='license_children') is_default = models.NullBooleanField(default=False, null=True, blank=True) selectable = models.NullBooleanField(default=True, null=True, blank=True) is_promotional = models.NullBooleanField(default=False, null=True, blank=True) translations = TranslatedFields( name_translated=models.CharField(max_length=200), excerpt=models.TextField(blank=True, null=True), license_text=models.TextField(blank=True, null=True)) objects = TranslationManager() class Meta: app_label = 'alibrary' verbose_name = _('License') verbose_name_plural = _('Licenses') ordering = ( 'parent__name', 'name', ) def __unicode__(self): if self.parent: return '%s - %s' % (self.parent.name, self.name) else: return '%s' % (self.name) @property def title(self): if self.parent: return '{} - {}'.format(self.parent.name, self.name) return self.name def get_absolute_url(self): return reverse('alibrary-license-detail', args=(self.slug, )) def get_admin_url(self): return reverse("admin:alibrary_license_change", args=(self.pk, )) @property def iconset_display(self): from django.utils.html import mark_safe html = '' if self.iconset: icons = self.iconset.split(',') for icon in icons: html += '<i class="icon icon-license-%s"></i>' % icon.strip( ' ') return mark_safe(html)
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ objects = TranslationManager() _plain_manager = models.Manager() class Meta: abstract = True base_manager_name = '_plain_manager' def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} translations_opts = self._meta.translations_model._meta for key, value in kwargs.items(): if key in veto_names: skwargs[key] = value else: try: translations_opts.get_field(key) except FieldDoesNotExist: skwargs[key] = value else: tkwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) language_code = tkwargs.get('language_code') or get_language() if language_code is not NoTranslation: tkwargs['language_code'] = language_code set_cached_translation(self, self._meta.translations_model(**tkwargs)) @classmethod def from_db(cls, db, field_names, values): if len(values) != len(cls._meta.concrete_fields): # Missing values are deferred and must be marked as such values = list(values) values.reverse() values = [ values.pop() if f.attname in field_names else models.DEFERRED for f in cls._meta.concrete_fields ] new = cls(*values, language_code=NoTranslation) new._state.adding = False new._state.db = db return new def save(self, *args, **skwargs): veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) translations_opts = self._meta.translations_model._meta translation = get_cached_translation(self) tkwargs = skwargs.copy() # split update_fields in shared/translated fields update_fields = skwargs.get('update_fields') if update_fields is not None: supdate, tupdate = [], [] for name in update_fields: if name in veto_names: supdate.append(name) else: try: translations_opts.get_field(name) except FieldDoesNotExist: supdate.append(name) else: tupdate.append(name) skwargs['update_fields'], tkwargs[ 'update_fields'] = supdate, tupdate # save share and translated model in a single transaction db = router.db_for_write(self.__class__, instance=self) with transaction.atomic(using=db, savepoint=False): if update_fields is None or skwargs['update_fields']: super(TranslatableModel, self).save(*args, **skwargs) if (update_fields is None or tkwargs['update_fields']) and translation is not None: if translation.pk is None and update_fields: del tkwargs['update_fields'] # allow new translations translation.master = self translation.save(*args, **tkwargs) save.alters_data = True def translate(self, language_code): """ Create a new translation for current instance. Does NOT check if the translation already exists. """ set_cached_translation( self, self._meta.translations_model(language_code=language_code)) translate.alters_data = True #=========================================================================== # Validation #=========================================================================== def clean_fields(self, exclude=None): super(TranslatableModel, self).clean_fields(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.clean_fields( exclude=exclude + ['id', 'master', 'master_id', 'language_code']) def validate_unique(self, exclude=None): super(TranslatableModel, self).validate_unique(exclude=exclude) translation = get_cached_translation(self) if translation is not None: translation.validate_unique(exclude=exclude) #=========================================================================== # Checks #=========================================================================== @classmethod def check(cls, **kwargs): errors = super(TranslatableModel, cls).check(**kwargs) errors.extend(cls._check_shared_translated_clash()) errors.extend(cls._check_default_manager_translation_aware()) return errors @classmethod def _check_shared_translated_clash(cls): fields = set( chain.from_iterable((f.name, f.attname) for f in cls._meta.fields)) tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('id', 'master'))) return [ checks.Error( "translated field '%s' clashes with untranslated field." % field, hint=None, obj=cls, id='hvad.models.E01') for field in tfields.intersection(fields) ] @classmethod def _check_default_manager_translation_aware(cls): errors = [] if not isinstance(cls._default_manager, TranslationManager): errors.append( checks.Error( "The default manager on a TranslatableModel must be a " "TranslationManager instance or an instance of a subclass of " "TranslationManager, the default manager of %r is not." % cls, hint=None, obj=cls, id='hvad.models.E02')) return errors @classmethod def _check_local_fields(cls, fields, option): """ Remove fields we recognize as translated fields from tests """ to_check = [] for field in fields: try: cls._meta.translations_model._meta.get_field(field) except FieldDoesNotExist: to_check.append(field) return super(TranslatableModel, cls)._check_local_fields(to_check, option) @classmethod def _check_ordering(cls): if not cls._meta.ordering: return [] if not isinstance(cls._meta.ordering, (list, tuple)): return [ checks.Error("'ordering' must be a tuple or list.", hint=None, obj=cls, id='models.E014') ] fields = [f for f in cls._meta.ordering if f != '?'] fields = [f[1:] if f.startswith('-') else f for f in fields] fields = set(f for f in fields if f not in ('_order', 'pk') and '__' not in f) valid_fields = set( chain.from_iterable((f.name, f.attname) for f in cls._meta.fields)) valid_tfields = set( chain.from_iterable( (f.name, f.attname) for f in cls._meta.translations_model._meta.fields if f.name not in ('master', 'language_code'))) return [ checks.Error( "'ordering' refers to the non-existent field '%s' --hvad." % field, hint=None, obj=cls, id='models.E015') for field in fields - valid_fields - valid_tfields ]
class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ # change the default manager to the translation manager objects = TranslationManager() class Meta: abstract = True def __init__(self, *args, **kwargs): tkwargs = {} # translated fields skwargs = {} # shared fields if 'master' in kwargs.keys(): raise RuntimeError( "Cannot init %s class with a 'master' argument" % \ self.__class__.__name__ ) # filter out all the translated fields (including 'master' and 'language_code') primary_key_names = ('pk', self._meta.pk.name) for key in list(kwargs.keys()): if key in self._translated_field_names: if not key in primary_key_names: # we exclude the pk of the shared model tkwargs[key] = kwargs.pop(key) if not tkwargs.keys(): # if there where no translated options, then we assume this is a # regular init and don't want to do any funky stuff super(TranslatableModel, self).__init__(*args, **kwargs) return # there was at least one of the translated fields (or a language_code) # in kwargs. We need to do magic. # extract all the shared fields (including the pk) for key in list(kwargs.keys()): if key in self._shared_field_names: skwargs[key] = kwargs.pop(key) # do the regular init minus the translated fields super(TranslatableModel, self).__init__(*args, **skwargs) # prepopulate the translations model cache with an translation model tkwargs['language_code'] = tkwargs.get('language_code', get_language()) tkwargs['master'] = self translated = self._meta.translations_model(*args, **tkwargs) setattr(self, self._meta.translations_cache, translated) @classmethod def save_translations(cls, instance, **kwargs): """ When this instance is saved, also save the (cached) translation """ opts = cls._meta if hasattr(instance, opts.translations_cache): trans = getattr(instance, opts.translations_cache) if not trans.master_id: trans.master = instance trans.save() def translate(self, language_code): """ Returns an Model instance in the specified language. Does NOT check if the translation already exists! Does NOT interact with the database. This will refresh the translations cache attribute on the instance. """ tkwargs = { 'language_code': language_code, 'master': self, } translated = self._meta.translations_model(**tkwargs) setattr(self, self._meta.translations_cache, translated) return self def safe_translation_getter(self, name, default=None): cache = getattr(self, self._meta.translations_cache, None) if not cache: return default return getattr(cache, name, default) def lazy_translation_getter(self, name, default=None): """ Lazy translation getter that fetches translations from DB in case the instance is currently untranslated and saves the translation instance in the translation cache """ stuff = self.safe_translation_getter(name, NoTranslation) if stuff is not NoTranslation: return stuff # get all translations translations = getattr(self, self._meta.translations_accessor).all() # if no translation exists, bail out now if len(translations) == 0: return default # organize translations into a nice dict translation_dict = dict((t.language_code, t) for t in translations) # see if we have the right language, or any language in fallbacks for code in (get_language(), settings.LANGUAGE_CODE) + FALLBACK_LANGUAGES: try: translation = translation_dict[code] except KeyError: continue break else: # none of the fallbacks was found, pick an arbitrary translation translation = translation_dict.popitem()[1] setattr(self, self._meta.translations_cache, translation) return getattr(translation, name, default) def get_available_languages(self): """ Get a list of all available language_code in db. """ qs = getattr(self, self._meta.translations_accessor).all() if qs._result_cache is not None: return [obj.language_code for obj in qs] return qs.values_list('language_code', flat=True) #=========================================================================== # Internals #=========================================================================== @property def _shared_field_names(self): if getattr(self, '_shared_field_names_cache', None) is None: opts = self._meta self._shared_field_names_cache = opts.get_all_field_names() for name in tuple(self._shared_field_names_cache): try: attname = opts.get_field(name).get_attname() except FieldDoesNotExist: pass else: if attname and attname != name: self._shared_field_names_cache.append(attname) return self._shared_field_names_cache @property def _translated_field_names(self): if getattr(self, '_translated_field_names_cache', None) is None: opts = self._meta.translations_model._meta self._translated_field_names_cache = opts.get_all_field_names() for name in tuple(self._translated_field_names_cache): try: attname = opts.get_field(name).get_attname() except FieldDoesNotExist: pass else: if attname and attname != name: self._translated_field_names_cache.append(attname) return self._translated_field_names_cache