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
+8 -2
View File
@@ -1,7 +1,13 @@
from django.contrib import admin
from apps.dca.models import DCAStrategy, DCAEntry
from apps.common.admin import SharedObjectModelAdmin
# Register your models here.
admin.site.register(DCAStrategy)
admin.site.register(DCAEntry)
@admin.register(DCAStrategy)
class TransactionEntityModelAdmin(SharedObjectModelAdmin):
def get_queryset(self, request):
return DCAStrategy.all_objects.all()
+20 -2
View File
@@ -168,7 +168,7 @@ class DCAEntryForm(forms.ModelForm):
Row(
Column(
"from_account",
css_class="form-group col-md-6 mb-0",
css_class="form-group",
),
css_class="form-row",
),
@@ -190,7 +190,7 @@ class DCAEntryForm(forms.ModelForm):
Row(
Column(
"to_account",
css_class="form-group col-md-6 mb-0",
css_class="form-group",
),
css_class="form-row",
),
@@ -266,6 +266,24 @@ class DCAEntryForm(forms.ModelForm):
id=expense_transaction.id
)
self.fields["from_account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["from_category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["from_tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["to_account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["to_category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["to_tags"].queryset = TransactionTag.objects.filter(active=True)
def clean(self):
cleaned_data = super().clean()
@@ -0,0 +1,31 @@
# Generated by Django 5.1.6 on 2025-03-07 18:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dca', '0002_alter_dcaentry_amount_paid_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='dcastrategy',
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='dcastrategy',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='dcastrategy',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
),
]
+5 -3
View File
@@ -1,16 +1,15 @@
from datetime import timedelta
from decimal import Decimal
from statistics import mean, stdev
from django.db import models
from django.template.defaultfilters import date
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from apps.common.models import SharedObject, SharedObjectManager
from apps.currencies.utils.convert import convert, get_exchange_rate
class DCAStrategy(models.Model):
class DCAStrategy(SharedObject):
name = models.CharField(max_length=255, verbose_name=_("Name"))
target_currency = models.ForeignKey(
"currencies.Currency",
@@ -28,6 +27,9 @@ class DCAStrategy(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
class Meta:
verbose_name = _("DCA Strategy")
verbose_name_plural = _("DCA Strategies")
+10
View File
@@ -12,6 +12,16 @@ urlpatterns = [
views.strategy_delete,
name="dca_strategy_delete",
),
path(
"dca/<int:strategy_id>/take-ownership/",
views.strategy_take_ownership,
name="dca_strategy_take_ownership",
),
path(
"dca/<int:pk>/share/",
views.strategy_share,
name="dca_strategy_share_settings",
),
path(
"dca/<int:strategy_id>/",
views.strategy_detail_index,
+80 -3
View File
@@ -11,6 +11,8 @@ from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
from apps.dca.models import DCAStrategy, DCAEntry
from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm
@login_required
@@ -57,6 +59,16 @@ def strategy_add(request):
def strategy_edit(request, strategy_id):
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
if dca_strategy.owner and dca_strategy.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 = DCAStrategyForm(request.POST, instance=dca_strategy)
if form.is_valid():
@@ -85,9 +97,15 @@ def strategy_edit(request, strategy_id):
def strategy_delete(request, strategy_id):
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
dca_strategy.delete()
messages.success(request, _("DCA strategy deleted successfully"))
if (
dca_strategy.owner != request.user
and request.user in dca_strategy.shared_with.all()
):
dca_strategy.shared_with.remove(request.user)
messages.success(request, _("Item no longer shared with you"))
else:
dca_strategy.delete()
messages.success(request, _("DCA strategy deleted successfully"))
return HttpResponse(
status=204,
@@ -97,6 +115,65 @@ def strategy_delete(request, strategy_id):
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def strategy_take_ownership(request, strategy_id):
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
if not dca_strategy.owner:
dca_strategy.owner = request.user
dca_strategy.visibility = SharedObject.Visibility.private
dca_strategy.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 strategy_share(request, pk):
obj = get_object_or_404(DCAStrategy, 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,
"dca/fragments/strategy/share.html",
{"form": form, "object": obj},
)
@login_required
def strategy_detail_index(request, strategy_id):
strategy = get_object_or_404(DCAStrategy, id=strategy_id)