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 @@
+
+
+
+
+
+
+
+
+
+
+