From 5fedbeb488e09dde6a0fa98d18d808d530af38f2 Mon Sep 17 00:00:00 2001 From: Ankur13997 Date: Fri, 14 May 2021 00:36:34 +0530 Subject: [PATCH 1/2] Add expenses and charts --- ExpenseTracker/settings.py | 8 +- ExpenseTracker/static/js/searchExp.js | 49 ++++++ ExpenseTracker/static/js/stat_exp.js | 161 +++++++++++++++++ ExpenseTracker/static/js/static.js | 64 +++++++ ExpenseTracker/static/js/statics.js | 45 +++++ ExpenseTracker/urls.py | 1 + Pipfile | 1 + Pipfile.lock | 81 +++++++-- ...s.py_f44b8b6b192c594f0a0805bb7d811b5c.prob | 1 + accounts/test/__init__.py | 0 accounts/test/test_models.py | 0 accounts/test/test_view.py | 166 ++++++++++++++++++ accounts/views.py | 10 +- db.sqlite3 | Bin 0 -> 155648 bytes expenses/__init__.py | 0 expenses/admin.py | 14 ++ expenses/apps.py | 5 + expenses/migrations/0001_initial.py | 36 ++++ .../migrations/0002_auto_20200508_1810.py | 17 ++ expenses/migrations/__init__.py | 0 expenses/models.py | 29 +++ expenses/test_models.py | 38 ++++ expenses/tests.py | 3 + expenses/urls.py | 17 ++ expenses/views.py | 144 +++++++++++++++ templates/base.html | 17 ++ templates/common/sidebar.html | 12 +- templates/expenses/add_expense.html | 65 +++++++ templates/expenses/edit-expense.html | 83 +++++++++ templates/expenses/index.html | 130 ++++++++++++++ templates/expenses/stat_exp.html | 41 +++++ templates/income/statics.html | 41 +++++ templates/income/stats.html | 51 ++++++ .../migrations/0002_alter_source_options.py | 17 ++ userincome/models.py | 3 + userincome/test/__init__.py | 0 userincome/test/test_view.py | 0 userincome/urls.py | 6 +- userincome/views.py | 30 ++++ 39 files changed, 1358 insertions(+), 28 deletions(-) create mode 100644 ExpenseTracker/static/js/searchExp.js create mode 100644 ExpenseTracker/static/js/stat_exp.js create mode 100644 ExpenseTracker/static/js/static.js create mode 100644 ExpenseTracker/static/js/statics.js create mode 100644 accounts/.cph/.views.py_f44b8b6b192c594f0a0805bb7d811b5c.prob create mode 100644 accounts/test/__init__.py create mode 100644 accounts/test/test_models.py create mode 100644 accounts/test/test_view.py create mode 100644 db.sqlite3 create mode 100644 expenses/__init__.py create mode 100644 expenses/admin.py create mode 100644 expenses/apps.py create mode 100644 expenses/migrations/0001_initial.py create mode 100644 expenses/migrations/0002_auto_20200508_1810.py create mode 100644 expenses/migrations/__init__.py create mode 100644 expenses/models.py create mode 100644 expenses/test_models.py create mode 100644 expenses/tests.py create mode 100644 expenses/urls.py create mode 100644 expenses/views.py create mode 100644 templates/expenses/add_expense.html create mode 100644 templates/expenses/edit-expense.html create mode 100644 templates/expenses/index.html create mode 100644 templates/expenses/stat_exp.html create mode 100644 templates/income/statics.html create mode 100644 templates/income/stats.html create mode 100644 userincome/migrations/0002_alter_source_options.py create mode 100644 userincome/test/__init__.py create mode 100644 userincome/test/test_view.py diff --git a/ExpenseTracker/settings.py b/ExpenseTracker/settings.py index c958458..751caa4 100644 --- a/ExpenseTracker/settings.py +++ b/ExpenseTracker/settings.py @@ -33,6 +33,7 @@ 'main', 'accounts', 'userincome', + 'expenses', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -90,11 +91,8 @@ else: DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get('DB_NAME'), - 'USER': os.environ.get('DB_USER'), - 'PASSWORD': os.environ.get('DB_USER_PASSWORD'), - 'HOST': os.environ.get('DB_HOST'), + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } diff --git a/ExpenseTracker/static/js/searchExp.js b/ExpenseTracker/static/js/searchExp.js new file mode 100644 index 0000000..5da7bc0 --- /dev/null +++ b/ExpenseTracker/static/js/searchExp.js @@ -0,0 +1,49 @@ +const searchField = document.querySelector("#searchField"); + +const tableOutput = document.querySelector(".table-output"); +const appTable = document.querySelector(".app-table"); +const paginationContainer = document.querySelector(".pagination-container"); +tableOutput.style.display = "none"; +const noResults = document.querySelector(".no-results"); +const tbody = document.querySelector(".table-body"); + +searchField.addEventListener("keyup", (e) => { + const searchValue = e.target.value; + + if (searchValue.trim().length > 0) { + paginationContainer.style.display = "none"; + tbody.innerHTML = ""; + fetch("search-expenses", { + body: JSON.stringify({ searchText: searchValue }), + method: "POST", + }) + .then((res) => res.json()) + .then((data) => { + console.log("data", data); + appTable.style.display = "none"; + tableOutput.style.display = "block"; + + console.log("data.length", data.length); + + if (data.length === 0) { + noResults.style.display = "block"; + tableOutput.style.display = "none"; + } else { + noResults.style.display = "none"; + data.forEach((item) => { + tbody.innerHTML += ` + + ${item.amount} + ${item.category} + ${item.description} + ${item.date} + `; + }); + } + }); + } else { + tableOutput.style.display = "none"; + appTable.style.display = "block"; + paginationContainer.style.display = "block"; + } +}); diff --git a/ExpenseTracker/static/js/stat_exp.js b/ExpenseTracker/static/js/stat_exp.js new file mode 100644 index 0000000..b3ea21a --- /dev/null +++ b/ExpenseTracker/static/js/stat_exp.js @@ -0,0 +1,161 @@ +// // const renderChart = (data, labels) => { +// // var ctx = document.getElementById("myChart").getContext("2d"); +// // var myChart = new Chart(ctx, { +// // type: "doughnut", +// // data: { +// // labels: labels, +// // datasets: [ +// // { +// // label: "Last 6 months expenses", +// // data: data, +// // backgroundColor: [ +// // "rgba(255, 99, 132, 0.2)", +// // "rgba(54, 162, 235, 0.2)", +// // "rgba(255, 206, 86, 0.2)", +// // "rgba(75, 192, 192, 0.2)", +// // "rgba(153, 102, 255, 0.2)", +// // "rgba(255, 159, 64, 0.2)", +// // ], +// // borderColor: [ +// // "rgba(255, 99, 132, 1)", +// // "rgba(54, 162, 235, 1)", +// // "rgba(255, 206, 86, 1)", +// // "rgba(75, 192, 192, 1)", +// // "rgba(153, 102, 255, 1)", +// // "rgba(255, 159, 64, 1)", +// // ], +// // borderWidth: 1, +// // }, +// // ], +// // }, +// // options: { +// // title: { +// // display: true, +// // text: "Expenses per category", +// // }, +// // }, +// // }); +// // }; + +// // const getChartData = () => { +// // console.log("fetching"); +// // fetch("/expense_category_summary") +// // .then((res) => res.json()) +// // .then((results) => { +// // console.log("results", results); +// // const category_data = results.expense_category_data; +// // const [labels, data] = [ +// // Object.keys(category_data), +// // Object.values(category_data), +// // ]; + +// // renderChart(data, labels); +// // }); +// // }; + +// // document.onload = getChartData(); + + + + + + + +// var ctx = document.getElementById('myChart').getContext('2d'); +// var myChart = new Chart(ctx, { +// type: 'bar', +// data: { +// labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], +// datasets: [{ +// label: '# of Votes', +// data: [12, 19, 3, 5, 2, 3], +// backgroundColor: [ +// 'rgba(255, 99, 132, 0.2)', +// 'rgba(54, 162, 235, 0.2)', +// 'rgba(255, 206, 86, 0.2)', +// 'rgba(75, 192, 192, 0.2)', +// 'rgba(153, 102, 255, 0.2)', +// 'rgba(255, 159, 64, 0.2)' +// ], +// borderColor: [ +// 'rgba(255, 99, 132, 1)', +// 'rgba(54, 162, 235, 1)', +// 'rgba(255, 206, 86, 1)', +// 'rgba(75, 192, 192, 1)', +// 'rgba(153, 102, 255, 1)', +// 'rgba(255, 159, 64, 1)' +// ], +// borderWidth: 1 +// }] +// }, +// options: { +// scales: { +// y: { +// beginAtZero: true +// } +// } +// } +// }); + + + +const renderChart = (data, labels) => { + var ctx = document.getElementById("myChart").getContext("2d"); + var myChart = new Chart(ctx, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: "Last 6 months expenses", + data: data, + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + ], + borderColor: [ + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + ], + borderWidth: 1, + }, + ], + + }, + options: { + title: { + display: true, + text: "Expenses per category", + }, + }, + }); + }; + + const getChartData = () => { + // console.log("fetching"); + fetch("expense_category_summary") + .then((res) => res.json()) + .then((results) => { + console.log("results", results); + const category_data = results.expense_category_data; + const [labels, data] = [ + Object.keys(category_data), + Object.values(category_data), + ]; + + renderChart(data, labels); + }); + }; + + document.onload = getChartData(); + + + diff --git a/ExpenseTracker/static/js/static.js b/ExpenseTracker/static/js/static.js new file mode 100644 index 0000000..4fe994f --- /dev/null +++ b/ExpenseTracker/static/js/static.js @@ -0,0 +1,64 @@ + + +const renderChart = (data, labels) => { + var ctx = document.getElementById("myChart").getContext("2d"); + var myChart = new Chart(ctx, { + + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: "Last 6 months expenses", + data: data, + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + ], + borderColor: [ + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + ], + borderWidth: 1, + }, + ], + + }, + options: { + title: { + display: true, + text: "Incomes per Sources", + }, + }, + }); + }; + + const getChartData = () => { + // console.log("fetching"); + fetch("income_source_summary") + .then((res) => res.json()) + .then((results) => { + console.log("results", results); + const category_data = results.income_source_data; + const [labels, data] = [ + Object.keys(category_data), + Object.values(category_data), + ]; + + renderChart(data, labels); + }); + }; + + document.onload = getChartData(); + + + + diff --git a/ExpenseTracker/static/js/statics.js b/ExpenseTracker/static/js/statics.js new file mode 100644 index 0000000..fb363c4 --- /dev/null +++ b/ExpenseTracker/static/js/statics.js @@ -0,0 +1,45 @@ + + + + +var ctx = document.getElementById('myChart').getContext('2d'); +var myChart = new Chart(ctx, { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } +}); + + + + + + diff --git a/ExpenseTracker/urls.py b/ExpenseTracker/urls.py index 15fd0a5..14d2487 100644 --- a/ExpenseTracker/urls.py +++ b/ExpenseTracker/urls.py @@ -21,4 +21,5 @@ path('', include('main.urls')), path('auth/', include('accounts.urls')), path('income/', include('userincome.urls')), + path('expenses/', include('expenses.urls')), ] diff --git a/Pipfile b/Pipfile index 756ac2d..56ef5c3 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ pycodestyle = "*" gunicorn = "*" django-heroku = "*" whitenoise = "*" +coverage = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9918a4f..dbaac46 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb5c8c9772b82158b452d4a3d8e082f7f2ce2cbd03206358c9a31f2e9e35071d" + "sha256": "cd6aa0e6d47d8a55aa59420e848433ad299a3e8f97233539ce17c23e0c68cc7d" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,64 @@ "markers": "python_version >= '3.6'", "version": "==3.3.4" }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "index": "pypi", + "version": "==5.5" + }, "dj-database-url": { "hashes": [ "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", @@ -33,11 +91,11 @@ }, "django": { "hashes": [ - "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927", - "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d" + "sha256:0a1d195ad65c52bf275b8277b3d49680bd1137a5f55039a806f25f6b9752ce3d", + "sha256:18dd3145ddbd04bf189ff79b9954d08fda5171ea7b57bf705789fea766a07d50" ], "index": "pypi", - "version": "==3.2" + "version": "==3.2.2" }, "django-heroku": { "hashes": [ @@ -49,6 +107,7 @@ }, "gunicorn": { "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" ], "index": "pypi", @@ -131,11 +190,11 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], "index": "pypi", - "version": "==1.15.0" + "version": "==1.16.0" }, "sqlparse": { "hashes": [ @@ -147,12 +206,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "markers": "python_version < '3.8'", - "version": "==3.7.4.3" + "version": "==3.10.0.0" }, "validate-email": { "hashes": [ diff --git a/accounts/.cph/.views.py_f44b8b6b192c594f0a0805bb7d811b5c.prob b/accounts/.cph/.views.py_f44b8b6b192c594f0a0805bb7d811b5c.prob new file mode 100644 index 0000000..20e8342 --- /dev/null +++ b/accounts/.cph/.views.py_f44b8b6b192c594f0a0805bb7d811b5c.prob @@ -0,0 +1 @@ +{"name":"Local: views","url":"c:\\Users\\HP\\OneDrive\\Desktop\\new_changes\\SE-Project\\accounts\\views.py","tests":[{"id":1620300639519,"input":"","output":""}],"interactive":false,"memoryLimit":1024,"timeLimit":3000,"srcPath":"c:\\Users\\HP\\OneDrive\\Desktop\\new_changes\\SE-Project\\accounts\\views.py","group":"local","local":true} \ No newline at end of file diff --git a/accounts/test/__init__.py b/accounts/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/test/test_models.py b/accounts/test/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/test/test_view.py b/accounts/test/test_view.py new file mode 100644 index 0000000..8e2597c --- /dev/null +++ b/accounts/test/test_view.py @@ -0,0 +1,166 @@ +from django.test import TestCase +from django.urls import reverse +from django.contrib.messages import get_messages +from django.contrib.auth.models import User +from django.utils.encoding import force_bytes, force_text, DjangoUnicodeDecodeError +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from accounts.utils import token_generator + + +class RegistrationView(TestCase): + + def test_can_view_page_correctly(self): + response = self.client.get(reverse('register')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'accounts/register.html') + + def test_should_signup_user(self): + self.user = { + "username": "username", + "email": "email@hmail2.com", + "password": "password", + } + response = self.client.post(reverse("register"), self.user) + self.assertEquals(response.status_code, 200) + + def test_should_not_signup_with_invalid_password(self): + self.user = { + "username": "username", + "email": "email@hmail2.com", + "password": "pass", + } + response = self.client.post(reverse("register"), self.user) + self.assertEquals(response.status_code, 302) + + storage = get_messages(response.wsgi_request) + + self.assertIn("Password too short", + list(map(lambda x: x.message, storage))) + + def test_should_not_signup_with_same_username(self): + self.user = { + "username": "username", + "email": "email@hmail.com", + "password": "password", + } + + self.user2 = { + "username": "username", + "email": "email@hmail2.com", + "password": "password", + } + self.client.post(reverse("register"), self.user) + response = self.client.post(reverse("register"), self.user2) + self.assertEquals(response.status_code, 409) + + storage = get_messages(response.wsgi_request) + + self.assertIn("This username is already registered", + list(map(lambda x: x.message, storage))) + + def test_should_not_signup_user_with_taken_email(self): + + self.user = { + "username": "username1", + "email": "email@hmail2.com", + "password": "password", + } + + self.test_user2 = { + "username": "username11", + "email": "email@hmail2.com", + "password": "password", + } + + self.client.post(reverse("register"), self.user) + response = self.client.post(reverse("register"), self.test_user2) + self.assertEquals(response.status_code, 409) + + storage = get_messages(response.wsgi_request) + + self.assertIn("Email address is already registered", + list(map(lambda x: x.message, storage))) + + def test_should_login_successfully(self): + self.user = { + "username": "username", + "password": "password", + } + response = self.client.post(reverse("login"), self.user) + self.assertEquals(response.status_code, 200) + + +class LoginView(TestCase): + def test_should_show_login_page(self): + response = self.client.get(reverse('login')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "accounts/login.html") + + def test_login_success(self): + self.user = { + "username": "username", + "email": "email@hmail2.com", + "password": "password", + } + self.client.post(reverse('register'), self.user, format='text/html') + user = User.objects.filter(email=self.user['email']).first() + user.is_active = True + user.save() + response = self.client.post(reverse('login'), self.user, format='text/html') + self.assertEqual(response.status_code, 302) + + def test_cannot_login_with_unverified_email(self): + self.user = { + "username": "username", + "email": "email@hmail2.com", + "password": "password", + } + self.client.post(reverse('register'), self.user, format='text/html') + response = self.client.post(reverse('login'), self.user, format='text/html') + self.assertEqual(response.status_code, 302) + storage = get_messages(response.wsgi_request) + + self.assertIn("Account is not active, Please check your email", + list(map(lambda x: x.message, storage))) + + def test_cantlogin_with_no_username(self): + response = self.client.post(reverse('login'), {'password': 'passwped', 'username': ''}, format='text/html') + self.assertEqual(response.status_code, 302) + + storage = get_messages(response.wsgi_request) + + self.assertIn("Please fill all fields", + list(map(lambda x: x.message, storage))) + + def test_cantlogin_with_no_password(self): + response = self.client.post(reverse('login'), {'username': 'passwped', 'password': ''}, format='text/html') + self.assertEqual(response.status_code, 302) + storage = get_messages(response.wsgi_request) + self.assertIn("Please fill all fields", + list(map(lambda x: x.message, storage))) + + +class VerificationView(TestCase): + def test_user_ctivates_success(self): + user = User.objects.create_user('testuser', 'test@gmail.com') + user.set_password('tetetebvghhhhj') + user.is_active = False + user.save() + uid = urlsafe_base64_encode(force_bytes(user.pk)) + token = token_generator.make_token(user) + response = self.client.get(reverse('activate', kwargs={'uidb64': uid, 'token': token})) + self.assertEqual(response.status_code, 302) + user = User.objects.get(email='test@gmail.com') + self.assertTrue(user.is_active) + + def test_user_cant_ctivates_succesfully(self): + user = User.objects.create_user('testuser', 'test@gmail.com') + user.set_password('tetetebvghhhhj') + user.is_active = False + user.save() + uid = urlsafe_base64_encode(force_bytes(user.pk)) + token = token_generator.make_token(user) + response = self.client.get(reverse('activate', kwargs={'uidb64': 'uid', 'token': 'token'})) + self.assertEqual(response.status_code, 302) + user = User.objects.get(email='test@gmail.com') + self.assertFalse(user.is_active) diff --git a/accounts/views.py b/accounts/views.py index ffea740..7b69436 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -34,7 +34,7 @@ def post(self, request): if not User.objects.filter(email=email).exists(): if len(password) < 6: messages.warning(request, 'Password too short') - return render(request, 'accounts/register.html', context) + return render(request, 'accounts/register.html', context, status=302) user = User.objects.create_user(username=username, email=email) user.set_password(password) @@ -58,9 +58,9 @@ def post(self, request): return render(request, 'accounts/register.html') else: messages.warning(request, 'Email address is already registered') - return render(request, 'accounts/register.html') + return render(request, 'accounts/register.html', status=409) messages.warning(request, 'This username is already registered') - return render(request, 'accounts/register.html') + return render(request, 'accounts/register.html', status=409) class VerificationView(View): @@ -106,13 +106,13 @@ def post(self, request): return redirect('main') messages.warning(request, "Account is not active, Please check your email") - return render(request, 'accounts/login.html') + return render(request, 'accounts/login.html', status=302) messages.warning(request, "Invalid credentials, try again") return render(request, 'accounts/login.html') messages.warning(request, "Please fill all fields") - return render(request, 'accounts/login.html') + return render(request, 'accounts/login.html', status=302) class LogoutView(View): diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..e01eb34a3ddd18e2bcd7cf0026efe25d08cd7f26 GIT binary patch literal 155648 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 b00AIypAjHi{d*dl{=MYLLjwBYfUf@s?ZZ`( literal 0 HcmV?d00001 diff --git a/expenses/__init__.py b/expenses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/expenses/admin.py b/expenses/admin.py new file mode 100644 index 0000000..84f5562 --- /dev/null +++ b/expenses/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from .models import Expense, Category +# Register your models here. + + +class ExpenseAdmin(admin.ModelAdmin): + list_display = ('amount', 'description', 'owner', 'category', 'date',) + search_fields = ('description', 'category', 'date',) + + list_per_page = 5 + + +admin.site.register(Expense, ExpenseAdmin) +admin.site.register(Category) diff --git a/expenses/apps.py b/expenses/apps.py new file mode 100644 index 0000000..12ca16e --- /dev/null +++ b/expenses/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ExpensesConfig(AppConfig): + name = 'expenses' diff --git a/expenses/migrations/0001_initial.py b/expenses/migrations/0001_initial.py new file mode 100644 index 0000000..1a871b4 --- /dev/null +++ b/expenses/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.5 on 2020-05-05 20:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='Expense', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.FloatField()), + ('date', models.DateField(default=django.utils.timezone.now)), + ('description', models.TextField()), + ('category', models.CharField(max_length=266)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/expenses/migrations/0002_auto_20200508_1810.py b/expenses/migrations/0002_auto_20200508_1810.py new file mode 100644 index 0000000..5023354 --- /dev/null +++ b/expenses/migrations/0002_auto_20200508_1810.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.5 on 2020-05-08 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('expenses', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + ] diff --git a/expenses/migrations/__init__.py b/expenses/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/expenses/models.py b/expenses/models.py new file mode 100644 index 0000000..78fdba2 --- /dev/null +++ b/expenses/models.py @@ -0,0 +1,29 @@ +from django.db import models +from django.contrib.auth.models import User +from django.utils.timezone import now + +# Create your models here. + + +class Expense(models.Model): + amount = models.FloatField() + date = models.DateField(default=now) + description = models.TextField() + owner = models.ForeignKey(to=User, on_delete=models.CASCADE) + category = models.CharField(max_length=266) + + def __str__(self): + return self.category + + class Meta: + ordering: ['-date'] + + +class Category(models.Model): + name = models.CharField(max_length=255) + + class Meta: + verbose_name_plural = 'Categories' + + def __str__(self): + return self.name diff --git a/expenses/test_models.py b/expenses/test_models.py new file mode 100644 index 0000000..b89305f --- /dev/null +++ b/expenses/test_models.py @@ -0,0 +1,38 @@ +from django.test import TestCase +from django.contrib.auth.models import User +from expenses.models import Expense, Category + + +class TestSourceModel(TestCase): + + def setUp(self): + self.data1 = Category.objects.create(name='django') + + def test_source_model_entry(self): + """ + Test Source model data insertion/type/field attributes + """ + data = self.data1 + self.assertTrue(isinstance(data, Category)) + + def test_source_model_entry_return(self): + """ + Test Source model default name + """ + data = self.data1 + self.assertEqual(str(data), 'django') + + +class TestUserIncomeModel(TestCase): + + def setUp(self): + User.objects.create(username='admin') + self.data1 = Expense.objects.create(amount=1, description='test description', owner_id=1, category="django") + + def test_source_model_entry(self): + """ + Test Source model data insertion/type/field attributes + """ + data = self.data1 + self.assertTrue(isinstance(data, Expense)) + self.assertEqual(str(data), 'django') diff --git a/expenses/tests.py b/expenses/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/expenses/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/expenses/urls.py b/expenses/urls.py new file mode 100644 index 0000000..19760af --- /dev/null +++ b/expenses/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from . import views + +from django.views.decorators.csrf import csrf_exempt + +urlpatterns = [ + path('', views.index, name="expenses"), + path('add-expense', views.add_expense, name="add-expenses"), + path('edit-expense/', views.expense_edit, name="expense-edit"), + path('expense-delete/', views.delete_expense, name="expense-delete"), + path('search-expenses', csrf_exempt(views.search_expenses), + name="search_expenses"), + path('expense_category_summary', views.expense_category_summary, + name="expense_category_summary"), + path('stat_exp', views.stats_vie, + name="stat_exp") +] diff --git a/expenses/views.py b/expenses/views.py new file mode 100644 index 0000000..01da786 --- /dev/null +++ b/expenses/views.py @@ -0,0 +1,144 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from .models import Category, Expense +# Create your views here. +from django.contrib import messages +from django.contrib.auth.models import User +from django.core.paginator import Paginator +import json +from django.http import JsonResponse +# from userpreferences.models import UserPreference +import datetime + + +def search_expenses(request): + if request.method == 'POST': + search_str = json.loads(request.body).get('searchText') + expenses = Expense.objects.filter( + amount__istartswith=search_str, owner=request.user) | Expense.objects.filter( + date__istartswith=search_str, owner=request.user) | Expense.objects.filter( + description__icontains=search_str, owner=request.user) | Expense.objects.filter( + category__icontains=search_str, owner=request.user) + data = expenses.values() + return JsonResponse(list(data), safe=False) + + +@login_required(login_url='/auth/login') +def index(request): + categories = Category.objects.all() + expenses = Expense.objects.filter(owner=request.user) + paginator = Paginator(expenses, 5) + page_number = request.GET.get('page') + page_obj = Paginator.get_page(paginator, page_number) + # currency = UserPreference.objects.get(user=request.user).currency + context = { + 'expenses': expenses, + 'page_obj': page_obj, + # 'currency': currency + } + return render(request, 'expenses/index.html', context) + + +@login_required(login_url='/auth/login') +def add_expense(request): + categories = Category.objects.all() + context = { + 'categories': categories, + 'values': request.POST + } + if request.method == 'GET': + return render(request, 'expenses/add_expense.html', context) + + if request.method == 'POST': + amount = request.POST['amount'] + + if not amount: + messages.error(request, 'Amount is required') + return render(request, 'expenses/add_expense.html', context) + description = request.POST['description'] + date = request.POST['expense_date'] + category = request.POST['category'] + + if not description: + messages.error(request, 'description is required') + return render(request, 'expenses/add_expense.html', context) + + Expense.objects.create(owner=request.user, amount=amount, date=date, + category=category, description=description) + messages.success(request, 'Expense saved successfully') + + return redirect('expenses') + + +@login_required(login_url='/auth/login') +def expense_edit(request, id): + expense = Expense.objects.get(pk=id) + categories = Category.objects.all() + context = { + 'expense': expense, + 'values': expense, + 'categories': categories + } + if request.method == 'GET': + return render(request, 'expenses/edit-expense.html', context) + if request.method == 'POST': + amount = request.POST['amount'] + + if not amount: + messages.error(request, 'Amount is required') + return render(request, 'expenses/edit-expense.html', context) + description = request.POST['description'] + date = request.POST['expense_date'] + category = request.POST['category'] + + if not description: + messages.error(request, 'description is required') + return render(request, 'expenses/edit-expense.html', context) + + expense.owner = request.user + expense.amount = amount + expense. date = date + expense.category = category + expense.description = description + + expense.save() + messages.success(request, 'Expense updated successfully') + + return redirect('expenses') + + +def delete_expense(request, id): + expense = Expense.objects.get(pk=id) + expense.delete() + messages.success(request, 'Expense removed') + return redirect('expenses') + + +def expense_category_summary(request): + todays_date = datetime.date.today() + six_months_ago = todays_date-datetime.timedelta(days=30*6) + expenses = Expense.objects.filter(owner=request.user, + date__gte=six_months_ago, date__lte=todays_date) + finalrep = {} + + def get_category(expense): + return expense.category + category_list = list(set(map(get_category, expenses))) + + def get_expense_category_amount(category): + amount = 0 + filtered_by_category = expenses.filter(category=category) + + for item in filtered_by_category: + amount += item.amount + return amount + + for x in expenses: + for y in category_list: + finalrep[y] = get_expense_category_amount(y) + + return JsonResponse({'expense_category_data': finalrep}, safe=False) + + +def stats_vie(request): + return render(request, 'expenses/stat_exp.html') diff --git a/templates/base.html b/templates/base.html index 8476ba2..1219186 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,7 +11,13 @@ + + + + + + @@ -43,7 +49,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/templates/common/sidebar.html b/templates/common/sidebar.html index 32dfa4f..9dec30b 100644 --- a/templates/common/sidebar.html +++ b/templates/common/sidebar.html @@ -8,8 +8,8 @@