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
+17 -6
View File
@@ -8,13 +8,14 @@ from apps.transactions.models import (
RecurringTransaction,
TransactionEntity,
)
from apps.common.admin import SharedObjectModelAdmin
@admin.register(Transaction)
class TransactionModelAdmin(admin.ModelAdmin):
def get_queryset(self, request):
# Use the all_objects manager to show all transactions, including deleted ones
return self.model.all_objects.all()
return self.model.userless_all_objects.all()
list_filter = ["deleted", "type", "is_paid", "date", "account"]
@@ -48,19 +49,29 @@ class TransactionInline(admin.TabularInline):
@admin.register(InstallmentPlan)
class InstallmentPlanAdmin(admin.ModelAdmin):
class InstallmentPlanAdmin(SharedObjectModelAdmin):
inlines = [
TransactionInline,
]
@admin.register(RecurringTransaction)
class RecurringTransactionAdmin(admin.ModelAdmin):
class RecurringTransactionAdmin(SharedObjectModelAdmin):
inlines = [
TransactionInline,
]
admin.site.register(TransactionCategory)
admin.site.register(TransactionTag)
admin.site.register(TransactionEntity)
@admin.register(TransactionCategory)
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
pass
@admin.register(TransactionTag)
class TransactionTagModelAdmin(SharedObjectModelAdmin):
pass
@admin.register(TransactionEntity)
class TransactionEntityModelAdmin(SharedObjectModelAdmin):
pass
+5
View File
@@ -184,3 +184,8 @@ class TransactionsFilter(django_filters.FilterSet):
self.form.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.form.fields["date_start"].widget = AirDatePickerInput()
self.form.fields["date_end"].widget = AirDatePickerInput()
self.form.fields["account"].queryset = Account.objects.all()
self.form.fields["category"].queryset = TransactionCategory.objects.all()
self.form.fields["tags"].queryset = TransactionTag.objects.all()
self.form.fields["entities"].queryset = TransactionEntity.objects.all()
+58 -5
View File
@@ -29,6 +29,7 @@ from apps.transactions.models import (
RecurringTransaction,
TransactionEntity,
)
from apps.common.middleware.thread_local import get_current_user
class TransactionForm(forms.ModelForm):
@@ -94,20 +95,30 @@ class TransactionForm(forms.ModelForm):
# if editing a transaction display non-archived items and it's own item even if it's archived
if self.instance.id:
self.fields["account"].queryset = Account.objects.filter(
Q(is_archived=False) | Q(transactions=self.instance.id)
).distinct()
Q(is_archived=False) | Q(transactions=self.instance.id),
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
).distinct()
)
self.fields["tags"].queryset = TransactionTag.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
).distinct()
)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(transactions=self.instance.id)
).distinct()
)
else:
self.fields["account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.all()
self.helper = FormHelper()
self.helper.form_tag = False
@@ -405,6 +416,24 @@ class TransferForm(forms.Form):
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
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()
from_account = cleaned_data.get("from_account")
@@ -536,6 +565,18 @@ class InstallmentPlanForm(forms.ModelForm):
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(installmentplan=self.instance.id)
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
active=True
)
self.helper = FormHelper()
self.helper.form_tag = False
@@ -781,6 +822,18 @@ class RecurringTransactionForm(forms.ModelForm):
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(recurringtransaction=self.instance.id)
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
active=True
)
self.helper = FormHelper()
self.helper.form_method = "post"
@@ -0,0 +1,62 @@
# Generated by Django 5.1.6 on 2025-03-05 04:19
import django.db.models.deletion
import django.db.models.manager
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0033_transaction_internal_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelManagers(
name='installmentplan',
managers=[
('all_objects', django.db.models.manager.Manager()),
],
),
migrations.AlterModelManagers(
name='recurringtransaction',
managers=[
('all_objects', django.db.models.manager.Manager()),
],
),
migrations.AlterModelManagers(
name='transactioncategory',
managers=[
('all_objects', django.db.models.manager.Manager()),
],
),
migrations.AlterModelManagers(
name='transactionentity',
managers=[
('all_objects', django.db.models.manager.Manager()),
],
),
migrations.AlterModelManagers(
name='transactiontag',
managers=[
('all_objects', django.db.models.manager.Manager()),
],
),
migrations.AddField(
model_name='transactioncategory',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_categories', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AddField(
model_name='transactionentity',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_entities', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AddField(
model_name='transactiontag',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_tags', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
]
@@ -0,0 +1,37 @@
# Generated by Django 5.1.6 on 2025-03-05 04:51
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0034_alter_installmentplan_managers_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='transactioncategory',
name='name',
field=models.CharField(max_length=255, verbose_name='Name'),
),
migrations.AlterField(
model_name='transactiontag',
name='name',
field=models.CharField(max_length=255, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='transactioncategory',
unique_together={('owner', 'name')},
),
migrations.AlterUniqueTogether(
name='transactionentity',
unique_together={('owner', 'name')},
),
migrations.AlterUniqueTogether(
name='transactiontag',
unique_together={('owner', 'name')},
),
]
@@ -0,0 +1,76 @@
# Generated by Django 5.1.6 on 2025-03-06 00:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0035_alter_transactioncategory_name_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelManagers(
name='transactioncategory',
managers=[
],
),
migrations.AlterModelManagers(
name='transactionentity',
managers=[
],
),
migrations.AlterModelManagers(
name='transactiontag',
managers=[
],
),
migrations.AddField(
model_name='transactioncategory',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='transactioncategory',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
),
migrations.AddField(
model_name='transactionentity',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='transactionentity',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
),
migrations.AddField(
model_name='transactiontag',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='transactiontag',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
),
migrations.AlterField(
model_name='transactioncategory',
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.AlterField(
model_name='transactionentity',
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.AlterField(
model_name='transactiontag',
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),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-03-06 01:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0036_alter_transactioncategory_managers_and_more'),
]
operations = [
migrations.AlterField(
model_name='transactioncategory',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
),
migrations.AlterField(
model_name='transactionentity',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
),
migrations.AlterField(
model_name='transactiontag',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 5.1.6 on 2025-03-07 03:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0037_alter_transactioncategory_visibility_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='transaction',
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),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.6 on 2025-03-07 03:16
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0038_transaction_owner'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='transaction',
name='internal_id',
field=models.TextField(blank=True, null=True, verbose_name='Internal ID'),
),
migrations.AlterUniqueTogether(
name='transaction',
unique_together={('owner', 'internal_id')},
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-07 03:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0039_alter_transaction_internal_id_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='transaction',
unique_together=set(),
),
migrations.AlterField(
model_name='transaction',
name='internal_id',
field=models.TextField(blank=True, null=True, unique=True, verbose_name='Internal ID'),
),
]
+99 -11
View File
@@ -15,6 +15,8 @@ from apps.common.functions.decimals import truncate_decimal
from apps.common.templatetags.decimal import localize_number, drop_trailing_zeros
from apps.currencies.utils.convert import convert
from apps.transactions.validators import validate_decimal_places, validate_non_negative
from apps.common.middleware.thread_local import get_current_user
from apps.common.models import SharedObject, SharedObjectManager, OwnedObject
logger = logging.getLogger()
@@ -93,10 +95,40 @@ class SoftDeleteQuerySet(models.QuerySet):
class SoftDeleteManager(models.Manager):
def get_queryset(self):
qs = SoftDeleteQuerySet(self.model, using=self._db)
return qs.filter(deleted=False)
user = get_current_user()
if user and not user.is_anonymous:
return qs.filter(
Q(account__visibility="public")
| Q(account__owner=user)
| Q(account__shared_with=user)
| Q(account__visibility="private", account__owner=None),
deleted=False,
).distinct()
else:
return qs.filter(
deleted=False,
)
class AllObjectsManager(models.Manager):
def get_queryset(self):
user = get_current_user()
if user and not user.is_anonymous:
return (
SoftDeleteQuerySet(self.model, using=self._db)
.filter(
Q(account__visibility="public")
| Q(account__owner=user)
| Q(account__shared_with=user)
| Q(account__visibility="private", account__owner=None),
)
.distinct()
)
else:
return SoftDeleteQuerySet(self.model, using=self._db)
class UserlessAllObjectsManager(models.Manager):
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db)
@@ -104,11 +136,45 @@ class AllObjectsManager(models.Manager):
class DeletedObjectsManager(models.Manager):
def get_queryset(self):
qs = SoftDeleteQuerySet(self.model, using=self._db)
return qs.filter(deleted=True)
user = get_current_user()
if user and not user.is_anonymous:
return qs.filter(
Q(account__visibility="public")
| Q(account__owner=user)
| Q(account__shared_with=user)
| Q(account__visibility="private", account__owner=None),
deleted=True,
).distinct()
else:
return qs.filter(
deleted=True,
)
class TransactionCategory(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
class UserlessDeletedObjectsManager(models.Manager):
def get_queryset(self):
qs = SoftDeleteQuerySet(self.model, using=self._db)
return qs.filter(
deleted=True,
)
class GenericAccountOwnerManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset()
user = get_current_user()
if user and not user.is_anonymous:
return queryset.filter(
Q(account__visibility="public")
| Q(account__owner=user)
| Q(account__shared_with=user)
| Q(account__visibility="private", account__owner=None),
).distinct()
return queryset.none()
class TransactionCategory(SharedObject):
name = models.CharField(max_length=255, verbose_name=_("Name"))
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
active = models.BooleanField(
default=True,
@@ -118,17 +184,21 @@ class TransactionCategory(models.Model):
),
)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
class Meta:
verbose_name = _("Transaction Category")
verbose_name_plural = _("Transaction Categories")
db_table = "t_categories"
unique_together = (("owner", "name"),)
def __str__(self):
return self.name
class TransactionTag(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
class TransactionTag(SharedObject):
name = models.CharField(max_length=255, verbose_name=_("Name"))
active = models.BooleanField(
default=True,
verbose_name=_("Active"),
@@ -137,16 +207,20 @@ class TransactionTag(models.Model):
),
)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
class Meta:
verbose_name = _("Transaction Tags")
verbose_name_plural = _("Transaction Tags")
db_table = "tags"
unique_together = (("owner", "name"),)
def __str__(self):
return self.name
class TransactionEntity(models.Model):
class TransactionEntity(SharedObject):
name = models.CharField(max_length=255, verbose_name=_("Name"))
active = models.BooleanField(
default=True,
@@ -156,16 +230,20 @@ class TransactionEntity(models.Model):
),
)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
class Meta:
verbose_name = _("Entity")
verbose_name_plural = _("Entities")
db_table = "entities"
unique_together = (("owner", "name"),)
def __str__(self):
return self.name
class Transaction(models.Model):
class Transaction(OwnedObject):
class Type(models.TextChoices):
INCOME = "IN", _("Income")
EXPENSE = "EX", _("Expense")
@@ -249,7 +327,11 @@ class Transaction(models.Model):
objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)()
all_objects = AllObjectsManager.from_queryset(SoftDeleteQuerySet)()
userless_all_objects = UserlessAllObjectsManager.from_queryset(SoftDeleteQuerySet)()
deleted_objects = DeletedObjectsManager.from_queryset(SoftDeleteQuerySet)()
userless_deleted_objects = UserlessDeletedObjectsManager.from_queryset(
SoftDeleteQuerySet
)()
class Meta:
verbose_name = _("Transaction")
@@ -386,6 +468,9 @@ class InstallmentPlan(models.Model):
notes = models.TextField(blank=True, verbose_name=_("Notes"))
all_objects = models.Manager() # Unfiltered manager
objects = GenericAccountOwnerManager() # Default filtered manager
class Meta:
verbose_name = _("Installment Plan")
verbose_name_plural = _("Installment Plans")
@@ -440,7 +525,7 @@ class InstallmentPlan(models.Model):
transaction_date = self.start_date + delta
transaction_reference_date = (self.reference_date + delta).replace(day=1)
new_transaction = Transaction.objects.create(
new_transaction = Transaction.all_objects.create(
account=self.account,
type=self.type,
date=transaction_date,
@@ -500,7 +585,7 @@ class InstallmentPlan(models.Model):
existing_transaction.entities.set(self.entities.all())
else:
# If the transaction doesn't exist, create a new one
new_transaction = Transaction.objects.create(
new_transaction = Transaction.all_objects.create(
account=self.account,
type=self.type,
date=transaction_date,
@@ -587,6 +672,9 @@ class RecurringTransaction(models.Model):
verbose_name=_("Last Generated Reference Date"), null=True, blank=True
)
all_objects = models.Manager() # Unfiltered manager
objects = GenericAccountOwnerManager() # Default filtered manager
class Meta:
verbose_name = _("Recurring Transaction")
verbose_name_plural = _("Recurring Transactions")
@@ -624,7 +712,7 @@ class RecurringTransaction(models.Model):
)
def create_transaction(self, date, reference_date):
created_transaction = Transaction.objects.create(
created_transaction = Transaction.all_objects.create(
account=self.account,
type=self.type,
date=date,
+4 -2
View File
@@ -34,7 +34,7 @@ def cleanup_deleted_transactions(timestamp=None):
if not settings.ENABLE_SOFT_DELETE:
# Hard delete all soft-deleted transactions
deleted_count, _ = Transaction.deleted_objects.all().hard_delete()
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
return (
f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
)
@@ -47,7 +47,9 @@ def cleanup_deleted_transactions(timestamp=None):
invalidate()
# Hard delete soft-deleted transactions older than the cutoff date
old_transactions = Transaction.deleted_objects.filter(deleted_at__lt=cutoff_date)
old_transactions = Transaction.userless_deleted_objects.filter(
deleted_at__lt=cutoff_date
)
deleted_count, _ = old_transactions.hard_delete()
return f"Hard deleted {deleted_count} objects older than {settings.KEEP_DELETED_TRANSACTIONS_FOR} days."
+30
View File
@@ -131,6 +131,16 @@ urlpatterns = [
views.tag_delete,
name="tag_delete",
),
path(
"tags/<int:tag_id>/take-ownership/",
views.tag_take_ownership,
name="tag_take_ownership",
),
path(
"tags/<int:pk>/share/",
views.tag_share,
name="tag_share_settings",
),
path("entities/", views.entities_index, name="entities_index"),
path("entities/list/", views.entities_list, name="entities_list"),
path(
@@ -154,6 +164,16 @@ urlpatterns = [
views.entity_delete,
name="entity_delete",
),
path(
"entities/<int:entity_id>/take-ownership/",
views.entity_take_ownership,
name="entity_take_ownership",
),
path(
"entities/<int:pk>/share/",
views.entity_share,
name="entity_share_settings",
),
path("categories/", views.categories_index, name="categories_index"),
path("categories/list/", views.categories_list, name="categories_list"),
path(
@@ -177,6 +197,16 @@ urlpatterns = [
views.category_delete,
name="category_delete",
),
path(
"categories/<int:pk>/share/",
views.category_share,
name="category_share_settings",
),
path(
"categories/<int:category_id>/take-ownership/",
views.category_take_ownership,
name="category_take_ownership",
),
path(
"installment-plans/",
views.installment_plans_index,
+77 -3
View File
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.transactions.forms import TransactionCategoryForm
from apps.transactions.models import TransactionCategory
from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm
@login_required
@@ -85,6 +87,16 @@ def category_add(request, **kwargs):
def category_edit(request, category_id):
category = get_object_or_404(TransactionCategory, id=category_id)
if category.owner and category.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 = TransactionCategoryForm(request.POST, instance=category)
if form.is_valid():
@@ -107,15 +119,77 @@ def category_edit(request, category_id):
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def category_share(request, pk):
obj = get_object_or_404(TransactionCategory, 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,
"categories/fragments/share.html",
{"form": form, "object": obj},
)
@only_htmx
@login_required
@require_http_methods(["DELETE"])
def category_delete(request, category_id):
category = get_object_or_404(TransactionCategory, id=category_id)
category.delete()
messages.success(request, _("Category deleted successfully"))
if category.owner != request.user and request.user in category.shared_with.all():
category.shared_with.remove(request.user)
messages.success(request, _("Item no longer shared with you"))
else:
category.delete()
messages.success(request, _("Category deleted successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def category_take_ownership(request, category_id):
category = get_object_or_404(TransactionCategory, id=category_id)
if not category.owner:
category.owner = request.user
category.visibility = SharedObject.Visibility.private
category.save()
messages.success(request, _("Ownership taken successfully"))
return HttpResponse(
status=204,
+77 -3
View File
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.transactions.forms import TransactionEntityForm
from apps.transactions.models import TransactionEntity
from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm
@login_required
@@ -85,6 +87,16 @@ def entity_add(request, **kwargs):
def entity_edit(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
if entity.owner and entity.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 = TransactionEntityForm(request.POST, instance=entity)
if form.is_valid():
@@ -113,9 +125,12 @@ def entity_edit(request, entity_id):
def entity_delete(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
entity.delete()
messages.success(request, _("Entity deleted successfully"))
if entity.owner != request.user and request.user in entity.shared_with.all():
entity.shared_with.remove(request.user)
messages.success(request, _("Item no longer shared with you"))
else:
entity.delete()
messages.success(request, _("Entity deleted successfully"))
return HttpResponse(
status=204,
@@ -123,3 +138,62 @@ def entity_delete(request, entity_id):
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def entity_take_ownership(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
if not entity.owner:
entity.owner = request.user
entity.visibility = SharedObject.Visibility.private
entity.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 entity_share(request, pk):
obj = get_object_or_404(TransactionEntity, 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,
"entities/fragments/share.html",
{"form": form, "object": obj},
)
+77 -3
View File
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.transactions.forms import TransactionTagForm
from apps.transactions.models import TransactionTag
from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm
@login_required
@@ -85,6 +87,16 @@ def tag_add(request, **kwargs):
def tag_edit(request, tag_id):
tag = get_object_or_404(TransactionTag, id=tag_id)
if tag.owner and tag.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 = TransactionTagForm(request.POST, instance=tag)
if form.is_valid():
@@ -113,9 +125,12 @@ def tag_edit(request, tag_id):
def tag_delete(request, tag_id):
tag = get_object_or_404(TransactionTag, id=tag_id)
tag.delete()
messages.success(request, _("Tag deleted successfully"))
if tag.owner != request.user and request.user in tag.shared_with.all():
tag.shared_with.remove(request.user)
messages.success(request, _("Item no longer shared with you"))
else:
tag.delete()
messages.success(request, _("Tag deleted successfully"))
return HttpResponse(
status=204,
@@ -123,3 +138,62 @@ def tag_delete(request, tag_id):
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def tag_take_ownership(request, tag_id):
tag = get_object_or_404(TransactionTag, id=tag_id)
if not tag.owner:
tag.owner = request.user
tag.visibility = SharedObject.Visibility.private
tag.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 tag_share(request, pk):
obj = get_object_or_404(TransactionTag, 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,
"tags/fragments/share.html",
{"form": form, "object": obj},
)