feat: multi tenancy support

This commit is contained in:
Herculino Trotta
2025-03-08 12:03:17 -03:00
parent c7d70a1748
commit 020dd74f80
79 changed files with 2401 additions and 399 deletions

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.1.6 on 2025-03-07 02:46
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0011_alter_updateorcreatetransactionruleaction_set_is_paid'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='transactionrule',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='transactionrule',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='transactionrule',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
),
]

View File

@@ -2,8 +2,10 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from apps.common.models import SharedObject, SharedObjectManager
class TransactionRule(models.Model):
class TransactionRule(SharedObject):
active = models.BooleanField(default=True)
on_update = models.BooleanField(default=False)
on_create = models.BooleanField(default=True)
@@ -11,6 +13,9 @@ class TransactionRule(models.Model):
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
trigger = models.TextField(verbose_name=_("Trigger"))
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
class Meta:
verbose_name = _("Transaction rule")
verbose_name_plural = _("Transaction rules")

View File

@@ -6,6 +6,7 @@ from apps.transactions.models import (
transaction_updated,
)
from apps.rules.tasks import check_for_transaction_rules
from apps.common.middleware.thread_local import get_current_user
@receiver(transaction_created)
@@ -20,6 +21,7 @@ def transaction_changed_receiver(sender: Transaction, signal, **kwargs):
check_for_transaction_rules.defer(
instance_id=sender.id,
user_id=get_current_user().id,
signal=(
"transaction_created"
if signal is transaction_created

View File

@@ -4,6 +4,7 @@ from datetime import datetime, date
from cachalot.api import cachalot_disabled
from dateutil.relativedelta import relativedelta
from django.contrib.auth import get_user_model
from procrastinate.contrib.django import app
from simpleeval import EvalWithCompoundTypes
@@ -18,6 +19,7 @@ from apps.transactions.models import (
TransactionTag,
TransactionEntity,
)
from apps.common.middleware.thread_local import write_current_user, delete_current_user
logger = logging.getLogger(__name__)
@@ -25,8 +27,12 @@ logger = logging.getLogger(__name__)
@app.task(name="check_for_transaction_rules")
def check_for_transaction_rules(
instance_id: int,
user_id: int,
signal,
):
user = get_user_model().objects.get(id=user_id)
write_current_user(user)
try:
with cachalot_disabled():
instance = Transaction.objects.get(id=instance_id)
@@ -91,8 +97,11 @@ def check_for_transaction_rules(
"Error while executing 'check_for_transaction_rules' task",
exc_info=True,
)
delete_current_user()
raise e
delete_current_user()
def _get_names(instance):
return {

View File

@@ -37,6 +37,16 @@ urlpatterns = [
views.transaction_rule_delete,
name="transaction_rule_delete",
),
path(
"rules/transaction/<int:transaction_rule_id>/take-ownership/",
views.transaction_rule_take_ownership,
name="transaction_rule_take_ownership",
),
path(
"rules/transaction/<int:pk>/share/",
views.transaction_rule_share,
name="transaction_rule_share_settings",
),
path(
"rules/transaction/<int:transaction_rule_id>/transaction-action/add/",
views.transaction_rule_action_add,

View File

@@ -16,6 +16,8 @@ from apps.rules.models import (
TransactionRuleAction,
UpdateOrCreateTransactionRuleAction,
)
from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm
@login_required
@@ -93,6 +95,16 @@ def transaction_rule_add(request, **kwargs):
def transaction_rule_edit(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
if transaction_rule.owner and transaction_rule.owner != request.user:
messages.error(request, _("Only the owner can edit this"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
if request.method == "POST":
form = TransactionRuleForm(request.POST, instance=transaction_rule)
if form.is_valid():
@@ -134,9 +146,15 @@ def transaction_rule_view(request, transaction_rule_id):
def transaction_rule_delete(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
transaction_rule.delete()
messages.success(request, _("Rule deleted successfully"))
if (
transaction_rule.owner != request.user
and request.user in transaction_rule.shared_with.all()
):
transaction_rule.shared_with.remove(request.user)
messages.success(request, _("Item no longer shared with you"))
else:
transaction_rule.delete()
messages.success(request, _("Rule deleted successfully"))
return HttpResponse(
status=204,
@@ -146,6 +164,65 @@ def transaction_rule_delete(request, transaction_rule_id):
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def transaction_rule_take_ownership(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
if not transaction_rule.owner:
transaction_rule.owner = request.user
transaction_rule.visibility = SharedObject.Visibility.private
transaction_rule.save()
messages.success(request, _("Ownership taken successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def transaction_rule_share(request, pk):
obj = get_object_or_404(TransactionRule, id=pk)
if obj.owner and obj.owner != request.user:
messages.error(request, _("Only the owner can edit this"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
if form.is_valid():
form.save()
messages.success(request, _("Configuration saved successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = SharedObjectForm(instance=obj, user=request.user)
return render(
request,
"rules/fragments/share.html",
{"form": form, "object": obj},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])