Replaced the combined localized data models into flexible, separated localized data models and their accompanying localization data models, so that adding new languages works automatically without having to change the data model definitions.

This commit is contained in:
依瑪貓 2020-08-24 21:59:50 +08:00
parent b25d1875ef
commit 04703df6b5
5 changed files with 181 additions and 75 deletions

View File

@ -28,24 +28,24 @@ from django.db import models, transaction
from django.db.models import Q, Max
from django.http import HttpRequest
from mia_core.models import BaseModel
from mia_core.utils import get_multi_lingual_attr, set_multi_lingual_attr
from mia_core.models import BaseModel, L10nModel, LocalizedModel
class Account(DirtyFieldsMixin, BaseModel):
class Account(DirtyFieldsMixin, LocalizedModel, BaseModel):
"""An account."""
parent = models.ForeignKey(
"self", on_delete=models.PROTECT, null=True,
related_name="child_set")
code = models.CharField(max_length=5, unique=True)
title_zh_hant = models.CharField(max_length=32)
title_en = models.CharField(max_length=128, null=True)
title_zh_hans = models.CharField(max_length=32, null=True)
title_l10n = models.CharField(max_length=32, db_column="title")
CASH = "1111"
ACCUMULATED_BALANCE = "3351"
NET_CHANGE = "3353"
def __init__(self, *args, **kwargs):
if "title" in kwargs:
self.title = kwargs["title"]
del kwargs["title"]
super().__init__(*args, **kwargs)
self.url = None
self.debit_amount = None
@ -69,12 +69,11 @@ class Account(DirtyFieldsMixin, BaseModel):
@property
def title(self) -> str:
"""The title in the current language."""
return get_multi_lingual_attr(self, "title")
return self.get_l10n("title")
@title.setter
def title(self, value: str) -> None:
set_multi_lingual_attr(self, "title", value)
def title(self, value):
self.set_l10n("title", value)
@property
def option_data(self) -> Dict[str, str]:
@ -109,6 +108,12 @@ class Account(DirtyFieldsMixin, BaseModel):
self._is_in_use = value
class AccountL10n(DirtyFieldsMixin, L10nModel, BaseModel):
"""The localization content of an account."""
master = models.ForeignKey(
Account, on_delete=models.CASCADE, related_name="l10n_set")
class Transaction(DirtyFieldsMixin, BaseModel):
"""An accounting transaction."""
date = models.DateField()

View File

@ -155,10 +155,12 @@ class DataFiller:
code = str(code)
parent = None if len(code) == 1\
else Account.objects.get(code=code[:-1])
Account(pk=new_pk(Account), parent=parent, code=code,
title_zh_hant=data[1], title_en=data[2],
title_zh_hans=data[3],
created_by=self.user, updated_by=self.user).save()
account = Account(parent=parent, code=code)
account.current_user = self.user
account.set_l10n_in("title", "zh-hant", data[1])
account.set_l10n_in("title", "en", data[2])
account.set_l10n_in("title", "zh-hans", data[3])
account.save()
def add_transfer_transaction(self, date: Union[datetime.date, int],
debit: List[RecordData],

View File

@ -39,8 +39,7 @@ from django.views.decorators.http import require_GET, require_POST
from django.views.generic import ListView, DetailView
from mia_core.period import Period
from mia_core.utils import Pagination, get_multi_lingual_search, \
PaginationException
from mia_core.utils import Pagination, PaginationException
from mia_core.views import DeleteView, FormView, RedirectView
from . import utils
from .forms import AccountForm, TransactionForm, TransactionSortForm
@ -767,7 +766,8 @@ def search(request: HttpRequest) -> HttpResponse:
records = []
else:
records = Record.objects.filter(
get_multi_lingual_search("account__title", query)
Q(account__title_l10n__icontains=query)
| Q(account__l10n_set__value__icontains=query)
| Q(account__code__icontains=query)
| Q(summary__icontains=query)
| Q(transaction__notes__icontains=query))

View File

@ -18,10 +18,13 @@
"""The data models of the Mia core application.
"""
from typing import Any, Dict, List
from dirtyfields import DirtyFieldsMixin
from django.conf import settings
from django.db import models
from mia_core.utils import new_pk
from mia_core.utils import new_pk, Language
class BaseModel(models.Model):
@ -54,3 +57,156 @@ class BaseModel(models.Model):
class Meta:
abstract = True
class LocalizedModel(models.Model):
"""An abstract localized model."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if getattr(self, "_l10n", None) is None:
self._l10n: Dict[str, Dict[str, Any]] = {}
# Amends the is_dirty() method in DirtyFieldsMixin
if isinstance(self, DirtyFieldsMixin):
old_is_dirty = getattr(self, "is_dirty", None)
def new_is_dirty(check_relationship=False, check_m2m=None) -> bool:
"""Returns whether the current data model is changed."""
if old_is_dirty(check_relationship=check_relationship,
check_m2m=check_m2m):
return True
default_language = self._get_default_language()
for name in self._l10n:
for language in self._l10n[name]:
new_value = self._l10n[name][language]
if language == default_language:
if getattr(self, name + "_l10n") != new_value:
return True
else:
l10n_rec = self._get_l10n_set() \
.filter(name=name, language=language) \
.first()
if l10n_rec is None or l10n_rec.value != new_value:
return True
return False
self.is_dirty = new_is_dirty
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
"""Saves the data model, along with the localized contents."""
default_language = self._get_default_language()
l10n_to_save: List[models.Model] = []
if getattr(self, "_l10n", None) is not None:
for name in self._l10n:
for language in self._l10n[name]:
new_value = self._l10n[name][language]
if language == default_language:
setattr(self, name + "_l10n", new_value)
else:
current_value = getattr(self, name + "_l10n")
if current_value is None or current_value == "":
setattr(self, name + "_l10n", new_value)
l10n_rec = self._get_l10n_set()\
.filter(name=name, language=language)\
.first()
if l10n_rec is None:
l10n_to_save.append(self._get_l10n_set().model(
master=self, name=name,
language=language,
value=self._l10n[name][language]))
elif l10n_rec.value != new_value:
if getattr(self, name + "_l10n") == l10n_rec.value:
setattr(self, name + "_l10n", new_value)
l10n_rec.value = new_value
l10n_to_save.append(l10n_rec)
super().save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
for l10n_rec in l10n_to_save:
if isinstance(self, BaseModel) and isinstance(l10n_rec, BaseModel):
l10n_rec.current_user = self.current_user
l10n_rec.save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
def _get_l10n_set(self):
"""Returns the related localization data model."""
l10n_set = getattr(self, "l10n_set", None)
if l10n_set is None:
raise AttributeError("Please define the localization data model.")
return l10n_set
def _get_default_language(self) -> str:
"""Returns the default language."""
default = getattr(self.__class__, "DEFAULT_LANGUAGE", None)
return Language.default().id if default is None else default
def get_l10n(self, name: str) -> Any:
"""Returns the value of a localized field in the current language.
Args:
name: The field name.
Returns:
The value of this field in the current language.
"""
return self.get_l10n_in(name, Language.current().id)
def get_l10n_in(self, name: str, language: str) -> Any:
"""Returns the value of a localized field in a specific language.
Args:
name: The field name.
language: The language ID.
Returns:
The value of this field in this language.
"""
if getattr(self, "_l10n", None) is None:
self._l10n: Dict[str, Dict[str, Any]] = {}
if name not in self._l10n:
self._l10n[name]: Dict[str, Any] = {}
if language not in self._l10n[name]:
if language != self._get_default_language():
l10n_rec = self._get_l10n_set() \
.filter(name=name, language=language) \
.first()
self._l10n[name][language] = getattr(self, name + "_l10n") \
if l10n_rec is None else l10n_rec.value
else:
self._l10n[name][language] = getattr(self, name + "_l10n")
return self._l10n[name][language]
def set_l10n(self, name: str, value: Any) -> None:
"""Sets the value of a localized field in the current language.
Args:
name: The field name.
value: The value.
"""
self.set_l10n_in(name, Language.current().id, value)
def set_l10n_in(self, name: str, language: str, value: Any) -> None:
"""Sets the value of a localized field in a specific language.
Args:
name: The field name.
language: The language ID.
value: The value.
"""
if getattr(self, "_l10n", None) is None:
self._l10n: Dict[str, Dict[str, Any]] = {}
if name not in self._l10n:
self._l10n[name]: Dict[str, Any] = {}
self._l10n[name][language] = value
class Meta:
abstract = True
class L10nModel(models.Model):
"""The abstract base localization model."""
name = models.CharField(max_length=128)
language = models.CharField(max_length=7)
value = models.CharField(max_length=65535)
class Meta:
abstract = True

View File

@ -89,63 +89,6 @@ class Language:
return Language(get_language())
def get_multi_lingual_attr(model: Model, name: str,
default: str = None) -> str:
"""Returns a multi-lingual attribute of a data model.
Args:
model: The data model.
name: The attribute name.
default: The default language.
Returns:
The attribute in this language, or in the default language if there is
no content in the current language.
"""
language = Language.current()
title = getattr(model, name + language.db)
if default is None:
default = Language.default().id
if language.id == default:
return title
if title is not None:
return title
return getattr(model, name + Language.default().db)
def set_multi_lingual_attr(model: Model, name: str, value: str) -> None:
"""Sets a multi-lingual attribute of a data model.
Args:
model: The data model.
name: The attribute name.
value: The new value
"""
language = Language.current()
setattr(model, name + language.db, value)
def get_multi_lingual_search(attr: str, query: str) -> Q:
"""Returns the query condition on a multi-lingual attribute.
Args:
attr: The base name of the multi-lingual attribute.
query: The query.
Returns:
The query condition
"""
language = Language.current()
if language.is_default:
return Q(**{attr + language.db + "__icontains": query})
default = Language.default()
q = (Q(**{attr + language.db + "__isnull": False})
& Q(**{attr + language.db + "__icontains": query}))\
| (Q(**{attr + language.db + "__isnull": True})
& Q(**{attr + default.db + "__icontains": query}))
return q
class UrlBuilder:
"""The URL builder.