From b39a094d92d1b5cd0dad1e46db596ec105429b8d Mon Sep 17 00:00:00 2001 From: paul Date: Sun, 25 Jan 2026 19:45:56 -0500 Subject: [PATCH] Fixing up the go code, adding initial work for the stats pages, adding a function to pre-populate the database with some example polls for testing. Will be removed later --- client/package-lock.json | 348 +++++++++++++++++++++++++++- client/package.json | 3 +- client/src/App.js | 3 + client/src/pages/PollList.js | 31 +-- server/common/common.go | 5 + server/main.go | 85 ++++++- server/models/poll.go | 24 +- server/services/members.go | 11 +- server/services/poll.go | 247 ++++++++++++++------ server/services/putmembers_test.go | 45 ---- server/services/services_test.go | 356 ++++++++++++++--------------- 11 files changed, 821 insertions(+), 337 deletions(-) delete mode 100644 server/services/putmembers_test.go diff --git a/client/package-lock.json b/client/package-lock.json index 07d984c..aa9f819 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,7 +17,8 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-router": "7.12.0", - "react-scripts": "5.0.1" + "react-scripts": "5.0.1", + "recharts": "^3.7.0" } }, "node_modules/@alloc/quick-lru": { @@ -3165,6 +3166,40 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3270,6 +3305,16 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3575,6 +3620,60 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -3827,6 +3926,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -6162,6 +6266,116 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6249,6 +6463,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6827,6 +7046,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8812,6 +9036,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -13004,6 +13236,28 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -13164,6 +13418,46 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" + }, + "node_modules/recharts/node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -13175,6 +13469,19 @@ "node": ">=6.0.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -13314,6 +13621,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -15009,6 +15321,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -15455,6 +15772,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15521,6 +15846,27 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index 6fd7a8d..25e522a 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,8 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-router": "7.12.0", - "react-scripts": "5.0.1" + "react-scripts": "5.0.1", + "recharts": "^3.7.0" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/App.js b/client/src/App.js index 38f490d..ae42e50 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -6,6 +6,7 @@ import AdminMembers from "./pages/AdminMembers"; import AdminMembersView from "./pages/AdminMembersView"; import AdminCreateVote from "./pages/AdminCreateVote"; import PollList from "./pages/PollList"; +import PollDetails from "./pages/PollDetails"; import './App.css'; export default function App() { @@ -27,6 +28,7 @@ export default function App() {
Create Vote Poll List + View Poll Details {/* Add this line */}
@@ -40,6 +42,7 @@ export default function App() { {/* Vote routes */} } /> } /> + } /> {/* Add this route */} ); diff --git a/client/src/pages/PollList.js b/client/src/pages/PollList.js index 453d7e7..944e478 100644 --- a/client/src/pages/PollList.js +++ b/client/src/pages/PollList.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; -import Table from '@mui/material/Table'; +import { Link } from 'react-router'; export default function PollList() { const [polls, setPolls] = useState([]); @@ -19,28 +19,15 @@ export default function PollList() { }; return ( -
+

Poll List

- - - - - - - - - - - {polls.map((poll) => ( - - - - - - - ))} - -
Created AtQuestionMember Yes VotesMember No Votes
{new Date(poll.created_at).toLocaleString()}{poll.question}{poll.member_yes}{poll.member_no}
+ {polls.map((poll) => ( +
+ +

{poll.question}

+ +
+ ))}
); } diff --git a/server/common/common.go b/server/common/common.go index 3de5d48..64c897b 100644 --- a/server/common/common.go +++ b/server/common/common.go @@ -5,6 +5,11 @@ import ( "net/http" ) +const ( + DATE_FORMAT = "2006-01-02 15:04:05" + SUCCESS = "success" +) + func SendError(w http.ResponseWriter, errStr string, statusCode int) { w.WriteHeader(statusCode) json.NewEncoder(w).Encode(map[string]string{"error": errStr}) diff --git a/server/main.go b/server/main.go index c4cfcdc..d0eb953 100644 --- a/server/main.go +++ b/server/main.go @@ -2,11 +2,14 @@ package main import ( "encoding/json" + "fmt" "log" + "math/rand" "net/http" "os" - "strconv" "path/filepath" + "strconv" + "time" "github.com/gorilla/mux" @@ -42,7 +45,7 @@ func voteIDHandler(resWriter http.ResponseWriter, request *http.Request) { vote := models.Vote{ PollId: id, Email: "example@example.com", // Replace with actual email retrieval logic - Vote: true, // Replace with actual vote retrieval logic + Vote: true, // Replace with actual vote retrieval logic } err = services.SetVote(&vote) @@ -74,12 +77,19 @@ func statsHandler(resWriter http.ResponseWriter, request *http.Request) { } } -func statsIDHandler(resWriter http.ResponseWriter, request *http.Request) { +func pollsIDHandler(resWriter http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) - id := vars["id"] - - poll, err := services.GetPollByQuestion(id) + id, err := strconv.ParseInt(vars["id"], 10, 64) if err != nil { + common.SendError(resWriter, "Invalid poll ID", http.StatusBadRequest) + return + } + + poll, err := services.GetPollById(id) + if err == services.ErrPollNotFound { + common.SendError(resWriter, "Poll not found", http.StatusNotFound) + return + } else if err != nil { common.SendError(resWriter, "Failed to get poll", http.StatusInternalServerError) return } @@ -125,14 +135,73 @@ func adminLoginHandler(resWriter http.ResponseWriter, request *http.Request) { }) } +func initDatabase() error { + // Seed random generator for reproducible results in tests + rand.Seed(42) + + polls := []models.Poll{ + { + ID: 1, + Question: "Should we increase the budget?", + MemberYes: rand.Int63n(50), + MemberNo: rand.Int63n(50), + NonMemberYes: rand.Int63n(20), + NonMemberNo: rand.Int63n(20), + TotalVotes: int(rand.Int63n(100)), + WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"}, + CreatedAt: time.Now().Format(time.RFC3339), + UpdatedAt: time.Now().Format(time.RFC3339), + ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }, + { + ID: 2, + Question: "Should we hire more staff?", + MemberYes: rand.Int63n(50), + MemberNo: rand.Int63n(50), + NonMemberYes: rand.Int63n(20), + NonMemberNo: rand.Int63n(20), + TotalVotes: int(rand.Int63n(100)), + WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"}, + CreatedAt: time.Now().Format(time.RFC3339), + UpdatedAt: time.Now().Format(time.RFC3339), + ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }, + { + ID: 3, + Question: "Should we renovate the building?", + MemberYes: rand.Int63n(50), + MemberNo: rand.Int63n(50), + NonMemberYes: rand.Int63n(20), + NonMemberNo: rand.Int63n(20), + TotalVotes: int(rand.Int63n(100)), + WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"}, + CreatedAt: time.Now().Format(time.RFC3339), + UpdatedAt: time.Now().Format(time.RFC3339), + ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }, + } + + for _, poll := range polls { + if err := services.CreatePollIgnore(&poll); err != nil { + return fmt.Errorf("failed to create poll %d: %v", poll.ID, err) + } + } + return nil +} + func main() { log.SetOutput(os.Stdout) log.SetFlags(log.LstdFlags | log.Lshortfile) + // Initialize database with sample data + if err := initDatabase(); err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + http.HandleFunc("/api/vote", voteHandler) http.HandleFunc("/api/vote/{id}", voteIDHandler) http.HandleFunc("/api/stats", statsHandler) - http.HandleFunc("/api/stats/{id}", statsIDHandler) + http.HandleFunc("/api/polls/{id}", pollsIDHandler) http.HandleFunc("/api/admin/new-vote", services.AdminNewVoteHandler) http.HandleFunc("/api/admin/view-votes", services.AdminViewVoteHandler) http.HandleFunc("/api/admin/login", adminLoginHandler) @@ -141,7 +210,7 @@ func main() { buildPath := filepath.Join(".", "client", "build") fs := http.FileServer(http.Dir(buildPath)) - + http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // If the file exists on disk, let the file server handle it. if _, err := os.Stat(filepath.Join(buildPath, r.URL.Path)); err == nil { diff --git a/server/models/poll.go b/server/models/poll.go index e1d5f38..02c083b 100644 --- a/server/models/poll.go +++ b/server/models/poll.go @@ -1,15 +1,15 @@ package models type Poll struct { - ID int64 `json:"id"` - Question string `json:"question"` - MemberYes int64 `json:"member_yes"` - MemberNo int64 `json:"member_no"` - NonMemberYes int64 `json:"non_member_yes` - NonMemberNo int64 `json:"non_member_no` - TotalVotes int `json:"total_votes"` - WhoVoted []string `json:"who_voted"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - ExpiresAt string `json:"expires_at"` -} \ No newline at end of file + ID int64 `json:"id"` + Question string `json:"question"` + MemberYes int64 `json:"member_yes"` + MemberNo int64 `json:"member_no"` + NonMemberYes int64 `json:"non_member_yes"` + NonMemberNo int64 `json:"non_member_no"` + TotalVotes int `json:"total_votes"` + WhoVoted []string `json:"who_voted"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ExpiresAt string `json:"expires_at"` +} diff --git a/server/services/members.go b/server/services/members.go index 164cfdd..758f257 100644 --- a/server/services/members.go +++ b/server/services/members.go @@ -22,6 +22,7 @@ type Member struct { } const BATCH_SIZE = 100 +const CVS_FILE_FIELD = "members.csv" func AdminMembersHandler(resWriter http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { @@ -49,16 +50,16 @@ func AdminMembersHandler(resWriter http.ResponseWriter, request *http.Request) { } } - file, _, err := request.FormFile("members.csv") + file, _, err := request.FormFile(CVS_FILE_FIELD) if err != nil { - common.SendError(resWriter, "Failed to read members.csv file", http.StatusBadRequest) + common.SendError(resWriter, "Failed to read " + CVS_FILE_FIELD + " file", http.StatusBadRequest) return } defer file.Close() fileBytes, err := ioutil.ReadAll(file) if err != nil { - common.SendError(resWriter, "Failed to read members.csv file", http.StatusInternalServerError) + common.SendError(resWriter, "Failed to read " + CVS_FILE_FIELD + " file", http.StatusInternalServerError) return } @@ -68,7 +69,7 @@ func AdminMembersHandler(resWriter http.ResponseWriter, request *http.Request) { } resWriter.WriteHeader(http.StatusOK) - json.NewEncoder(resWriter).Encode(map[string]bool{"success": true}) + json.NewEncoder(resWriter).Encode(map[string]bool{common.SUCCESS: true}) } func AdminMembersView(resWriter http.ResponseWriter, request *http.Request) { @@ -92,7 +93,7 @@ func AdminMembersView(resWriter http.ResponseWriter, request *http.Request) { resWriter.WriteHeader(http.StatusOK) json.NewEncoder(resWriter).Encode(map[string]interface{}{ - "success": true, + common.SUCCESS: true, "members": members, }) } diff --git a/server/services/poll.go b/server/services/poll.go index 5e0cdef..8dd3fcf 100644 --- a/server/services/poll.go +++ b/server/services/poll.go @@ -2,27 +2,28 @@ package services import ( "database/sql" - "errors" - "time" - "log" "encoding/json" - "strconv" + "errors" + "log" "net/http" + "strconv" + "time" "go-sjles-pta-vote/server/common" "go-sjles-pta-vote/server/db" "go-sjles-pta-vote/server/models" ) -var ErrQuestionAlreadyExists = errors.New("Question already exists") -var ErrQuestionDoesntExist = errors.New("Question does not exist yet") -var ErrVoterAlreadyVoted = errors.New("Voter already voted") -var ErrPollNotFound = errors.New("Poll not found") -var ErrFailedToUpdateVote = errors.New("Failed to update vote") -var ErrFailedToDeletePoll = errors.New("Failed to delete poll") +var ( + ErrQuestionAlreadyExists = errors.New("Question already exists") + ErrQuestionDoesntExist = errors.New("Question does not exist yet") + ErrVoterAlreadyVoted = errors.New("Voter already voted") + ErrPollNotFound = errors.New("Poll not found") + ErrFailedToUpdateVote = errors.New("Failed to update vote") + ErrFailedToDeletePoll = errors.New("Failed to delete poll") +) -const ( - DATE_FORMAT = "2006-01-02 15:04:05" +const ( DEFAULT_POLL_DURATION_HOURS = 24 ) @@ -49,8 +50,8 @@ func AdminNewVoteHandler(resWriter http.ResponseWriter, request *http.Request) { } poll := models.Poll{ - Question: question, - ExpiresAt: time.Now().Add(time.Duration(durationHours) * time.Hour).Format(DATE_FORMAT), + Question: question, + ExpiresAt: time.Now().Add(time.Duration(durationHours) * time.Hour).Format(common.DATE_FORMAT), } _, err := CreatePoll(&poll) @@ -60,14 +61,14 @@ func AdminNewVoteHandler(resWriter http.ResponseWriter, request *http.Request) { } resWriter.WriteHeader(http.StatusOK) - json.NewEncoder(resWriter).Encode(map[string]bool{"success": true}) + json.NewEncoder(resWriter).Encode(map[string]bool{common.SUCCESS: true}) } -func CreatePoll(poll *models.Poll) (*int64, error) { +func CreatePoll(poll *models.Poll) (int64, error) { db_conn, err := db.Connect() if err != nil { - log.Fatal(err) - return nil, err + log.Printf("Failed to connect to database: %s", err.Error()) + return -1, err } defer db.Close() @@ -77,21 +78,21 @@ func CreatePoll(poll *models.Poll) (*int64, error) { WHERE question == $1 `) if err != nil { - log.Fatal(err) - return nil, err + log.Printf("%s", err.Error()) + return -1, err } defer get_stmt.Close() var id int err = get_stmt.QueryRow(poll.Question).Scan(&id) - if err != sql.ErrNoRows { - if err != nil { - log.Fatal(err) - return nil, err + if err != nil { + if err != sql.ErrNoRows { + log.Printf("%s", err.Error()) + return -1, err } - return nil, ErrQuestionAlreadyExists + return -1, ErrQuestionAlreadyExists } - + stmt, err := db_conn.Prepare(` INSERT INTO polls ( question, @@ -103,20 +104,20 @@ func CreatePoll(poll *models.Poll) (*int64, error) { `) if err != nil { - log.Fatal(err) - return nil, err + log.Printf("%s", err.Error()) + return -1, err } defer stmt.Close() res, err := stmt.Exec(poll.Question, poll.ExpiresAt) if err != nil { - log.Fatal(err) - return nil, err + log.Printf("%s", err.Error()) + return -1, err } - new_poll_id, err := res.LastInsertId() - return &new_poll_id, err + new_poll_id, err := res.LastInsertId() + return new_poll_id, err } func AdminViewVoteHandler(resWriter http.ResponseWriter, request *http.Request) { @@ -125,19 +126,36 @@ func AdminViewVoteHandler(resWriter http.ResponseWriter, request *http.Request) return } - polls, err := GetAllPolls() - if err != nil { - common.SendError(resWriter, "Failed to get polls", http.StatusInternalServerError) - return + var polls []models.Poll + var err error + question := request.FormValue("question") + if question == "" { + polls, err = GetAllPolls() + if err != nil { + common.SendError(resWriter, "Failed to get polls", http.StatusInternalServerError) + return + } + } else { + poll, err := GetPollByQuestion(question) + if err != nil { + common.SendError(resWriter, "Failed to get poll question "+question, http.StatusInternalServerError) + return + } + polls = append(polls, *poll) } - json.NewEncoder(resWriter).Encode(polls) + err = json.NewEncoder(resWriter).Encode(polls) + if err != nil { + log.Printf("Error encoding response: %v", err) + common.SendError(resWriter, "Failed to encode polls", http.StatusInternalServerError) + return + } } func GetAllPolls() ([]models.Poll, error) { db_conn, err := db.Connect() if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } defer db.Close() @@ -152,14 +170,14 @@ func GetAllPolls() ([]models.Poll, error) { FROM polls `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } defer get_polls_stmt.Close() rows, err := get_polls_stmt.Query() if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } defer rows.Close() @@ -175,7 +193,7 @@ func GetAllPolls() ([]models.Poll, error) { &new_poll.ExpiresAt, ) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } @@ -188,7 +206,7 @@ func GetAllPolls() ([]models.Poll, error) { func GetPollByQuestion(question string) (*models.Poll, error) { db_conn, err := db.Connect() if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } defer db.Close() @@ -204,7 +222,7 @@ func GetPollByQuestion(question string) (*models.Poll, error) { WHERE question == $1 `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } defer get_poll_stmt.Close() @@ -221,11 +239,11 @@ func GetPollByQuestion(question string) (*models.Poll, error) { if err == sql.ErrNoRows { return nil, ErrPollNotFound } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } - get_voters_stmt, err := db_conn.Prepare (` + get_voters_stmt, err := db_conn.Prepare(` SELECT voter_email FROM voters WHERE poll_id == $1 @@ -240,7 +258,7 @@ func GetPollByQuestion(question string) (*models.Poll, error) { var voter_email string err = rows.Scan(&voter_email) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } new_poll.WhoVoted = append(new_poll.WhoVoted, voter_email) @@ -249,13 +267,56 @@ func GetPollByQuestion(question string) (*models.Poll, error) { return &new_poll, nil } +func GetPollById(id int64) (*models.Poll, error) { + db_conn, err := db.Connect() + if err != nil { + log.Printf("%s", err.Error()) + return nil, err + } + defer db.Close() + + get_poll_stmt, err := db_conn.Prepare(` + SELECT + id, question, + member_yes_votes, member_no_votes, + non_member_yes_votes, non_member_no_votes, + created_at, updated_at, + expires_at + FROM polls + WHERE id == $1 + `) + if err != nil { + log.Printf("%s", err.Error()) + return nil, err + } + defer get_poll_stmt.Close() + + new_poll := models.Poll{} + err = get_poll_stmt.QueryRow(id).Scan( + &new_poll.ID, &new_poll.Question, + &new_poll.MemberYes, &new_poll.MemberNo, + &new_poll.NonMemberYes, &new_poll.NonMemberNo, + &new_poll.CreatedAt, &new_poll.UpdatedAt, + &new_poll.ExpiresAt, + ) + + if err == sql.ErrNoRows { + return nil, ErrPollNotFound + } else if err != nil { + log.Printf("%s", err.Error()) + return nil, err + } + + return &new_poll, nil +} + func GetAndCreatePollByQuestion(question string) (*models.Poll, error) { new_poll, err := GetPollByQuestion(question) if err == ErrPollNotFound { create_poll := &models.Poll{ - Question: question, - ExpiresAt: time.Now().Add(time.Hour * 10).Format(DATE_FORMAT), + Question: question, + ExpiresAt: time.Now().Add(time.Hour * 10).Format(common.DATE_FORMAT), } if _, err = CreatePoll(create_poll); err != nil { @@ -264,7 +325,7 @@ func GetAndCreatePollByQuestion(question string) (*models.Poll, error) { return GetPollByQuestion(question) } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return nil, err } else { return new_poll, err @@ -274,7 +335,7 @@ func GetAndCreatePollByQuestion(question string) (*models.Poll, error) { func SetVote(vote *models.Vote) error { db_conn, err := db.Connect() if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer db.Close() @@ -285,21 +346,21 @@ func SetVote(vote *models.Vote) error { VALUES ($1, $2) `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer set_voter_stmt.Close() res, err := set_voter_stmt.Exec(vote.PollId, vote.Email) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } else { rows_changed, err := res.RowsAffected() if rows_changed != 1 { return ErrVoterAlreadyVoted } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } } @@ -310,7 +371,7 @@ func SetVote(vote *models.Vote) error { WHERE email == $1 `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer is_voter_member_stmt.Close() @@ -321,7 +382,7 @@ func SetVote(vote *models.Vote) error { if err == sql.ErrNoRows { is_member = false } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } @@ -344,21 +405,21 @@ func SetVote(vote *models.Vote) error { WHERE id == $1 `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer add_vote_stmt.Close() res, err = add_vote_stmt.Exec(vote.PollId) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } if num, err := res.RowsAffected(); num != 1 { return ErrFailedToUpdateVote } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } @@ -369,8 +430,8 @@ func SetVote(vote *models.Vote) error { func DeletePollByQuestion(question string) error { db_conn, err := db.Connect() if err != nil { + log.Printf("%s", err.Error()) return err - log.Fatal(err) } defer db.Close() @@ -383,14 +444,14 @@ func DeletePollByQuestion(question string) error { ) `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer delete_votes_stmt.Close() _, err = delete_votes_stmt.Exec(question) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } @@ -399,23 +460,79 @@ func DeletePollByQuestion(question string) error { WHERE question == $1 `) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } defer delete_poll_stmt.Close() res, err := delete_poll_stmt.Exec(question) if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } if num, err := res.RowsAffected(); num != 1 { return ErrFailedToDeletePoll } else if err != nil { - log.Fatal(err) + log.Printf("%s", err.Error()) return err } return nil -} \ No newline at end of file +} + +func CreatePollIgnore(poll *models.Poll) error { + db_conn, err := db.Connect() + if err != nil { + log.Printf("%s", err.Error()) + return err + } + defer db.Close() + + stmt, err := db_conn.Prepare(` + INSERT OR IGNORE INTO polls ( + question, + expires_at, + member_yes_votes, + member_no_votes, + non_member_yes_votes, + non_member_no_votes, + created_at, + updated_at + ) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 + ) + `) + + if err != nil { + log.Printf("%s", err.Error()) + return err + } + + defer stmt.Close() + + _, err = stmt.Exec( + poll.Question, + poll.ExpiresAt, + poll.MemberYes, + poll.MemberNo, + poll.NonMemberYes, + poll.NonMemberNo, + poll.CreatedAt, + poll.UpdatedAt, + ) + + if err != nil { + log.Printf("%s", err.Error()) + return err + } + + return nil +} diff --git a/server/services/putmembers_test.go b/server/services/putmembers_test.go deleted file mode 100644 index 35604aa..0000000 --- a/server/services/putmembers_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package services - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseMembersFromBytes(t *testing.T) { - testCases := []struct { - name string - input string - expected []Member - }{ - { - name: "Valid CSV with multiple members", - input: `date,First,Last,Email -2023-01-01,John,Doe,john.doe@example.com -2023-01-02,Jane,Smith,jane.smith@example.com`, - expected: []Member{ - {Name: "John Doe", Email: "john.doe@example.com"}, - {Name: "Jane Smith", Email: "jane.smith@example.com"}, - }, - }, - { - name: "CSV with missing fields", - input: `date,First,Last -2023-01-01,John,Doe`, - expected: []Member{}, - }, - { - name: "Empty CSV", - input: ``, - expected: []Member{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - members, err := ParseMembersFromBytes(2023, []byte(tc.input)) - assert.NoError(t, err) - assert.Equal(t, tc.expected, members) - }) - } -} diff --git a/server/services/services_test.go b/server/services/services_test.go index 82489ef..67cd899 100644 --- a/server/services/services_test.go +++ b/server/services/services_test.go @@ -13,8 +13,8 @@ import ( const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=`~!@#$%^&*()_+[]\\;',./{}|:\"<>?" -var new_members = []struct{ - email string +var new_members = []struct { + email string member_name string }{ {"test1@mail.me", "test1"}, @@ -32,20 +32,20 @@ var new_members = []struct{ {"test105@mail.me", "test105"}, } -var new_polls = []struct{ - question string - member_yes_votes int64 - member_no_votes int64 +var new_polls = []struct { + question string + member_yes_votes int64 + member_no_votes int64 non_member_yes_votes int64 - non_member_no_votes int64 + non_member_no_votes int64 }{ {"ques1", 1, 2, 3, 4}, {"ques2", 3, 2, 4, 5}, {"ques3", 4, 3, 6, 5}, } -var new_voters = []struct{ - poll_id int64 +var new_voters = []struct { + poll_id int64 voter_email string }{ {1, "test1@mail.me"}, @@ -117,7 +117,7 @@ func PreLoadDB() error { // Insert polls for i := range new_polls { - result, err := db_conn.Exec(`INSERT INTO polls (question, member_yes_votes, member_no_votes, non_member_yes_votes, non_member_no_votes, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, new_polls[i].question, new_polls[i].member_yes_votes, new_polls[i].member_no_votes, new_polls[i].non_member_yes_votes, new_polls[i].non_member_no_votes, time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05")) + result, err := db_conn.Exec(`INSERT INTO polls (question, member_yes_votes, member_no_votes, non_member_yes_votes, non_member_no_votes, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, new_polls[i].question, new_polls[i].member_yes_votes, new_polls[i].member_no_votes, new_polls[i].non_member_yes_votes, new_polls[i].non_member_no_votes, time.Now().Add(time.Hour*10).Format("2006-01-02 15:04:05")) if err != nil { return err } @@ -139,8 +139,8 @@ func PreLoadDB() error { } func TestCreatePoll(t *testing.T) { - parameters := []struct{ - question string + parameters := []struct { + question string table_index int64 }{ {RandString(10) + "1", 1}, @@ -165,29 +165,29 @@ func TestCreatePoll(t *testing.T) { defer os.Remove(tmp_db.Name()) tmp_db.Close() - + if _, err := db.Connect(); err != nil { t.Errorf(`Failed to create the database: %v`, err) } for i := range parameters { create_poll := &models.Poll{ - Question: parameters[i].question, + Question: parameters[i].question, ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), } - new_poll, err := CreatePoll(create_poll) + new_poll_id, err := CreatePoll(create_poll) if err != nil { - t.Fatalf(`Failed to create new poll %s: %v`, parameters[i].question, err) + t.Errorf(`Failed to create new poll %s: %v`, parameters[i].question, err) } - if new_poll == nil { - t.Fatalf(`Failed to insert %s into table`, parameters[i].question) + if new_poll_id == -1 { + t.Errorf(`Failed to insert %s into table`, parameters[i].question) } - if new_poll.ID != parameters[i].table_index { - t.Fatalf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll.ID) + if new_poll_id != parameters[i].table_index { + t.Errorf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll_id) } } } @@ -207,30 +207,30 @@ func TestAlreadyExists(t *testing.T) { defer os.Remove(tmp_db.Name()) tmp_db.Close() - + if _, err := db.Connect(); err != nil { t.Errorf(`Failed to create the database: %v`, err) } create_poll := &models.Poll{ - Question: question, + Question: question, ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), } new_poll, err := CreatePoll(create_poll) if err != nil { - t.Fatalf(`Failed to create new poll %s: %v`, question, err) + t.Errorf(`Failed to create new poll %s: %v`, question, err) } - if new_poll == nil { - t.Fatalf(`Failed to insert %s into table`, question) + if new_poll == -1 { + t.Errorf(`Failed to insert %s into table`, question) } new_poll, err = CreatePoll(create_poll) if err != ErrQuestionAlreadyExists { - t.Fatalf(`Should have failed adding %s as it already exists`, question) + t.Errorf(`Should have failed adding %s as it already exists`, question) } } @@ -249,40 +249,40 @@ func TestGetPollByQuestion(t *testing.T) { defer os.Remove(tmp_db.Name()) tmp_db.Close() - + if _, err := db.Connect(); err != nil { t.Errorf(`Failed to create the database: %v`, err) } create_poll := &models.Poll{ - Question: question, + Question: question, ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), } new_poll, err := CreatePoll(create_poll) if err != nil { - t.Fatalf(`Failed to create new poll %s: %v`, question, err) + t.Errorf(`Failed to create new poll %s: %v`, question, err) } - if new_poll == nil { - t.Fatalf(`Failed to insert %s into table`, question) + if new_poll == -1 { + t.Errorf(`Failed to insert %s into table`, question) } get_poll, err := GetPollByQuestion(question) if err != nil { - t.Fatalf(`Failed to get the poll %s: %v`, question, err) + t.Errorf(`Failed to get the poll %s: %v`, question, err) } if get_poll.Question != question { - t.Fatalf(`Questions don't match: expected %s: recieved %s`, question, get_poll.Question) + t.Errorf(`Questions don't match: expected %s: recieved %s`, question, get_poll.Question) } } func TestGetCreatePollByQuestion(t *testing.T) { - parameters := []struct{ - question string + parameters := []struct { + question string table_index int64 }{ {RandString(10) + "1", 1}, @@ -305,7 +305,7 @@ func TestGetCreatePollByQuestion(t *testing.T) { defer os.Remove(tmp_db.Name()) tmp_db.Close() - + if _, err := db.Connect(); err != nil { t.Errorf(`Failed to create the database: %v`, err) } @@ -314,184 +314,184 @@ func TestGetCreatePollByQuestion(t *testing.T) { new_poll, err := GetAndCreatePollByQuestion(parameters[i].question) if err != nil { - t.Fatalf(`Failed to create new poll %s: %v`, parameters[i].question, err) + t.Errorf(`Failed to create new poll %s: %v`, parameters[i].question, err) } if new_poll == nil { - t.Fatalf(`Failed to insert %s into table`, parameters[i].question) + t.Errorf(`Failed to insert %s into table`, parameters[i].question) } if new_poll.ID != parameters[i].table_index { - t.Fatalf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll.ID) + t.Errorf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll.ID) } if new_poll.Question != parameters[i].question { - t.Fatalf(`Incorrect question returned: Expected %s != %s`, parameters[i].question, new_poll.Question) + t.Errorf(`Incorrect question returned: Expected %s != %s`, parameters[i].question, new_poll.Question) } } } func TestSetVote(t *testing.T) { - // Preload the database with members, polls, and voters - tmp_db, err := os.CreateTemp("", "vote_test.*.db") - if err != nil { - t.Fatalf("Failed to create temporary database: %v", err) - } - defer os.Remove(tmp_db.Name()) + // Preload the database with members, polls, and voters + tmp_db, err := os.CreateTemp("", "vote_test.*.db") + if err != nil { + t.Errorf("Failed to create temporary database: %v", err) + } + defer os.Remove(tmp_db.Name()) - init_conf := &config.Config{ - DBPath: string(tmp_db.Name()), - } - config.SetConfig(init_conf) + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) - err = PreLoadDB() - if err != nil { - t.Fatalf("Failed to preload database: %v", err) - } + err = PreLoadDB() + if err != nil { + t.Errorf("Failed to preload database: %v", err) + } - // Add a non-member vote - random_email := RandString(10) + "@mail.me" - vote := &models.Vote{ - PollId: 1, - Email: random_email, - Vote: true, - } - err = SetVote(vote) - if err != nil { - t.Fatalf("Failed to set non-member vote: %v", err) - } + // Add a non-member vote + random_email := RandString(10) + "@mail.me" + vote := &models.Vote{ + PollId: 1, + Email: random_email, + Vote: true, + } + err = SetVote(vote) + if err != nil { + t.Errorf("Failed to set non-member vote: %v", err) + } - // Add a member vote - member_email := "test100@mail.me" - vote = &models.Vote{ - PollId: 1, - Email: member_email, - Vote: true, - } - err = SetVote(vote) - if err != nil { - t.Fatalf("Failed to set member vote: %v", err) - } + // Add a member vote + member_email := "test100@mail.me" + vote = &models.Vote{ + PollId: 1, + Email: member_email, + Vote: true, + } + err = SetVote(vote) + if err != nil { + t.Errorf("Failed to set member vote: %v", err) + } - // Verify the votes were added correctly - voters, err := models.GetVoters(1) // Use GetVoters from models - if err != nil { - t.Fatalf("Failed to get voters: %v", err) - } + // Verify the votes were added correctly + voters, err := models.GetVoters(1) // Use GetVoters from models + if err != nil { + t.Errorf("Failed to get voters: %v", err) + } - expected_non_member_votes := 4 + 1 // Original non-member votes + new non-member vote - expected_member_votes := 3 + 1 // Original member votes + new member vote + expected_non_member_votes := 4 + 1 // Original non-member votes + new non-member vote + expected_member_votes := 3 + 1 // Original member votes + new member vote - for _, voter := range voters { - if voter.Email == random_email && voter.YesVote { - expected_non_member_votes-- - } else if voter.Email == member_email && voter.YesVote { - expected_member_votes-- - } - } + for _, voter := range voters { + if voter.Email == random_email && voter.YesVote { + expected_non_member_votes-- + } else if voter.Email == member_email && voter.YesVote { + expected_member_votes-- + } + } - if expected_non_member_votes != 5 || expected_member_votes != 4 { - t.Errorf("Expected %d non-member votes and %d member votes, but got %d non-member votes and %d member votes", 4+1, 3+1, expected_non_member_votes, expected_member_votes) - } + if expected_non_member_votes != 5 || expected_member_votes != 4 { + t.Errorf("Expected %d non-member votes and %d member votes, but got %d non-member votes and %d member votes", 4+1, 3+1, expected_non_member_votes, expected_member_votes) + } } func TestVoterAlreadyVoted(t *testing.T) { - // Preload the database with members, polls, and voters - tmp_db, err := os.CreateTemp("", "vote_test.*.db") - if err != nil { - t.Fatalf("Failed to create temporary database: %v", err) - } - defer os.Remove(tmp_db.Name()) + // Preload the database with members, polls, and voters + tmp_db, err := os.CreateTemp("", "vote_test.*.db") + if err != nil { + t.Errorf("Failed to create temporary database: %v", err) + } + defer os.Remove(tmp_db.Name()) - init_conf := &config.Config{ - DBPath: string(tmp_db.Name()), - } - config.SetConfig(init_conf) + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) - err = PreLoadDB() - if err != nil { - t.Fatalf("Failed to preload database: %v", err) - } + err = PreLoadDB() + if err != nil { + t.Errorf("Failed to preload database: %v", err) + } - // Add a non-member vote - random_email := RandString(10) + "@mail.me" - vote := &models.Vote{ - PollId: 1, - Email: random_email, - Vote: true, - } - err = SetVote(vote) - if err != nil { - t.Fatalf("Failed to set non-member vote: %v", err) - } + // Add a non-member vote + random_email := RandString(10) + "@mail.me" + vote := &models.Vote{ + PollId: 1, + Email: random_email, + Vote: true, + } + err = SetVote(vote) + if err != nil { + t.Errorf("Failed to set non-member vote: %v", err) + } - // Add a member vote - member_email := "test100@mail.me" - vote = &models.Vote{ - PollId: 1, - Email: member_email, - Vote: true, - } - err = SetVote(vote) - if err != nil { - t.Fatalf("Failed to set member vote: %v", err) - } + // Add a member vote + member_email := "test100@mail.me" + vote = &models.Vote{ + PollId: 1, + Email: member_email, + Vote: true, + } + err = SetVote(vote) + if err != nil { + t.Errorf("Failed to set member vote: %v", err) + } - // Attempt to add another non-member vote - vote = &models.Vote{ - PollId: 1, - Email: random_email, - Vote: true, - } - err = SetVote(vote) - if err != ErrVoterAlreadyVoted { - t.Fatalf("Expected ErrVoterAlreadyVoted, but got %v", err) - } + // Attempt to add another non-member vote + vote = &models.Vote{ + PollId: 1, + Email: random_email, + Vote: true, + } + err = SetVote(vote) + if err != ErrVoterAlreadyVoted { + t.Errorf("Expected ErrVoterAlreadyVoted, but got %v", err) + } - // Attempt to add another member vote - vote = &models.Vote{ - PollId: 1, - Email: member_email, - Vote: true, - } - err = SetVote(vote) - if err != ErrVoterAlreadyVoted { - t.Fatalf("Expected ErrVoterAlreadyVoted, but got %v", err) - } + // Attempt to add another member vote + vote = &models.Vote{ + PollId: 1, + Email: member_email, + Vote: true, + } + err = SetVote(vote) + if err != ErrVoterAlreadyVoted { + t.Errorf("Expected ErrVoterAlreadyVoted, but got %v", err) + } } func TestDeletePollByQuestion(t *testing.T) { - // Preload the database with members, polls, and voters - tmp_db, err := os.CreateTemp("", "vote_test.*.db") - if err != nil { - t.Fatalf("Failed to create temporary database: %v", err) - } - defer os.Remove(tmp_db.Name()) + // Preload the database with members, polls, and voters + tmp_db, err := os.CreateTemp("", "vote_test.*.db") + if err != nil { + t.Errorf("Failed to create temporary database: %v", err) + } + defer os.Remove(tmp_db.Name()) - init_conf := &config.Config{ - DBPath: string(tmp_db.Name()), - } - config.SetConfig(init_conf) + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) - err = PreLoadDB() - if err != nil { - t.Fatalf("Failed to preload database: %v", err) - } + err = PreLoadDB() + if err != nil { + t.Errorf("Failed to preload database: %v", err) + } - // Get a question from the new_polls array - testQuestion := new_polls[0].question + // Get a question from the new_polls array + testQuestion := new_polls[0].question - // Delete the poll by question - err = DeletePollByQuestion(testQuestion) - if err != nil { - t.Fatalf("Failed to delete poll by question: %v", err) - } + // Delete the poll by question + err = DeletePollByQuestion(testQuestion) + if err != nil { + t.Errorf("Failed to delete poll by question: %v", err) + } - // Verify that the poll was deleted - _, err = GetPollByQuestion(testQuestion) - if err == nil { - t.Fatalf("Expected error when getting deleted poll, but got none") - } else if err != ErrPollNotFound { - t.Fatalf("Expected ErrPollNotFound, but got %v", err) - } -} \ No newline at end of file + // Verify that the poll was deleted + _, err = GetPollByQuestion(testQuestion) + if err == nil { + t.Errorf("Expected error when getting deleted poll, but got none") + } else if err != ErrPollNotFound { + t.Errorf("Expected ErrPollNotFound, but got %v", err) + } +}