Total Complexity | 112 |
Total Lines | 501 |
Duplicated Lines | 0 % |
Complex classes like TranslatableModelMixin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
218 | class TranslatableModelMixin(object): |
||
219 | """ |
||
220 | Base model mixin class to handle translations. |
||
221 | |||
222 | All translatable fields will appear on this model, proxying the calls to the :class:`TranslatedFieldsModel`. |
||
223 | """ |
||
224 | #: Access to the metadata of the translatable model |
||
225 | _parler_meta = None |
||
226 | |||
227 | #: Access to the language code |
||
228 | language_code = LanguageCodeDescriptor() |
||
229 | |||
230 | def __init__(self, *args, **kwargs): |
||
231 | # Still allow to pass the translated fields (e.g. title=...) to this function. |
||
232 | translated_kwargs = {} |
||
233 | current_language = None |
||
234 | if kwargs: |
||
235 | current_language = kwargs.pop('_current_language', None) |
||
236 | for field in self._parler_meta.get_all_fields(): |
||
237 | try: |
||
238 | translated_kwargs[field] = kwargs.pop(field) |
||
239 | except KeyError: |
||
240 | pass |
||
241 | |||
242 | # Have the attributes available, but they can't be ready yet; |
||
243 | # self._state.adding is always True at this point, |
||
244 | # the QuerySet.iterator() code changes it after construction. |
||
245 | self._translations_cache = None |
||
246 | self._current_language = None |
||
247 | |||
248 | # Run original Django model __init__ |
||
249 | super(TranslatableModelMixin, self).__init__(*args, **kwargs) |
||
250 | |||
251 | # Assign translated args manually. |
||
252 | self._translations_cache = defaultdict(dict) |
||
253 | self._current_language = normalize_language_code(current_language or get_language()) # What you used to fetch the object is what you get. |
||
254 | |||
255 | if translated_kwargs: |
||
256 | self._set_translated_fields(self._current_language, **translated_kwargs) |
||
257 | |||
258 | def _set_translated_fields(self, language_code=None, **fields): |
||
259 | """ |
||
260 | Assign fields to the translated models. |
||
261 | """ |
||
262 | objects = [] # no generator, make sure objects are all filled first |
||
263 | for parler_meta, model_fields in self._parler_meta._split_fields(**fields): |
||
264 | translation = self._get_translated_model(language_code=language_code, auto_create=True, meta=parler_meta) |
||
265 | for field, value in six.iteritems(model_fields): |
||
266 | setattr(translation, field, value) |
||
267 | |||
268 | objects.append(translation) |
||
269 | return objects |
||
270 | |||
271 | def create_translation(self, language_code, **fields): |
||
272 | """ |
||
273 | Add a translation to the model. |
||
274 | |||
275 | The :func:`save_translations` function is called afterwards. |
||
276 | |||
277 | The object will be saved immediately, similar to |
||
278 | calling :func:`~django.db.models.manager.Manager.create` |
||
279 | or :func:`~django.db.models.fields.related.RelatedManager.create` on related fields. |
||
280 | """ |
||
281 | meta = self._parler_meta |
||
282 | if self._translations_cache[meta.root_model].get(language_code, None): # MISSING evaluates to False too |
||
283 | raise ValueError("Translation already exists: {0}".format(language_code)) |
||
284 | |||
285 | # Save all fields in the proper translated model. |
||
286 | for translation in self._set_translated_fields(language_code, **fields): |
||
287 | self.save_translation(translation) |
||
288 | |||
289 | def get_current_language(self): |
||
290 | """ |
||
291 | Get the current language. |
||
292 | """ |
||
293 | # not a property, so won't conflict with model fields. |
||
294 | return self._current_language |
||
295 | |||
296 | def set_current_language(self, language_code, initialize=False): |
||
297 | """ |
||
298 | Switch the currently activate language of the object. |
||
299 | """ |
||
300 | self._current_language = normalize_language_code(language_code or get_language()) |
||
301 | |||
302 | # Ensure the translation is present for __get__ queries. |
||
303 | if initialize: |
||
304 | self._get_translated_model(use_fallback=False, auto_create=True) |
||
305 | |||
306 | def get_fallback_language(self): |
||
307 | """ |
||
308 | .. deprecated:: 1.5 |
||
309 | Use :func:`get_fallback_languages` instead. |
||
310 | """ |
||
311 | fallbacks = self.get_fallback_languages() |
||
312 | return fallbacks[0] if fallbacks else None |
||
313 | |||
314 | def get_fallback_languages(self): |
||
315 | """ |
||
316 | Return the fallback language codes, |
||
317 | which are used in case there is no translation for the currently active language. |
||
318 | """ |
||
319 | lang_dict = get_language_settings(self._current_language) |
||
320 | fallbacks = [lang for lang in lang_dict['fallbacks'] if lang != self._current_language] |
||
321 | return fallbacks or [] |
||
322 | |||
323 | def has_translation(self, language_code=None, related_name=None): |
||
324 | """ |
||
325 | Return whether a translation for the given language exists. |
||
326 | Defaults to the current language code. |
||
327 | |||
328 | .. versionadded 1.2 Added the ``related_name`` parameter. |
||
329 | """ |
||
330 | if language_code is None: |
||
331 | language_code = self._current_language |
||
332 | |||
333 | meta = self._parler_meta._get_extension_by_related_name(related_name) |
||
334 | |||
335 | try: |
||
336 | # Check the local cache directly, and the answer is known. |
||
337 | # NOTE this may also return newly auto created translations which are not saved yet. |
||
338 | return self._translations_cache[meta.model][language_code] is not MISSING |
||
339 | except KeyError: |
||
340 | # If there is a prefetch, will be using that. |
||
341 | # However, don't assume the prefetch contains all possible languages. |
||
342 | # With Django 1.8, there are custom Prefetch objects. |
||
343 | # TODO: improve this, detect whether this is the case. |
||
344 | if language_code in self._read_prefetched_translations(meta=meta): |
||
345 | return True |
||
346 | |||
347 | # Try to fetch from the cache first. |
||
348 | # If the cache returns the fallback, it means the original does not exist. |
||
349 | object = get_cached_translation(self, language_code, related_name=related_name, use_fallback=True) |
||
350 | if object is not None: |
||
351 | return object.language_code == language_code |
||
352 | |||
353 | try: |
||
354 | # Fetch from DB, fill the cache. |
||
355 | self._get_translated_model(language_code, use_fallback=False, auto_create=False, meta=meta) |
||
356 | except meta.model.DoesNotExist: |
||
357 | return False |
||
358 | else: |
||
359 | return True |
||
360 | |||
361 | def get_available_languages(self, related_name=None, include_unsaved=False): |
||
362 | """ |
||
363 | Return the language codes of all translated variations. |
||
364 | |||
365 | .. versionadded 1.2 Added the ``include_unsaved`` and ``related_name`` parameters. |
||
366 | """ |
||
367 | meta = self._parler_meta._get_extension_by_related_name(related_name) |
||
368 | |||
369 | prefetch = self._get_prefetched_translations(meta=meta) |
||
370 | if prefetch is not None: |
||
371 | # TODO: this will break when using custom Django 1.8 Prefetch objects? |
||
372 | db_languages = sorted(obj.language_code for obj in prefetch) |
||
373 | else: |
||
374 | qs = self._get_translated_queryset(meta=meta) |
||
375 | db_languages = qs.values_list('language_code', flat=True).order_by('language_code') |
||
376 | |||
377 | if include_unsaved: |
||
378 | local_languages = (k for k, v in six.iteritems(self._translations_cache[meta.model]) if v is not MISSING) |
||
379 | return list(set(db_languages) | set(local_languages)) |
||
380 | else: |
||
381 | return db_languages |
||
382 | |||
383 | def get_translation(self, language_code, related_name=None): |
||
384 | """ |
||
385 | Fetch the translated model |
||
386 | """ |
||
387 | meta = self._parler_meta._get_extension_by_related_name(related_name) |
||
388 | return self._get_translated_model(language_code, meta=meta) |
||
389 | |||
390 | def _get_translated_model(self, language_code=None, use_fallback=False, auto_create=False, meta=None): |
||
391 | """ |
||
392 | Fetch the translated fields model. |
||
393 | """ |
||
394 | if self._parler_meta is None: |
||
395 | raise ImproperlyConfigured("No translation is assigned to the current model!") |
||
396 | if self._translations_cache is None: |
||
397 | raise RuntimeError("Accessing translated fields before super.__init__() is not possible.") |
||
398 | |||
399 | if not language_code: |
||
400 | language_code = self._current_language |
||
401 | if meta is None: |
||
402 | meta = self._parler_meta.root # work on base model by default |
||
403 | |||
404 | local_cache = self._translations_cache[meta.model] |
||
405 | |||
406 | # 1. fetch the object from the local cache |
||
407 | try: |
||
408 | object = local_cache[language_code] |
||
409 | |||
410 | # If cached object indicates the language doesn't exist, need to query the fallback. |
||
411 | if object is not MISSING: |
||
412 | return object |
||
413 | except KeyError: |
||
414 | # 2. No cache, need to query |
||
415 | # Check that this object already exists, would be pointless otherwise to check for a translation. |
||
416 | if not self._state.adding and self.pk is not None: |
||
417 | prefetch = self._get_prefetched_translations(meta=meta) |
||
418 | if prefetch is not None: |
||
419 | # 2.1, use prefetched data |
||
420 | # If the object is not found in the prefetched data (which contains all translations), |
||
421 | # it's pointless to check for memcached (2.2) or perform a single query (2.3) |
||
422 | for object in prefetch: |
||
423 | if object.language_code == language_code: |
||
424 | local_cache[language_code] = object |
||
425 | _cache_translation(object) # Store in memcached |
||
426 | return object |
||
427 | else: |
||
428 | # 2.2, fetch from memcached |
||
429 | object = get_cached_translation(self, language_code, related_name=meta.rel_name, use_fallback=use_fallback) |
||
430 | if object is not None: |
||
431 | # Track in local cache |
||
432 | if object.language_code != language_code: |
||
433 | local_cache[language_code] = MISSING # Set fallback marker |
||
434 | local_cache[object.language_code] = object |
||
435 | return object |
||
436 | elif local_cache.get(language_code, None) is MISSING: |
||
437 | # If get_cached_translation() explicitly set the "does not exist" marker, |
||
438 | # there is no need to try a database query. |
||
439 | pass |
||
440 | else: |
||
441 | # 2.3, fetch from database |
||
442 | try: |
||
443 | object = self._get_translated_queryset(meta).get(language_code=language_code) |
||
444 | except meta.model.DoesNotExist: |
||
445 | pass |
||
446 | else: |
||
447 | local_cache[language_code] = object |
||
448 | _cache_translation(object) # Store in memcached |
||
449 | return object |
||
450 | |||
451 | # Not in cache, or default. |
||
452 | # Not fetched from DB |
||
453 | |||
454 | # 3. Auto create? |
||
455 | if auto_create: |
||
456 | # Auto create policy first (e.g. a __set__ call) |
||
457 | kwargs = { |
||
458 | 'language_code': language_code, |
||
459 | } |
||
460 | if self.pk: |
||
461 | # ID might be None at this point, and Django 1.8 does not allow that. |
||
462 | kwargs['master'] = self |
||
463 | |||
464 | object = meta.model(**kwargs) |
||
465 | local_cache[language_code] = object |
||
466 | # Not stored in memcached here yet, first fill + save it. |
||
467 | return object |
||
468 | |||
469 | # 4. Fallback? |
||
470 | fallback_msg = None |
||
471 | lang_dict = get_language_settings(language_code) |
||
472 | |||
473 | if language_code not in local_cache: |
||
474 | # Explicitly set a marker for the fact that this translation uses the fallback instead. |
||
475 | # Avoid making that query again. |
||
476 | local_cache[language_code] = MISSING # None value is the marker. |
||
477 | if not self._state.adding or self.pk is not None: |
||
478 | _cache_translation_needs_fallback(self, language_code, related_name=meta.rel_name) |
||
479 | |||
480 | fallback_choices = [lang_dict['code']] + list(lang_dict['fallbacks']) |
||
481 | if use_fallback and fallback_choices: |
||
482 | # Jump to fallback language, return directly. |
||
483 | # Don't cache under this language_code |
||
484 | for fallback_lang in fallback_choices: |
||
485 | if fallback_lang == language_code: # Skip the current language, could also be fallback 1 of 2 choices |
||
486 | continue |
||
487 | |||
488 | try: |
||
489 | return self._get_translated_model(fallback_lang, use_fallback=False, auto_create=auto_create, meta=meta) |
||
490 | except meta.model.DoesNotExist: |
||
491 | pass |
||
492 | |||
493 | fallback_msg = " (tried fallbacks {0})".format(', '.join(lang_dict['fallbacks'])) |
||
494 | |||
495 | # None of the above, bail out! |
||
496 | raise meta.model.DoesNotExist( |
||
497 | "{0} does not have a translation for the current language!\n" |
||
498 | "{0} ID #{1}, language={2}{3}".format(self._meta.verbose_name, self.pk, language_code, fallback_msg or '' |
||
499 | )) |
||
500 | |||
501 | def _get_any_translated_model(self, meta=None): |
||
502 | """ |
||
503 | Return any available translation. |
||
504 | Returns None if there are no translations at all. |
||
505 | """ |
||
506 | if meta is None: |
||
507 | meta = self._parler_meta.root |
||
508 | |||
509 | tr_model = meta.model |
||
510 | local_cache = self._translations_cache[tr_model] |
||
511 | if local_cache: |
||
512 | # There is already a language available in the case. No need for queries. |
||
513 | # Give consistent answers if they exist. |
||
514 | check_languages = [self._current_language] + self.get_fallback_languages() |
||
515 | try: |
||
516 | for fallback_lang in check_languages: |
||
517 | trans = local_cache.get(fallback_lang, None) |
||
518 | if trans: |
||
519 | return trans |
||
520 | return next(t for t in six.itervalues(local_cache) if t is not MISSING) |
||
521 | except StopIteration: |
||
522 | pass |
||
523 | |||
524 | try: |
||
525 | # Use prefetch if available, otherwise perform separate query. |
||
526 | prefetch = self._get_prefetched_translations(meta=meta) |
||
527 | if prefetch is not None: |
||
528 | translation = prefetch[0] # Already a list |
||
529 | else: |
||
530 | translation = self._get_translated_queryset(meta=meta)[0] |
||
531 | except IndexError: |
||
532 | return None |
||
533 | else: |
||
534 | local_cache[translation.language_code] = translation |
||
535 | _cache_translation(translation) |
||
536 | return translation |
||
537 | |||
538 | def _get_translated_queryset(self, meta=None): |
||
539 | """ |
||
540 | Return the queryset that points to the translated model. |
||
541 | If there is a prefetch, it can be read from this queryset. |
||
542 | """ |
||
543 | # Get via self.TRANSLATIONS_FIELD.get(..) so it also uses the prefetch/select_related cache. |
||
544 | if meta is None: |
||
545 | meta = self._parler_meta.root |
||
546 | |||
547 | accessor = getattr(self, meta.rel_name) |
||
548 | if django.VERSION >= (1, 6): |
||
549 | # Call latest version |
||
550 | return accessor.get_queryset() |
||
551 | else: |
||
552 | # Must call RelatedManager.get_query_set() and avoid calling a custom get_queryset() |
||
553 | # method for packages with Django 1.6/1.7 compatibility. |
||
554 | return accessor.get_query_set() |
||
555 | |||
556 | def _get_prefetched_translations(self, meta=None): |
||
557 | """ |
||
558 | Return the queryset with prefetch results. |
||
559 | """ |
||
560 | if meta is None: |
||
561 | meta = self._parler_meta.root |
||
562 | |||
563 | related_name = meta.rel_name |
||
564 | try: |
||
565 | # Read the list directly, avoid QuerySet construction. |
||
566 | # Accessing self._get_translated_queryset(parler_meta)._prefetch_done is more expensive. |
||
567 | return self._prefetched_objects_cache[related_name] |
||
568 | except (AttributeError, KeyError): |
||
569 | return None |
||
570 | |||
571 | def _read_prefetched_translations(self, meta=None): |
||
572 | # Load the prefetched translations into the local cache. |
||
573 | if meta is None: |
||
574 | meta = self._parler_meta.root |
||
575 | |||
576 | local_cache = self._translations_cache[meta.model] |
||
577 | prefetch = self._get_prefetched_translations(meta=meta) |
||
578 | |||
579 | languages_seen = [] |
||
580 | if prefetch is not None: |
||
581 | for translation in prefetch: |
||
582 | lang = translation.language_code |
||
583 | languages_seen.append(lang) |
||
584 | if lang not in local_cache or local_cache[lang] is MISSING: |
||
585 | local_cache[lang] = translation |
||
586 | |||
587 | return languages_seen |
||
588 | |||
589 | def save(self, *args, **kwargs): |
||
590 | super(TranslatableModelMixin, self).save(*args, **kwargs) |
||
591 | |||
592 | # Makes no sense to add these for translated model |
||
593 | # Even worse: mptt 0.7 injects this parameter when it avoids updating the lft/rgt fields, |
||
594 | # but that misses all the translated fields. |
||
595 | kwargs.pop('update_fields', None) |
||
596 | self.save_translations(*args, **kwargs) |
||
597 | |||
598 | def delete(self, using=None): |
||
599 | _delete_cached_translations(self) |
||
600 | super(TranslatableModelMixin, self).delete(using) |
||
601 | |||
602 | def validate_unique(self, exclude=None): |
||
603 | """ |
||
604 | Also validate the unique_together of the translated model. |
||
605 | """ |
||
606 | # This is called from ModelForm._post_clean() or Model.full_clean() |
||
607 | errors = {} |
||
608 | try: |
||
609 | super(TranslatableModelMixin, self).validate_unique(exclude=exclude) |
||
610 | except ValidationError as e: |
||
611 | errors = e.message_dict # Django 1.5 + 1.6 compatible |
||
612 | |||
613 | for local_cache in six.itervalues(self._translations_cache): |
||
614 | for translation in six.itervalues(local_cache): |
||
615 | if translation is MISSING: # Skip fallback markers |
||
616 | continue |
||
617 | |||
618 | try: |
||
619 | translation.validate_unique(exclude=exclude) |
||
620 | except ValidationError as e: |
||
621 | errors.update(e.message_dict) |
||
622 | |||
623 | if errors: |
||
624 | raise ValidationError(errors) |
||
625 | |||
626 | def save_translations(self, *args, **kwargs): |
||
627 | """ |
||
628 | The method to save all translations. |
||
629 | This can be overwritten to implement any custom additions. |
||
630 | This method calls :func:`save_translation` for every fetched language. |
||
631 | |||
632 | :param args: Any custom arguments to pass to :func:`save`. |
||
633 | :param kwargs: Any custom arguments to pass to :func:`save`. |
||
634 | """ |
||
635 | # Copy cache, new objects (e.g. fallbacks) might be fetched if users override save_translation() |
||
636 | # Not looping over the cache, but using _parler_meta so the translations are processed in the order of inheritance. |
||
637 | local_caches = self._translations_cache.copy() |
||
638 | for meta in self._parler_meta: |
||
639 | local_cache = local_caches[meta.model] |
||
640 | translations = list(local_cache.values()) |
||
641 | |||
642 | # Save all translated objects which were fetched. |
||
643 | # This also supports switching languages several times, and save everything in the end. |
||
644 | for translation in translations: |
||
645 | if translation is MISSING: # Skip fallback markers |
||
646 | continue |
||
647 | |||
648 | self.save_translation(translation, *args, **kwargs) |
||
649 | |||
650 | def save_translation(self, translation, *args, **kwargs): |
||
651 | """ |
||
652 | Save the translation when it's modified, or unsaved. |
||
653 | |||
654 | .. note:: |
||
655 | |||
656 | When a derived model provides additional translated fields, |
||
657 | this method receives both the original and extended translation. |
||
658 | To distinguish between both objects, check for ``translation.related_name``. |
||
659 | |||
660 | :param translation: The translation |
||
661 | :type translation: TranslatedFieldsModel |
||
662 | :param args: Any custom arguments to pass to :func:`save`. |
||
663 | :param kwargs: Any custom arguments to pass to :func:`save`. |
||
664 | """ |
||
665 | if self.pk is None or self._state.adding: |
||
666 | raise RuntimeError("Can't save translations when the master object is not yet saved.") |
||
667 | |||
668 | # Translation models without any fields are also supported. |
||
669 | # This is useful for parent objects that have inlines; |
||
670 | # the parent object defines how many translations there are. |
||
671 | if translation.is_modified or (translation.is_empty and not translation.pk): |
||
672 | if not translation.master_id: # Might not exist during first construction |
||
673 | translation._state.db = self._state.db |
||
674 | translation.master = self |
||
675 | translation.save(*args, **kwargs) |
||
676 | |||
677 | def safe_translation_getter(self, field, default=None, language_code=None, any_language=False): |
||
678 | """ |
||
679 | Fetch a translated property, and return a default value |
||
680 | when both the translation and fallback language are missing. |
||
681 | |||
682 | When ``any_language=True`` is used, the function also looks |
||
683 | into other languages to find a suitable value. This feature can be useful |
||
684 | for "title" attributes for example, to make sure there is at least something being displayed. |
||
685 | Also consider using ``field = TranslatedField(any_language=True)`` in the model itself, |
||
686 | to make this behavior the default for the given field. |
||
687 | |||
688 | .. versionchanged 1.5:: The *default* parameter may also be a callable. |
||
689 | """ |
||
690 | meta = self._parler_meta._get_extension_by_field(field) |
||
691 | |||
692 | # Extra feature: query a single field from a other translation. |
||
693 | if language_code and language_code != self._current_language: |
||
694 | try: |
||
695 | tr_model = self._get_translated_model(language_code, meta=meta, use_fallback=True) |
||
696 | return getattr(tr_model, field) |
||
697 | except TranslationDoesNotExist: |
||
698 | pass |
||
699 | else: |
||
700 | # By default, query via descriptor (TranslatedFieldDescriptor) |
||
701 | # which also attempts the fallback language if configured to do so. |
||
702 | try: |
||
703 | return getattr(self, field) |
||
704 | except TranslationDoesNotExist: |
||
705 | pass |
||
706 | |||
707 | if any_language: |
||
708 | translation = self._get_any_translated_model(meta=meta) |
||
709 | if translation is not None: |
||
710 | try: |
||
711 | return getattr(translation, field) |
||
712 | except KeyError: |
||
713 | pass |
||
714 | |||
715 | if callable(default): |
||
716 | return default() |
||
717 | else: |
||
718 | return default |
||
719 | |||
1172 |