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:
parent
b25d1875ef
commit
04703df6b5
@ -28,24 +28,24 @@ from django.db import models, transaction
|
|||||||
from django.db.models import Q, Max
|
from django.db.models import Q, Max
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from mia_core.models import BaseModel
|
from mia_core.models import BaseModel, L10nModel, LocalizedModel
|
||||||
from mia_core.utils import get_multi_lingual_attr, set_multi_lingual_attr
|
|
||||||
|
|
||||||
|
|
||||||
class Account(DirtyFieldsMixin, BaseModel):
|
class Account(DirtyFieldsMixin, LocalizedModel, BaseModel):
|
||||||
"""An account."""
|
"""An account."""
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self", on_delete=models.PROTECT, null=True,
|
"self", on_delete=models.PROTECT, null=True,
|
||||||
related_name="child_set")
|
related_name="child_set")
|
||||||
code = models.CharField(max_length=5, unique=True)
|
code = models.CharField(max_length=5, unique=True)
|
||||||
title_zh_hant = models.CharField(max_length=32)
|
title_l10n = models.CharField(max_length=32, db_column="title")
|
||||||
title_en = models.CharField(max_length=128, null=True)
|
|
||||||
title_zh_hans = models.CharField(max_length=32, null=True)
|
|
||||||
CASH = "1111"
|
CASH = "1111"
|
||||||
ACCUMULATED_BALANCE = "3351"
|
ACCUMULATED_BALANCE = "3351"
|
||||||
NET_CHANGE = "3353"
|
NET_CHANGE = "3353"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "title" in kwargs:
|
||||||
|
self.title = kwargs["title"]
|
||||||
|
del kwargs["title"]
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.url = None
|
self.url = None
|
||||||
self.debit_amount = None
|
self.debit_amount = None
|
||||||
@ -69,12 +69,11 @@ class Account(DirtyFieldsMixin, BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
"""The title in the current language."""
|
return self.get_l10n("title")
|
||||||
return get_multi_lingual_attr(self, "title")
|
|
||||||
|
|
||||||
@title.setter
|
@title.setter
|
||||||
def title(self, value: str) -> None:
|
def title(self, value):
|
||||||
set_multi_lingual_attr(self, "title", value)
|
self.set_l10n("title", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def option_data(self) -> Dict[str, str]:
|
def option_data(self) -> Dict[str, str]:
|
||||||
@ -109,6 +108,12 @@ class Account(DirtyFieldsMixin, BaseModel):
|
|||||||
self._is_in_use = value
|
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):
|
class Transaction(DirtyFieldsMixin, BaseModel):
|
||||||
"""An accounting transaction."""
|
"""An accounting transaction."""
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
@ -155,10 +155,12 @@ class DataFiller:
|
|||||||
code = str(code)
|
code = str(code)
|
||||||
parent = None if len(code) == 1\
|
parent = None if len(code) == 1\
|
||||||
else Account.objects.get(code=code[:-1])
|
else Account.objects.get(code=code[:-1])
|
||||||
Account(pk=new_pk(Account), parent=parent, code=code,
|
account = Account(parent=parent, code=code)
|
||||||
title_zh_hant=data[1], title_en=data[2],
|
account.current_user = self.user
|
||||||
title_zh_hans=data[3],
|
account.set_l10n_in("title", "zh-hant", data[1])
|
||||||
created_by=self.user, updated_by=self.user).save()
|
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],
|
def add_transfer_transaction(self, date: Union[datetime.date, int],
|
||||||
debit: List[RecordData],
|
debit: List[RecordData],
|
||||||
|
@ -39,8 +39,7 @@ from django.views.decorators.http import require_GET, require_POST
|
|||||||
from django.views.generic import ListView, DetailView
|
from django.views.generic import ListView, DetailView
|
||||||
|
|
||||||
from mia_core.period import Period
|
from mia_core.period import Period
|
||||||
from mia_core.utils import Pagination, get_multi_lingual_search, \
|
from mia_core.utils import Pagination, PaginationException
|
||||||
PaginationException
|
|
||||||
from mia_core.views import DeleteView, FormView, RedirectView
|
from mia_core.views import DeleteView, FormView, RedirectView
|
||||||
from . import utils
|
from . import utils
|
||||||
from .forms import AccountForm, TransactionForm, TransactionSortForm
|
from .forms import AccountForm, TransactionForm, TransactionSortForm
|
||||||
@ -767,7 +766,8 @@ def search(request: HttpRequest) -> HttpResponse:
|
|||||||
records = []
|
records = []
|
||||||
else:
|
else:
|
||||||
records = Record.objects.filter(
|
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(account__code__icontains=query)
|
||||||
| Q(summary__icontains=query)
|
| Q(summary__icontains=query)
|
||||||
| Q(transaction__notes__icontains=query))
|
| Q(transaction__notes__icontains=query))
|
||||||
|
@ -18,10 +18,13 @@
|
|||||||
"""The data models of the Mia core application.
|
"""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.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from mia_core.utils import new_pk
|
from mia_core.utils import new_pk, Language
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
@ -54,3 +57,156 @@ class BaseModel(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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
|
||||||
|
@ -89,63 +89,6 @@ class Language:
|
|||||||
return Language(get_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:
|
class UrlBuilder:
|
||||||
"""The URL builder.
|
"""The URL builder.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user