diff --git a/.gitignore b/.gitignore index f538c7d..f3d5a46 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,9 @@ celerybeat-schedule dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ + +db.sqlite3 +accounts/test/test_view.py +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..074089f --- /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('tetetebvgj') + 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('tetetebvgj') + 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 0000000..1e266c3 Binary files /dev/null and b/db.sqlite3 differ 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 @@