feat: add Transaction Entity

This commit is contained in:
Herculino Trotta
2024-11-30 17:12:35 -03:00
parent 28c8a961bc
commit 2382abf3c0
21 changed files with 517 additions and 11 deletions

View File

@@ -2,7 +2,11 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from apps.transactions.models import TransactionCategory, TransactionTag
from apps.transactions.models import (
TransactionCategory,
TransactionTag,
TransactionEntity,
)
@extend_schema_field(
@@ -67,3 +71,27 @@ class TransactionTagField(serializers.Field):
)
tags.append(tag)
return tags
class TransactionEntityField(serializers.Field):
def to_representation(self, value):
return [{"id": entity.id, "name": entity.name} for entity in value.all()]
def to_internal_value(self, data):
entities = []
for item in data:
if isinstance(item, int):
try:
entity = TransactionEntity.objects.get(pk=item)
except TransactionTag.DoesNotExist:
raise serializers.ValidationError(
f"Entity with ID {item} does not exist."
)
elif isinstance(item, str):
entity, created = TransactionEntity.objects.get_or_create(name=item)
else:
raise serializers.ValidationError(
"Invalid entity data. Provide an ID or name."
)
entities.append(entity)
return entities

View File

@@ -7,17 +7,21 @@ from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated
from apps.accounts.models import Account
from apps.api.fields.transactions import TransactionTagField, TransactionCategoryField
from apps.api.fields.transactions import (
TransactionTagField,
TransactionCategoryField,
TransactionEntityField,
)
from apps.api.serializers.accounts import AccountSerializer
from apps.transactions.models import (
Transaction,
TransactionCategory,
TransactionTag,
InstallmentPlan,
TransactionEntity,
)
# Create serializers for other related models as needed
class TransactionCategorySerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
@@ -34,6 +38,14 @@ class TransactionTagSerializer(serializers.ModelSerializer):
fields = "__all__"
class TransactionEntitySerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
class Meta:
model = TransactionEntity
fields = "__all__"
class InstallmentPlanSerializer(serializers.ModelSerializer):
permission_classes = [IsAuthenticated]
@@ -45,6 +57,7 @@ class InstallmentPlanSerializer(serializers.ModelSerializer):
class TransactionSerializer(serializers.ModelSerializer):
category = TransactionCategoryField(required=False)
tags = TransactionTagField(required=False)
entities = TransactionEntityField(required=False)
exchanged_amount = serializers.SerializerMethodField()
@@ -86,17 +99,24 @@ class TransactionSerializer(serializers.ModelSerializer):
def create(self, validated_data):
tags = validated_data.pop("tags", [])
entities = validated_data.pop("entities", [])
transaction = Transaction.objects.create(**validated_data)
transaction.tags.set(tags)
transaction.entities.set(entities)
return transaction
def update(self, instance, validated_data):
tags = validated_data.pop("tags", None)
entities = validated_data.pop("entities", None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
if tags is not None:
instance.tags.set(tags)
if entities is not None:
instance.entities.set(entities)
return instance
@staticmethod

View File

@@ -7,6 +7,7 @@ router = routers.DefaultRouter()
router.register(r"transactions", views.TransactionViewSet)
router.register(r"categories", views.TransactionCategoryViewSet)
router.register(r"tags", views.TransactionTagViewSet)
router.register(r"entities", views.TransactionEntityViewSet)
router.register(r"installment-plans", views.InstallmentPlanViewSet)
router.register(r"account-groups", views.AccountGroupViewSet)
router.register(r"accounts", views.AccountViewSet)

View File

@@ -5,12 +5,14 @@ from apps.api.serializers import (
TransactionCategorySerializer,
TransactionTagSerializer,
InstallmentPlanSerializer,
TransactionEntitySerializer,
)
from apps.transactions.models import (
Transaction,
TransactionCategory,
TransactionTag,
InstallmentPlan,
TransactionEntity,
)
from apps.rules.signals import transaction_updated, transaction_created
@@ -42,6 +44,11 @@ class TransactionTagViewSet(viewsets.ModelViewSet):
serializer_class = TransactionTagSerializer
class TransactionEntityViewSet(viewsets.ModelViewSet):
queryset = TransactionEntity.objects.all()
serializer_class = TransactionEntitySerializer
class InstallmentPlanViewSet(viewsets.ModelViewSet):
queryset = InstallmentPlan.objects.all()
serializer_class = InstallmentPlanSerializer

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2024-11-30 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0003_alter_transactionruleaction_unique_together'),
]
operations = [
migrations.AlterField(
model_name='transactionruleaction',
name='field',
field=models.CharField(choices=[('account', 'Account'), ('type', 'Type'), ('is_paid', 'Paid'), ('date', 'Date'), ('reference_date', 'Reference Date'), ('amount', 'Amount'), ('description', 'Description'), ('notes', 'Notes'), ('category', 'Category'), ('tags', 'Tags'), ('entities', 'Entities')], max_length=50, verbose_name='Field'),
),
]

View File

@@ -26,6 +26,7 @@ class TransactionRuleAction(models.Model):
notes = "notes", _("Notes")
category = "category", _("Category")
tags = "tags", _("Tags")
entities = "entities", _("Entities")
rule = models.ForeignKey(
TransactionRule,

View File

@@ -5,7 +5,12 @@ from simpleeval import EvalWithCompoundTypes
from apps.accounts.models import Account
from apps.rules.models import TransactionRule, TransactionRuleAction
from apps.transactions.models import Transaction, TransactionCategory, TransactionTag
from apps.transactions.models import (
Transaction,
TransactionCategory,
TransactionTag,
TransactionEntity,
)
@app.task
@@ -32,6 +37,8 @@ def check_for_transaction_rules(
"category_id": instance.category.id if instance.category else None,
"tag_names": [x.name for x in instance.tags.all()],
"tag_ids": [x.id for x in instance.tags.all()],
"entities_names": [x.name for x in instance.entities.all()],
"entities_ids": [x.id for x in instance.entities.all()],
"is_expense": instance.type == Transaction.Type.EXPENSE,
"is_income": instance.type == Transaction.Type.INCOME,
"is_paid": instance.is_paid,
@@ -112,4 +119,31 @@ def check_for_transaction_rules(
instance.tags.add(tag)
elif action.field == TransactionRuleAction.Field.entities:
value = simple.eval(action.value)
if isinstance(value, list):
# Clear existing entities
instance.entities.clear()
for entity_value in value:
if isinstance(entity_value, int):
entity = TransactionEntity.objects.get(
id=entity_value
)
instance.entities.add(entity)
elif isinstance(entity_value, str):
entity = TransactionEntity.objects.get(
name=entity_value
)
instance.entities.add(entity)
elif isinstance(value, (int, str)):
# If a single value is provided, treat it as a single entity
instance.entities.clear()
if isinstance(value, int):
entity = TransactionEntity.objects.get(id=value)
else:
entity = TransactionEntity.objects.get(name=value)
instance.entities.add(entity)
instance.save()

View File

@@ -6,6 +6,7 @@ from apps.transactions.models import (
TransactionTag,
InstallmentPlan,
RecurringTransaction,
TransactionEntity,
)
@@ -43,3 +44,4 @@ class RecurringTransactionAdmin(admin.ModelAdmin):
admin.site.register(TransactionCategory)
admin.site.register(TransactionTag)
admin.site.register(TransactionEntity)

View File

@@ -7,9 +7,12 @@ from django.utils.translation import gettext_lazy as _
from django_filters import Filter
from apps.accounts.models import Account
from apps.transactions.models import Transaction
from apps.transactions.models import TransactionCategory
from apps.transactions.models import TransactionTag
from apps.transactions.models import (
Transaction,
TransactionCategory,
TransactionTag,
TransactionEntity,
)
from apps.common.widgets.tom_select import TomSelectMultiple
from apps.common.fields.month_year import MonthYearFormField
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
@@ -62,6 +65,13 @@ class TransactionsFilter(django_filters.FilterSet):
label=_("Tags"),
widget=TomSelectMultiple(checkboxes=True, remove_button=True),
)
entities = django_filters.ModelMultipleChoiceFilter(
field_name="entities__name",
queryset=TransactionEntity.objects.all(),
to_field_name="name",
label=_("Entities"),
widget=TomSelectMultiple(checkboxes=True, remove_button=True),
)
is_paid = django_filters.MultipleChoiceFilter(
choices=SITUACAO_CHOICES,
field_name="is_paid",
@@ -159,6 +169,7 @@ class TransactionsFilter(django_filters.FilterSet):
Field("account", size=1),
Field("category", size=1),
Field("tags", size=1),
Field("entities", size=1),
)
self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()

View File

@@ -25,6 +25,7 @@ from apps.transactions.models import (
TransactionTag,
InstallmentPlan,
RecurringTransaction,
TransactionEntity,
)
from apps.rules.signals import transaction_created, transaction_updated
@@ -42,6 +43,13 @@ class TransactionForm(forms.ModelForm):
required=False,
label=_("Tags"),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("Account"),
@@ -62,6 +70,7 @@ class TransactionForm(forms.ModelForm):
"notes",
"category",
"tags",
"entities",
]
widgets = {
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
@@ -81,7 +90,11 @@ class TransactionForm(forms.ModelForm):
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Switch("is_paid"),
"account",
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
Row(
Column("date", css_class="form-group col-md-6 mb-0"),
Column("reference_date", css_class="form-group col-md-6 mb-0"),
@@ -351,6 +364,13 @@ class InstallmentPlanForm(forms.ModelForm):
required=False,
label=_("Category"),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -369,6 +389,7 @@ class InstallmentPlanForm(forms.ModelForm):
"tags",
"notes",
"installment_start",
"entities",
]
widgets = {
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
@@ -389,7 +410,11 @@ class InstallmentPlanForm(forms.ModelForm):
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
"account",
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"description",
"notes",
Row(
@@ -474,6 +499,38 @@ class TransactionTagForm(forms.ModelForm):
)
class TransactionEntityForm(forms.ModelForm):
class Meta:
model = TransactionEntity
fields = ["name"]
labels = {"name": _("Entity name")}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(Field("name", css_class="mb-3"))
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit(
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit(
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
),
),
)
class TransactionCategoryForm(forms.ModelForm):
class Meta:
model = TransactionCategory
@@ -527,6 +584,13 @@ class RecurringTransactionForm(forms.ModelForm):
required=False,
label=_("Category"),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
@@ -568,7 +632,11 @@ class RecurringTransactionForm(forms.ModelForm):
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
"account",
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"description",
"amount",
Row(

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.1.3 on 2024-11-30 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0022_rename_paused_recurringtransaction_is_paused'),
]
operations = [
migrations.CreateModel(
name='TransactionEntity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
],
options={
'verbose_name': 'Entity',
'verbose_name_plural': 'Entities',
'db_table': 'entities',
},
),
migrations.AddField(
model_name='transaction',
name='entities',
field=models.ManyToManyField(blank=True, help_text='Payees/Payers involved in this transaction', related_name='transactions', to='transactions.transactionentity', verbose_name='Entities'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.3 on 2024-11-30 20:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0023_transactionentity_transaction_entities'),
]
operations = [
migrations.AddField(
model_name='installmentplan',
name='entities',
field=models.ManyToManyField(blank=True, to='transactions.transactionentity', verbose_name='Entities'),
),
migrations.AddField(
model_name='recurringtransaction',
name='entities',
field=models.ManyToManyField(blank=True, to='transactions.transactionentity', verbose_name='Entities'),
),
migrations.AlterField(
model_name='transaction',
name='entities',
field=models.ManyToManyField(blank=True, related_name='transactions', to='transactions.transactionentity', verbose_name='Entities'),
),
]

View File

@@ -40,6 +40,20 @@ class TransactionTag(models.Model):
return self.name
class TransactionEntity(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"))
# Add any other fields you might want for entities
class Meta:
verbose_name = _("Entity")
verbose_name_plural = _("Entities")
db_table = "entities"
def __str__(self):
return self.name
class Transaction(models.Model):
class Type(models.TextChoices):
INCOME = "IN", _("Income")
@@ -77,7 +91,17 @@ class Transaction(models.Model):
blank=True,
null=True,
)
tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True)
tags = models.ManyToManyField(
TransactionTag,
verbose_name=_("Tags"),
blank=True,
)
entities = models.ManyToManyField(
TransactionEntity,
verbose_name=_("Entities"),
blank=True,
related_name="transactions",
)
installment_plan = models.ForeignKey(
"InstallmentPlan",
@@ -185,6 +209,12 @@ class InstallmentPlan(models.Model):
verbose_name=_("Category"),
)
tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True)
entities = models.ManyToManyField(
TransactionEntity,
verbose_name=_("Entities"),
blank=True,
)
notes = models.TextField(blank=True, verbose_name=_("Notes"))
class Meta:
@@ -255,6 +285,7 @@ class InstallmentPlan(models.Model):
notes=self.notes,
)
new_transaction.tags.set(self.tags.all())
new_transaction.entities.set(self.entities.all())
@transaction.atomic
def update_transactions(self):
@@ -292,6 +323,7 @@ class InstallmentPlan(models.Model):
# Update tags
existing_transaction.tags.set(self.tags.all())
existing_transaction.entities.set(self.entities.all())
else:
# If the transaction doesn't exist, create a new one
new_transaction = Transaction.objects.create(
@@ -308,6 +340,7 @@ class InstallmentPlan(models.Model):
notes=self.notes,
)
new_transaction.tags.set(self.tags.all())
new_transaction.entities.set(self.entities.all())
# Remove any extra transactions that are no longer part of the plan
self.transactions.filter(
@@ -353,6 +386,11 @@ class RecurringTransaction(models.Model):
null=True,
)
tags = models.ManyToManyField(TransactionTag, verbose_name=_("Tags"), blank=True)
entities = models.ManyToManyField(
TransactionEntity,
verbose_name=_("Entities"),
blank=True,
)
notes = models.TextField(blank=True, verbose_name=_("Notes"))
reference_date = models.DateField(
verbose_name=_("Reference Date"), null=True, blank=True
@@ -426,6 +464,8 @@ class RecurringTransaction(models.Model):
)
if self.tags.exists():
created_transaction.tags.set(self.tags.all())
if self.entities.exists():
created_transaction.entities.set(self.entities.all())
def get_recurrence_delta(self):
if self.recurrence_type == self.RecurrenceType.DAY:

View File

@@ -59,6 +59,19 @@ urlpatterns = [
views.tag_delete,
name="tag_delete",
),
path("entities/", views.entities_index, name="entities_index"),
path("entities/list/", views.entities_list, name="entities_list"),
path("entities/add/", views.entity_add, name="entity_add"),
path(
"entities/<int:entity_id>/edit/",
views.entity_edit,
name="entity_edit",
),
path(
"entities/<int:entity_id>/delete/",
views.entity_delete,
name="entity_delete",
),
path("categories/", views.categories_index, name="categories_index"),
path("categories/list/", views.categories_list, name="categories_list"),
path("categories/add/", views.category_add, name="category_add"),

View File

@@ -1,5 +1,6 @@
from .transactions import *
from .tags import *
from .entities import *
from .categories import *
from .actions import *
from .installment_plans import *

View File

@@ -0,0 +1,105 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
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
@login_required
@require_http_methods(["GET"])
def entities_index(request):
return render(
request,
"entities/pages/index.html",
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def entities_list(request):
entities = TransactionEntity.objects.all().order_by("id")
return render(
request,
"entities/fragments/list.html",
{"entities": entities},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def entity_add(request, **kwargs):
if request.method == "POST":
form = TransactionEntityForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Entity added successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = TransactionEntityForm()
return render(
request,
"entities/fragments/add.html",
{"form": form},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def entity_edit(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
if request.method == "POST":
form = TransactionEntityForm(request.POST, instance=entity)
if form.is_valid():
form.save()
messages.success(request, _("Entity updated successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = TransactionEntityForm(instance=entity)
return render(
request,
"entities/fragments/edit.html",
{"form": form, "entity": entity},
)
@only_htmx
@login_required
@csrf_exempt
@require_http_methods(["DELETE"])
def entity_delete(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
entity.delete()
messages.success(request, _("Entity deleted successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)

View File

@@ -41,6 +41,15 @@
{% endspaceless %}
</div>
<div class="tw-text-gray-400 tw-text-sm">
{# Entities #}
{% with transaction.entities.all as entities %}
{% if entities %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ entities|join:", " }}</div>
</div>
{% endif %}
{% endwith %}
{# Notes#}
{% if transaction.notes %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">

View File

@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add entity' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'entity_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit entity' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'entity_edit' entity_id=entity.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Entities' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'entity_add' %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3 table-responsive">
{% if entities %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr class="entity">
<td class="col-auto">
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
<a class="btn btn-secondary btn-sm"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'entity_edit' entity_id=entity.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm text-danger"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'entity_delete' entity_id=entity.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col">{{ entity.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<c-msg.empty title="{% translate "No entities" %}" remove-padding></c-msg.empty>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,8 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Entities' %}{% endblock %}
{% block content %}
<div hx-get="{% url 'entities_list' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
{% endblock %}