diff --git a/app/apps/currencies/forms.py b/app/apps/currencies/forms.py index b16ab66..10369c8 100644 --- a/app/apps/currencies/forms.py +++ b/app/apps/currencies/forms.py @@ -6,7 +6,8 @@ from django.forms import CharField from django.utils.translation import gettext_lazy as _ from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.currencies.models import Currency +from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput +from apps.currencies.models import Currency, ExchangeRate class CurrencyForm(forms.ModelForm): @@ -43,3 +44,42 @@ class CurrencyForm(forms.ModelForm): ), ), ) + + +class ExchangeRateForm(forms.ModelForm): + date = forms.DateTimeField( + widget=forms.DateTimeInput( + attrs={"type": "datetime-local"}, format="%Y-%m-%dT%H:%M" + ) + ) + + class Meta: + model = ExchangeRate + fields = ["from_currency", "to_currency", "rate", "date"] + + 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("date", "from_currency", "to_currency", "rate") + + self.fields["rate"].widget = ArbitraryDecimalDisplayNumberInput() + + 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" + ), + ), + ) diff --git a/app/apps/currencies/urls.py b/app/apps/currencies/urls.py index 9f83c45..1ebe84d 100644 --- a/app/apps/currencies/urls.py +++ b/app/apps/currencies/urls.py @@ -16,4 +16,17 @@ urlpatterns = [ views.currency_delete, name="currency_delete", ), + path("exchange-rates/", views.exchange_rates_index, name="exchange_rates_index"), + path("exchange-rates/list/", views.exchange_rates_list, name="exchange_rates_list"), + path("exchange-rates/add/", views.exchange_rate_add, name="exchange_rate_add"), + path( + "exchange-rates//edit/", + views.exchange_rate_edit, + name="exchange_rate_edit", + ), + path( + "exchange-rates//delete/", + views.exchange_rate_delete, + name="exchange_rate_delete", + ), ] diff --git a/app/apps/currencies/views/__init__.py b/app/apps/currencies/views/__init__.py new file mode 100644 index 0000000..6b013c5 --- /dev/null +++ b/app/apps/currencies/views/__init__.py @@ -0,0 +1,2 @@ +from .currencies import * +from .exchange_rates import * diff --git a/app/apps/currencies/views.py b/app/apps/currencies/views/currencies.py similarity index 100% rename from app/apps/currencies/views.py rename to app/apps/currencies/views/currencies.py diff --git a/app/apps/currencies/views/exchange_rates.py b/app/apps/currencies/views/exchange_rates.py new file mode 100644 index 0000000..f989fe6 --- /dev/null +++ b/app/apps/currencies/views/exchange_rates.py @@ -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.currencies.forms import ExchangeRateForm +from apps.currencies.models import ExchangeRate + + +@login_required +@require_http_methods(["GET"]) +def exchange_rates_index(request): + return render( + request, + "exchange_rates/pages/index.html", + ) + + +@only_htmx +@login_required +@require_http_methods(["GET"]) +def exchange_rates_list(request): + exchange_rates = ExchangeRate.objects.all().order_by("-date") + return render( + request, + "exchange_rates/fragments/list.html", + {"exchange_rates": exchange_rates}, + ) + + +@only_htmx +@login_required +@require_http_methods(["GET", "POST"]) +def exchange_rate_add(request): + if request.method == "POST": + form = ExchangeRateForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Exchange rate added successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas, toasts", + }, + ) + else: + form = ExchangeRateForm() + + return render( + request, + "exchange_rates/fragments/add.html", + {"form": form}, + ) + + +@only_htmx +@login_required +@require_http_methods(["GET", "POST"]) +def exchange_rate_edit(request, pk): + exchange_rate = get_object_or_404(ExchangeRate, id=pk) + + if request.method == "POST": + form = ExchangeRateForm(request.POST, instance=exchange_rate) + if form.is_valid(): + form.save() + messages.success(request, _("Exchange rate updated successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas, toasts", + }, + ) + else: + form = ExchangeRateForm(instance=exchange_rate) + + return render( + request, + "exchange_rates/fragments/edit.html", + {"form": form, "exchange_rate": exchange_rate}, + ) + + +@only_htmx +@login_required +@csrf_exempt +@require_http_methods(["DELETE"]) +def exchange_rate_delete(request, pk): + exchange_rate = get_object_or_404(ExchangeRate, id=pk) + + exchange_rate.delete() + + messages.success(request, _("Exchange rate deleted successfully")) + + return HttpResponse( + status=204, + headers={ + "HX-Trigger": "updated, hide_offcanvas, toasts", + }, + ) diff --git a/app/templates/exchange_rates/fragments/add.html b/app/templates/exchange_rates/fragments/add.html new file mode 100644 index 0000000..8df8d9f --- /dev/null +++ b/app/templates/exchange_rates/fragments/add.html @@ -0,0 +1,11 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Add exchange rate' %}{% endblock %} + +{% block body %} +
+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/exchange_rates/fragments/edit.html b/app/templates/exchange_rates/fragments/edit.html new file mode 100644 index 0000000..35aa638 --- /dev/null +++ b/app/templates/exchange_rates/fragments/edit.html @@ -0,0 +1,11 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Edit exchange rate' %}{% endblock %} + +{% block body %} +
+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/exchange_rates/fragments/list.html b/app/templates/exchange_rates/fragments/list.html new file mode 100644 index 0000000..b999d0e --- /dev/null +++ b/app/templates/exchange_rates/fragments/list.html @@ -0,0 +1,59 @@ +{% load currency_display %} +{% load i18n %} +
+
+ {% spaceless %} +
{% translate 'Exchange Rates' %} + + +
+ {% endspaceless %} +
+ +
+ + + + + + + + + + + {% for exchange_rate in exchange_rates %} + + + + + + + {% endfor %} + +
{% translate 'Date' %}{% translate 'Pairing' %}{% translate 'Rate' %}
+ + + {{ exchange_rate.date|date:"SHORT_DATETIME_FORMAT" }}{{ exchange_rate.from_currency.code }} x {{ exchange_rate.to_currency.code }}1 {{ exchange_rate.from_currency.code }} ≅ {% currency_display amount=exchange_rate.rate prefix=exchange_rate.to_currency.prefix suffix=exchange_rate.to_currency.suffix decimal_places=exchange_rate.to_currency.decimal_places%}
+
+
diff --git a/app/templates/exchange_rates/pages/index.html b/app/templates/exchange_rates/pages/index.html new file mode 100644 index 0000000..cf6f324 --- /dev/null +++ b/app/templates/exchange_rates/pages/index.html @@ -0,0 +1,8 @@ +{% extends "layouts/base.html" %} +{% load i18n %} + +{% block title %}{% translate 'Exchange Rates' %}{% endblock %} + +{% block content %} +
+{% endblock %} diff --git a/app/templates/includes/navbar.html b/app/templates/includes/navbar.html index f46f990..0c0a6f6 100644 --- a/app/templates/includes/navbar.html +++ b/app/templates/includes/navbar.html @@ -34,7 +34,7 @@
  • {% translate 'Account Groups' %}
  • -
  • + +
  • +
  • +
  • {% translate 'Currencies' %}
  • +
  • {% translate 'Exchange Rates' %}