Skip to content

Commit

Permalink
Merge pull request #395 from TampereHacklab/transaction_overview_data…
Browse files Browse the repository at this point in the history
…_api2

BankTransaction aggregatedata for graphs
  • Loading branch information
olmari authored Oct 23, 2022
2 parents 145589f + 549d43c commit 17dc93b
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 3 deletions.
2 changes: 2 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from rest_framework.routers import DefaultRouter

from . import views
from users import views as userviews

router = DefaultRouter()
router.register(r"access", views.AccessViewSet, basename="access")

router.register(r"banktransactionaggregate", userviews.BankTransactionAggregateViewSet, basename="banktransactionaggregate")

urlpatterns = [
path("auth/", include("rest_auth.urls")),
Expand Down
18 changes: 18 additions & 0 deletions users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ class Meta:
model = models.CustomUser
fields = ("is_active",)
extra_kwargs = {"is_active": {"required": True}}


class BankTransactionAggregateSerializer(serializers.Serializer):
"""
Serializer for BankTransactionAggregate data
"""

aggregatedate = serializers.DateField()
withdrawals = serializers.DecimalField(
max_digits=10, decimal_places=2, coerce_to_string=False
)
deposits = serializers.DecimalField(
max_digits=10, decimal_places=2, coerce_to_string=False
)

class Meta:
model = models.BankTransaction
fields = ("aggregatedate", "withdrawals", "deposits")
84 changes: 82 additions & 2 deletions users/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime
from datetime import timedelta
from datetime import date, timedelta

from django.contrib.auth import get_user_model
from django.core import mail
Expand All @@ -8,6 +8,7 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.http import urlencode

from mailer.models import Message
from rest_framework import status
Expand Down Expand Up @@ -394,11 +395,12 @@ def dummy_deactivate_listener(sender, instance: models.CustomUser, **kwargs):
class UsersAPITests(APITestCase):
def test_get_users(self):
"""
get users api call
get users api call. unauthenticated users don't get anything
"""
url = reverse("customuser-list")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 0)


class CustomInvoiceTests(TestCase):
Expand Down Expand Up @@ -466,3 +468,81 @@ def tearDown(self):
models.CustomUser.objects.all().delete()
models.MemberService.objects.all().delete()
models.ServiceSubscription.objects.all().delete()


class BankAggreagetApiTests(APITestCase):
def setUp(self):
"""
Generate few banktransactions for testing
"""
self.user = get_user_model().objects.create_customuser(
first_name="FirstName",
last_name="LastName",
email="user1@example.com",
birthday=timezone.now(),
municipality="City",
nick="user1",
phone="+358123123",
)
# for these dates, generate 10 deposits and withdravals for each day
start_date = date(2022, 1, 1)
end_date = date(2022, 1, 11)
for single_date in self.daterange(start_date, end_date):
for amount in range(1, 11):
models.BankTransaction.objects.create(
archival_reference=f"{single_date}_deposit_{amount}",
date=single_date,
amount=amount,
)
models.BankTransaction.objects.create(
archival_reference=f"{single_date}_withdraval_{amount}",
date=single_date,
amount=amount * -1,
)

def daterange(self, start_date, end_date):
for n in range(int((end_date - start_date).days)):
yield start_date + timedelta(n)

def test_get_banktransactionaggregatedata_not_loggedin(self):
"""
only logged in users can get the data
"""
url = reverse("banktransactionaggregate-list")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_get_banktransactionaggregatedata(self):
"""
aggregated data
"""
self.client.force_login(user=self.user)
url = reverse("banktransactionaggregate-list")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 10)
firstDay = response.data[0]
self.assertEqual(firstDay["withdrawals"], -55)
self.assertEqual(firstDay["deposits"] + firstDay["withdrawals"], 0)
self.assertEqual(firstDay["aggregatedate"], "2022-01-01")

def test_get_banktransactionaggregatedata_filtered(self):
"""
aggregated data
"""
self.client.force_login(user=self.user)
urlbase = reverse("banktransactionaggregate-list")
qparams = urlencode({"date__gte": "2022-01-01", "date__lte": "2022-01-01"})
url = f"{urlbase}?{qparams}"
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
# only one day result
self.assertEqual(len(response.data), 1)
firstDay = response.data[0]
self.assertEqual(firstDay["withdrawals"], -55)
self.assertEqual(firstDay["deposits"] + firstDay["withdrawals"], 0)
self.assertEqual(firstDay["aggregatedate"], "2022-01-01")

def tearDown(self):
models.CustomUser.objects.all().delete()
models.BankTransaction.objects.all().delete()
37 changes: 36 additions & 1 deletion users/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from decimal import Decimal

from django.db.models import Q, Sum
from django.db.models.functions import TruncDay, Coalesce

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAdminUser
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response

from . import filters, models, permissions, serializers
Expand Down Expand Up @@ -46,3 +51,33 @@ def set_activation(self, request, pk=None):
return Response(
serializers.UserSerializer(user, context={"request": request}).data
)


class BankTransactionAggregateViewSet(viewsets.ModelViewSet):
"""
Get aggregate overview of bank transaction data
Response contains
aggregatedate (day for aggregation)
withdravals summed by aggregatedate
deposits summed by aggregatedate
filter with `?date__gte=2022-01-01&date__lte=2022-12-31`
"""

serializer_class = serializers.BankTransactionAggregateSerializer
permission_classes = (IsAuthenticated,)
http_method_names = ["get", "options", "trace"]
pagination_class = None
filterset_fields = {"date": ["gte", "lte"]}

queryset = (
models.BankTransaction.objects.values(
aggregatedate=TruncDay("date"),
)
.annotate(
withdrawals=Coalesce(Sum("amount", filter=Q(amount__lt=0)), Decimal(0)),
deposits=Coalesce(Sum("amount", filter=Q(amount__gt=0)), Decimal(0)),
)
.order_by("date")
)
26 changes: 26 additions & 0 deletions www/locale/fi/LC_MESSAGES/djangojs.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-10-22 16:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: www/static/www/graphs.js:25
msgid "Deposits"
msgstr "Talletukset"

#: www/static/www/graphs.js:30
msgid "Withdrawals"
msgstr "Nostot"
13 changes: 13 additions & 0 deletions www/static/www/chartjs/chart.js

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions www/static/www/graphs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
async function renderTransactionsGraph() {
// for now default to fetching 1 year back
let startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
const queryParams = {
date__gte: startDate.toISOString().slice(0, 10),
};
const searchParams = new URLSearchParams(queryParams);
const response = await fetch("/api/v1/banktransactionaggregate/?" + searchParams.toString());
const data = await response.json();
let labels = [];
let deposits = [];
let withdrawals = [];
for (i = 0; i < data.length; i++) {
labels.push(data[i].aggregatedate);
deposits.push(data[i].deposits);
withdrawals.push(data[i].withdrawals);
}
new Chart(document.getElementById("transactions"), {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: gettext('Deposits'),
data: deposits,
backgroundColor: "#227D7D"
},
{
label: gettext('Withdrawals'),
data: withdrawals,
backgroundColor: "#D13838"
},
],
},
options: {
legend: { display: true },
scales: {
x: {
stacked: true,
},
},
},
});
}
renderTransactionsGraph();
2 changes: 2 additions & 0 deletions www/templates/www/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{% bootstrap_javascript jquery='full' %}
<link rel="shortcut icon" type="image/png" href="{% static 'www/favicon.ico' %}"/>
<link rel="stylesheet" href="{% static 'www/mulysa.css' %}" />
<script src="{% url 'javascript-catalog' %}"></script>
</head>
<body>

Expand All @@ -28,6 +29,7 @@
<li class="nav-item"><a class="nav-link" href="{% url 'userdetails' user.id %}">{% trans 'Show my user information' %}</a></li>
{% if user.membershipapplication_set.count == 0%}
<li class="nav-item"><a class="nav-link" href="{% url 'usersettings' user.id %}">{% trans 'Change my settings' %}</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'graphs' %}">{% trans 'Graphs' %}</a></li>
{% endif %}
{% if user.is_staff %}
<li class="nav-item"><a class="nav-link" href="{% url 'users' %}">{% trans 'Users' %}</a></li>
Expand Down
21 changes: 21 additions & 0 deletions www/templates/www/graphs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "www/base.html" %}
{% load i18n %}
{% load bootstrap4 %}
{% load static %}

{% block content %}
<div class="container">
<h1>{% trans 'Statistics' %}</h1>

<h2>{% trans 'Transactions' %}</h2>
<canvas
id="transactions"
style="display: block; box-sizing: border-box; height: 400px; width: 800px"
width="800"
height="400"
></canvas>
</div>

<script src="{% static 'www/chartjs/chart.js' %}"></script>
<script src="{% static 'www/graphs.js' %}"></script>
{% endblock %}
2 changes: 2 additions & 0 deletions www/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def test_secured_urls(self):
reverse("custominvoice_action", args=("test", 1)),
reverse("application_operation", args=(1, "test")),
reverse("banktransaction-view", args=(1,)),
reverse("banktransactionaggregate-list", args=()),
reverse("banktransactionaggregate-detail", args=(1,)),
]
self.client.logout()
for url in urls:
Expand Down
3 changes: 3 additions & 0 deletions www/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.urls import include, path
from django.views.generic import TemplateView
from django.views.i18n import JavaScriptCatalog

from . import views

Expand All @@ -16,6 +17,7 @@
path("custominvoices", views.custominvoices, name="custominvoices"),
path("userdetails/<int:id>/", views.userdetails, name="userdetails"),
path("usersettings/<int:id>/", views.usersettings, name="usersettings"),
path("graphs", TemplateView.as_view(template_name="www/graphs.html"), name="graphs"),
path(
"usersettings/<int:id>/subscribe_service",
views.usersettings_subscribe_service,
Expand Down Expand Up @@ -56,4 +58,5 @@
),
path("changelog", views.changelog_view, name="changelog-view"),
path("i18n/", include("django.conf.urls.i18n")),
path('jsi18n/', JavaScriptCatalog.as_view(packages=['www']), name='javascript-catalog'),
]

0 comments on commit 17dc93b

Please sign in to comment.