Skip to content

Commit

Permalink
v0.6.1 (#204)
Browse files Browse the repository at this point in the history
* Relaxing Dependency Constraints

* Several dependencies in Pipfile.lock have been updated to their latest versions.

* Update version and refactor authorization checks

The project version is updated to 0.6.1. In addition, updates in authorization checks are made across different Django ledger classes with a focus on enhancing control flow and efficiency. This refactor removes direct superuser checks embedded in business logic with a dedicated `get_superuser_authorization` method, streamlines permission checks, and ensures that special case handling (such as ledger lock, unlock, post and unpost) upholds the principle of early return for easier code maintenance.

* Rename 'io_digest' module to 'io_context' and enhance DigestContextMixIn class

Renamed the 'io_digest' module to 'io_context' for better clarity and improved the DigestContextMixIn class. The 'IO_DIGEST' and 'IO_DIGEST_EQUITY' attributes have been replaced with 'IO_DIGEST_UNBOUNDED' and 'IO_DIGEST_BOUNDED'. Correspondingly, new context names for these attributes have been created. Also added two properties 'from_datetime' and 'to_datetime' to the class 'IODigestContextManager' in the 'io_context' module. The imports have been updated to reflect the module name change.

* Pipfile Update

* Update version and refactor authorization checks

The project version is updated to 0.6.1. In addition, updates in authorization checks are made across different Django ledger classes with a focus on enhancing control flow and efficiency. This refactor removes direct superuser checks embedded in business logic with a dedicated `get_superuser_authorization` method, streamlines permission checks, and ensures that special case handling (such as ledger lock, unlock, post and unpost) upholds the principle of early return for easier code maintenance.

* Rename 'io_digest' module to 'io_context' and enhance DigestContextMixIn class

Renamed the 'io_digest' module to 'io_context' for better clarity and improved the DigestContextMixIn class. The 'IO_DIGEST' and 'IO_DIGEST_EQUITY' attributes have been replaced with 'IO_DIGEST_UNBOUNDED' and 'IO_DIGEST_BOUNDED'. Correspondingly, new context names for these attributes have been created. Also added two properties 'from_datetime' and 'to_datetime' to the class 'IODigestContextManager' in the 'io_context' module. The imports have been updated to reflect the module name change.
  • Loading branch information
elarroba committed May 10, 2024
1 parent 4cf092d commit 7348491
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 237 deletions.
288 changes: 136 additions & 152 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion django_ledger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
default_app_config = 'django_ledger.apps.DjangoLedgerConfig'

"""Django Ledger"""
__version__ = '0.6.0.2'
__version__ = '0.6.1'
__license__ = 'GPLv3 License'

__author__ = 'Miguel Sanda'
Expand Down
4 changes: 1 addition & 3 deletions django_ledger/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
Miguel Sanda <msanda@arrobalytics.com>
"""

from django_ledger.io.io_digest import *
from django_ledger.io.io_context import *
from django_ledger.io.io_middleware import *
from django_ledger.io.ratios import *
from django_ledger.io.roles import *
# due to circular import
# from django_ledger.io.io_library import IOLibrary
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def get_io_txs_queryset(self):
def get_strftime_format(self):
return self.STRFTIME_FORMAT

@property
def from_datetime(self):
return self.get_from_datetime()

def get_from_datetime(self, as_str: bool = False, fmt=None) -> Optional[datetime]:
from_date = self.IO_DATA['from_date']
if from_date:
Expand All @@ -47,6 +51,10 @@ def get_from_datetime(self, as_str: bool = False, fmt=None) -> Optional[datetime
return from_date.strftime(fmt)
return from_date

@property
def to_datetime(self):
return self.get_to_datetime()

def get_to_datetime(self, as_str: bool = False, fmt=None) -> datetime:
if as_str:
if not fmt:
Expand Down
2 changes: 1 addition & 1 deletion django_ledger/io/io_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from django_ledger import settings
from django_ledger.exceptions import InvalidDateInputError, TransactionNotInBalanceError
from django_ledger.io import roles as roles_module
from django_ledger.io.io_digest import IODigestContextManager
from django_ledger.io.io_context import IODigestContextManager
from django_ledger.io.io_middleware import (
AccountRoleIOMiddleware,
AccountGroupIOMiddleware,
Expand Down
6 changes: 4 additions & 2 deletions django_ledger/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def get_queryset(self):
qs = EntityModelQuerySet(self.model, using=self._db).order_by('path')
return qs.order_by('path').select_related('admin', 'default_coa')

def for_user(self, user_model):
def for_user(self, user_model, authorized_superuser: bool = False):
"""
This QuerySet guarantees that Users do not access or operate on EntityModels that don't have access to.
This is the recommended initial QuerySet.
Expand All @@ -125,6 +125,8 @@ def for_user(self, user_model):
----------
user_model
The Django User Model making the request.
authorized_superuser
Allows any superuser to access the EntityModel. Default is False.
Returns
-------
Expand All @@ -134,7 +136,7 @@ def for_user(self, user_model):
2. Is a manager.
"""
qs = self.get_queryset()
if user_model.is_superuser:
if user_model.is_superuser and authorized_superuser:
return qs
return qs.filter(
Q(admin=user_model) |
Expand Down
4 changes: 1 addition & 3 deletions django_ledger/models/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,8 +881,6 @@ class ItemTransactionModelManager(models.Manager):

def for_user(self, user_model):
qs = self.get_queryset()
if user_model.is_superuser:
return qs
return qs.filter(
Q(item_model__entity__admin=user_model) |
Q(item_model__entity__managers__in=[user_model])
Expand All @@ -891,7 +889,7 @@ def for_user(self, user_model):
def for_entity(self, user_model, entity_slug):
qs = self.for_user(user_model)
if isinstance(entity_slug, lazy_loader.get_entity_model()):
qs.filter(
return qs.filter(
Q(item_model__entity=entity_slug)
)
return qs.filter(
Expand Down
23 changes: 14 additions & 9 deletions django_ledger/models/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ def __str__(self):
ledger_str = f'LedgerModel: {self.uuid}'
return f'{ledger_str} | Posted: {self.posted} | Locked: {self.locked}'


def has_wrapped_model_info(self):
if self.additional_info is not None:
return self._WRAPPED_MODEL_KEY in self.additional_info
Expand Down Expand Up @@ -523,7 +522,7 @@ def lock_journal_entries(self, commit: bool = True, **kwargs):
je_model_qs.bulk_update(objs=je_model_qs, fields=['locked', 'updated'])
return je_model_qs

def unlock(self, commit: bool = False, **kwargs):
def unlock(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Un-locks the LedgerModel.
Expand All @@ -532,13 +531,19 @@ def unlock(self, commit: bool = False, **kwargs):
commit: bool
If True, saves the LedgerModel instance instantly. Defaults to False.
"""
if self.can_unlock():
self.locked = False
if commit:
self.save(update_fields=[
'locked',
'updated'
])
if not self.can_unlock():
if raise_exception:
raise LedgerModelValidationError(
message=_(f'Ledger {self.name} cannot be un-locked. UUID: {self.uuid}')
)
return

self.locked = False
if commit:
self.save(update_fields=[
'locked',
'updated'
])

def hide(self, commit: bool = False, raise_exception: bool = True, **kwargs):
if not self.can_hide():
Expand Down
18 changes: 11 additions & 7 deletions django_ledger/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ def lock_ledger(self, commit: bool = False, raise_exception: bool = True, **kwar
if ledger_model.locked:
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already locked...')
ledger_model.lock(commit)
return
ledger_model.lock(commit, raise_exception=raise_exception)

def unlock_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Expand All @@ -501,10 +502,11 @@ def unlock_ledger(self, commit: bool = False, raise_exception: bool = True, **kw
If True, raises ValidationError if LedgerModel already locked.
"""
ledger_model = self.ledger
if not ledger_model.locked:
if not ledger_model.is_locked():
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already unlocked...')
ledger_model.unlock(commit)
return
ledger_model.unlock(commit, raise_exception=raise_exception)

# POST/UNPOST Ledger...
def post_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
Expand All @@ -522,7 +524,8 @@ def post_ledger(self, commit: bool = False, raise_exception: bool = True, **kwar
if ledger_model.posted:
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already posted...')
ledger_model.post(commit)
return
ledger_model.post(commit, raise_exception=raise_exception)

def unpost_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Expand All @@ -536,13 +539,14 @@ def unpost_ledger(self, commit: bool = False, raise_exception: bool = True, **kw
If True, raises ValidationError if LedgerModel already locked.
"""
ledger_model = self.ledger
if not ledger_model.posted:
if not ledger_model.is_posted():
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is not posted...')
ledger_model.post(commit)
return
ledger_model.post(commit, raise_exception=raise_exception)

def migrate_state(self,
# todo: remove usermodel param...
# todo: remove usermodel param...?
user_model,
entity_slug: str,
itemtxs_qs: Optional[QuerySet] = None,
Expand Down
33 changes: 1 addition & 32 deletions django_ledger/models/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,6 @@ class TransactionModelQuerySet(QuerySet):
"""
A custom QuerySet class for TransactionModels implementing methods to effectively and safely read
TransactionModels from the database.
Methods
-------
posted() -> TransactionModelQuerySet:
Fetches a QuerySet of posted transactions only.
for_accounts(account_list: List[str or AccountModel]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
for_roles(role_list: Union[str, List[str]]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
for_unit(unit_slug: Union[str, EntityUnitModel]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a specific EntityUnitModel.
for_activity(activity_list: Union[str, List[str]]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a specific activity or list of activities.
to_date(to_date: Union[str, date, datetime]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a maximum date or timestamp filter.
from_date(from_date: Union[str, date, datetime]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a minimum date or timestamp filter.
not_closing_entry() -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels that are not part of a closing entry.
is_closing_entry() -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels that are part of a closing entry.
"""

def posted(self) -> QuerySet:
Expand Down Expand Up @@ -111,7 +82,7 @@ def for_accounts(self, account_list: List[str or AccountModel]):
TransactionModelQuerySet
Returns a TransactionModelQuerySet with applied filters.
"""
if len(account_list) > 0 and isinstance(account_list[0], str):
if isinstance(account_list, list) > 0 and isinstance(account_list[0], str):
return self.filter(account__code__in=account_list)
return self.filter(account__in=account_list)

Expand Down Expand Up @@ -276,8 +247,6 @@ def for_user(self, user_model) -> TransactionModelQuerySet:
ledger or the user is one of the managers of the entity associated with the transaction's ledger.
"""
qs = self.get_queryset()
if user_model.is_superuser:
return qs
return qs.filter(
Q(journal_entry__ledger__entity__admin=user_model) |
Q(journal_entry__ledger__entity__managers__in=[user_model])
Expand Down
67 changes: 41 additions & 26 deletions django_ledger/views/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,25 +311,20 @@ def get_entity_slug_kwarg(self):
)
return self.ENTITY_SLUG_URL_KWARG

def get_superuser_authorization(self):
return self.AUTHORIZE_SUPERUSER

def has_permission(self):
has_perm = super().has_permission()
if not has_perm:
return False

entity_slug_kwarg = self.get_entity_slug_kwarg()
if self.request.user.is_superuser:
if not self.AUTHORIZE_SUPERUSER:
return False
if entity_slug_kwarg in self.kwargs:
try:
entity_model_qs = self.get_authorized_entity_queryset()
self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
except ObjectDoesNotExist:
return False
return True
elif self.request.user.is_authenticated:
has_perm = super().has_permission()
if not has_perm:
return False
entity_model_qs = self.get_authorized_entity_queryset()

if self.request.user.is_authenticated:
if entity_slug_kwarg in self.kwargs:
try:
entity_model_qs = self.get_authorized_entity_queryset()
self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
except ObjectDoesNotExist:
return False
Expand All @@ -338,7 +333,9 @@ def has_permission(self):

def get_authorized_entity_queryset(self):
return EntityModel.objects.for_user(
user_model=self.request.user).only(
user_model=self.request.user,
authorized_superuser=self.get_superuser_authorization(),
).only(
'uuid', 'slug', 'name', 'default_coa', 'admin')

def get_authorized_entity_instance(self) -> Optional[EntityModel]:
Expand Down Expand Up @@ -372,8 +369,26 @@ def get_context_data(self, **kwargs):


class DigestContextMixIn:
IO_DIGEST = False
IO_DIGEST_EQUITY = False
IO_DIGEST_UNBOUNDED = False
IO_DIGEST_BOUNDED = False

IO_DIGEST_UNBOUNDED_CONTEXT_NAME = 'tx_digest'
IO_MANAGER_UNBOUNDED_CONTEXT_NAME = 'tx_digest_context'

IO_DIGEST_BOUNDED_CONTEXT_NAME = 'equity_digest'
IO_MANAGER_BOUNDED_CONTEXT_NAME = 'equity_digest_context'

def get_io_digest_unbounded_context_name(self):
return self.IO_DIGEST_UNBOUNDED_CONTEXT_NAME

def get_io_manager_unbounded_context_name(self):
return self.IO_MANAGER_UNBOUNDED_CONTEXT_NAME

def get_io_digest_bounded_context_name(self):
return self.IO_DIGEST_BOUNDED_CONTEXT_NAME

def get_io_manager_bounded_context_name(self):
return self.IO_MANAGER_BOUNDED_CONTEXT_NAME

def get_context_data(self, **kwargs):
context = super(DigestContextMixIn, self).get_context_data(**kwargs)
Expand All @@ -385,8 +400,8 @@ def get_io_digest(self,
to_date=None,
**kwargs):

if any([self.IO_DIGEST,
self.IO_DIGEST_EQUITY]):
if any([self.IO_DIGEST_UNBOUNDED,
self.IO_DIGEST_BOUNDED]):

by_period = self.request.GET.get('by_period')
entity_model: EntityModel = self.object
Expand All @@ -401,7 +416,7 @@ def get_io_digest(self,
else:
unit_slug = None

if self.IO_DIGEST:
if self.IO_DIGEST_UNBOUNDED:
io_digest = entity_model.digest(user_model=self.request.user,
to_date=to_date,
unit_slug=unit_slug,
Expand All @@ -410,10 +425,10 @@ def get_io_digest(self,
process_roles=True,
process_groups=True)

context['tx_digest_context'] = io_digest
context['tx_digest'] = io_digest.get_io_data()
context[self.get_io_manager_unbounded_context_name()] = io_digest
context[self.get_io_digest_unbounded_context_name()] = io_digest.get_io_data()

if self.IO_DIGEST_EQUITY:
if self.IO_DIGEST_BOUNDED:
io_digest_equity = entity_model.digest(user_model=self.request.user,
equity_only=True,
to_date=to_date,
Expand All @@ -424,8 +439,8 @@ def get_io_digest(self,
process_roles=False,
process_groups=True)

context['equity_digest_context'] = io_digest_equity
context['equity_digest'] = io_digest_equity.get_io_data()
context[self.get_io_manager_bounded_context_name()] = io_digest_equity
context[self.get_io_digest_bounded_context_name()] = io_digest_equity.get_io_data()

# todo: how is this used??....
context['date_filter'] = to_date
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "django-ledger"
version = "0.6.0.2"
version = "0.6.1"
readme = "README.md"
requires-python = ">=3.10"
description = "Double entry accounting system built on the Django Web Framework."
Expand Down

0 comments on commit 7348491

Please sign in to comment.