From 54a95143cb4536a31d3ccbbe05803395ae5a9f2a Mon Sep 17 00:00:00 2001 From: mroley1 <64291507+mroley1@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:31:27 -0400 Subject: [PATCH 01/76] has been annoying me for a while --- frontend/stylesheets/pages/_evals.scss | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/stylesheets/pages/_evals.scss b/frontend/stylesheets/pages/_evals.scss index b134244c..7488542e 100644 --- a/frontend/stylesheets/pages/_evals.scss +++ b/frontend/stylesheets/pages/_evals.scss @@ -61,8 +61,19 @@ border: 0; background: transparent; width: 100%; - height: 0; text-align: center; + display: block; + } + + &[aria-expanded="true"] { + span.glyphicon { + transform: rotate(180deg); + } + } + + span.glyphicon { + transition: transform ease-in-out 0.2s; + transform: rotate(0deg); } } From 76f193c7fb1b3c2a39db657e7d993afb6df2e2e1 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:44:27 -0400 Subject: [PATCH 02/76] Added line to major project form --- .../templates/major_project_submission.html | 9 + package-lock.json | 742 +++++++----------- package.json | 8 +- 3 files changed, 302 insertions(+), 457 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 8705d2ad..cb7c5ac0 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -16,6 +16,15 @@

Major Project Form

+
+
+
+ + +
+
+
diff --git a/package-lock.json b/package-lock.json index e232319f..4688860d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "conditional", - "version": "1.10.3", + "version": "1.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -67,6 +67,12 @@ "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=", "dev": true }, + "a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -184,10 +190,10 @@ "ansi-wrap": "0.1.0" } }, - "ansi-html": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.5.tgz", - "integrity": "sha1-DcqloIEgaGa8JAo7dzoYTqO4i2Q=", + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true }, "ansi-red": { @@ -363,7 +369,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, "asn1": { @@ -424,9 +430,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "requires": { "lodash": "^4.17.14" } @@ -1487,9 +1493,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "optional": true }, @@ -1533,40 +1539,14 @@ } }, "bl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", - "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "optional": true, "requires": { - "readable-stream": "~2.0.5" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "blob": { @@ -1854,9 +1834,9 @@ } }, "bytes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz", - "integrity": "sha1-1baAoWW2IBc5rLYRVCqrwtjOsHA=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true }, "cache-base": { @@ -2381,48 +2361,49 @@ } }, "compression": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.6.2.tgz", - "integrity": "sha1-zOsSHsydCcUtetDDNQ6pPd1AK8M=", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, "requires": { - "accepts": "~1.3.3", - "bytes": "2.3.0", - "compressible": "~2.0.8", - "debug": "~2.2.0", - "on-headers": "~1.0.1", - "vary": "~1.1.0" + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" }, "dependencies": { "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" } }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "ms": "0.7.1" + "mime-db": "1.52.0" } }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true } } @@ -2774,11 +2755,11 @@ } }, "datatables.net": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.2.tgz", - "integrity": "sha512-Dg+Yeyky+BhXoooJqTDTGssLxkAxe73CljcjYd4630XQZKqqqy0akYk2r4RSHSJLyYCOGRZEZiUF/N39+5nqvg==", + "version": "1.13.11", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.11.tgz", + "integrity": "sha512-AE6RkMXziRaqzPcu/pl3SJXeRa6fmXQG/fVjuRESujvkzqDCYEeKTTpPMuVJSGYJpPi32WGSphVNNY1G4nSN/g==", "requires": { - "jquery": ">=1.7" + "jquery": "1.8 - 4" } }, "datatables.net-bs": { @@ -4061,14 +4042,23 @@ } }, "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "dependencies": { + "next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + } } }, "es6-iterator": { @@ -4248,6 +4238,26 @@ "integrity": "sha1-+RZ2VDK6Z9L8enF3uLz+8/brBWQ=", "dev": true }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + } + } + }, "espree": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", @@ -4462,18 +4472,18 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" } }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -4482,9 +4492,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true } } @@ -4585,12 +4595,6 @@ "is-extglob": "^1.0.0" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -5690,9 +5694,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "semver": { @@ -6089,217 +6093,40 @@ } }, "gulp-nunjucks-render": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-nunjucks-render/-/gulp-nunjucks-render-2.0.0.tgz", - "integrity": "sha1-z0hQasXAWtiQIyYKMPaVHyI7JZ8=", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/gulp-nunjucks-render/-/gulp-nunjucks-render-2.2.3.tgz", + "integrity": "sha512-YHrmwiwPw2DN16WJVj8S2tgUanB1ssZE8e5rRF6rH1T41mhO7r59iAqw92Yz5WSZXYB9G+uYXDTCODgixYPIRw==", "dev": true, "requires": { - "gulp-util": "~2.2.0", - "lodash": "^3.3.0", - "nunjucks": "^2.0.0", - "through2": "~0.4.0" + "lodash": "^4.17.11", + "nunjucks": "^3.1.2", + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "through2": "^2.0.3" }, "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true - }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true, - "requires": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" - } - }, - "gulp-util": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", - "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", - "dev": true, - "requires": { - "chalk": "^0.5.0", - "dateformat": "^1.0.7-1.2.3", - "lodash._reinterpolate": "^2.4.1", - "lodash.template": "^2.4.1", - "minimist": "^0.2.0", - "multipipe": "^0.1.0", - "through2": "^0.5.0", - "vinyl": "^0.2.1" - }, - "dependencies": { - "through2": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", - "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", - "dev": true, - "requires": { - "readable-stream": "~1.0.17", - "xtend": "~3.0.0" - } - } - } - }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "dev": true, - "requires": { - "ansi-regex": "^0.2.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", - "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", - "dev": true - }, - "lodash.escape": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", - "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", - "dev": true, - "requires": { - "lodash._escapehtmlchar": "~2.4.1", - "lodash._reunescapedhtml": "~2.4.1", - "lodash.keys": "~2.4.1" - } - }, - "lodash.keys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", - "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", - "dev": true, - "requires": { - "lodash._isnative": "~2.4.1", - "lodash._shimkeys": "~2.4.1", - "lodash.isobject": "~2.4.1" - } - }, - "lodash.template": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", - "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", - "dev": true, - "requires": { - "lodash._escapestringchar": "~2.4.1", - "lodash._reinterpolate": "~2.4.1", - "lodash.defaults": "~2.4.1", - "lodash.escape": "~2.4.1", - "lodash.keys": "~2.4.1", - "lodash.templatesettings": "~2.4.1", - "lodash.values": "~2.4.1" - } - }, - "lodash.templatesettings": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", - "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~2.4.1", - "lodash.escape": "~2.4.1" - } - }, - "minimist": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.1.tgz", - "integrity": "sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true, - "requires": { - "ansi-regex": "^0.2.1" - } - }, - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true }, - "through2": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", - "dev": true, - "requires": { - "readable-stream": "~1.0.17", - "xtend": "~2.1.1" - }, - "dependencies": { - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "vinyl": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", - "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, "requires": { - "clone-stats": "~0.0.1" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" } }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true } } @@ -7100,9 +6927,9 @@ "dev": true }, "html-entities": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", - "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true }, "html-minifier": { @@ -7625,16 +7452,24 @@ "dev": true }, "is-my-json-valid": { - "version": "2.20.5", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz", - "integrity": "sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A==", + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", "dev": true, "requires": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", + "jsonpointer": "^5.0.0", "xtend": "^4.0.0" + }, + "dependencies": { + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true + } } }, "is-natural-number": { @@ -7920,19 +7755,19 @@ "dev": true }, "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { "argparse": "^1.0.7", - "esprima": "^2.6.0" + "esprima": "^4.0.0" }, "dependencies": { "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true } } @@ -7949,12 +7784,6 @@ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -8009,22 +7838,12 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, - "jsonpointer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", - "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==", - "dev": true - }, "jsprim": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "resolved": "", "dev": true, "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "assert-plus": "1.0.0" }, "dependencies": { "assert-plus": { @@ -8897,9 +8716,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true } } @@ -8973,9 +8792,9 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -9023,9 +8842,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true } } @@ -9244,12 +9063,6 @@ "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=", "dev": true }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -9510,9 +9323,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true } } @@ -9609,8 +9422,7 @@ "dev": true, "requires": { "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" + "lodash": "~4.17.10" } }, "har-validator": { @@ -9834,53 +9646,21 @@ "dev": true }, "nunjucks": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-2.5.2.tgz", - "integrity": "sha1-6n00bnhbikh0Zmw8yp4YxXf7oiw=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", "dev": true, "requires": { + "a-sync-waterfall": "^1.0.0", "asap": "^2.0.3", - "chokidar": "^1.6.0", - "yargs": "^3.32.0" + "commander": "^5.1.0" }, "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", - "dev": true, - "requires": { - "camelcase": "^2.0.1", - "cliui": "^3.0.3", - "decamelize": "^1.1.1", - "os-locale": "^1.4.0", - "string-width": "^1.0.1", - "window-size": "^0.1.4", - "y18n": "^3.2.0" - } } } }, @@ -9924,12 +9704,6 @@ } } }, - "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, "object-path": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz", @@ -11026,9 +10800,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "optional": true } @@ -11576,11 +11350,46 @@ "tunnel-agent": "~0.4.1" }, "dependencies": { + "bl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha512-phbvN+yOk05EGoFcV/0S8N8ShnJqf6VCWRAw5he2gvRwBubFt/OzmcTNGqBt5b7Y4RK3YCgf6jrgGSR0Cwtsgw==", + "dev": true, + "requires": { + "readable-stream": "~2.0.5" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==", + "dev": true + }, "qs": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true } } }, @@ -11802,8 +11611,7 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "resolved": "", "dev": true }, "ansi-styles": { @@ -12029,6 +11837,17 @@ "glob": "~7.1.1", "lodash": "~4.17.10", "minimatch": "~3.0.2" + }, + "dependencies": { + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "gonzales-pe-sl": { @@ -12130,9 +11949,9 @@ "integrity": "sha1-myE4zYi8JQSdi7SnCcMzHxg+XJ4=" }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "semver-regex": { @@ -13113,9 +12932,9 @@ "optional": true }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "optional": true } @@ -13283,6 +13102,24 @@ "mkdirp": "~0.5.1", "sax": "~1.2.1", "whet.extend": "~0.9.9" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "dev": true + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha512-eIlkGty7HGmntbV6P/ZlAsoncFLGsNoM27lkTzS+oneY/EiNhj+geqD9ezg/ip+SW6Var0BJU2JtV0vEUZpWVQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + } } }, "svgpath": { @@ -13841,9 +13678,9 @@ "dev": true }, "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "dev": true }, "union-value": { @@ -14132,7 +13969,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, "vendors": { @@ -14141,31 +13978,6 @@ "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", "dev": true }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - } - } - }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", @@ -14495,15 +14307,31 @@ } }, "webpack-hot-middleware": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.12.1.tgz", - "integrity": "sha1-Qlf5ZeaKa4HaW93b1h+J+pB170w=", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", + "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", "dev": true, "requires": { - "ansi-html": "0.0.5", - "html-entities": "^1.2.0", - "querystring": "^0.2.0", - "strip-ansi": "^3.0.0" + "ansi-html-community": "0.0.8", + "html-entities": "^2.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "weinre": { @@ -14515,6 +14343,14 @@ "express": "2.5.x", "nopt": "3.0.x", "underscore": "1.7.x" + }, + "dependencies": { + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", + "dev": true + } } }, "whatwg-fetch": { @@ -14559,9 +14395,9 @@ "dev": true }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wordwrap": { diff --git a/package.json b/package.json index efcb531e..970e23b1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "bootstrap-sass": "^3.4.1", "bootstrap-sweetalert": "^1.0.1", "csh-material-bootstrap": "1.0.0", - "datatables.net": "^1.10.12", + "datatables.net": "^1.13.11", "datatables.net-bs": "^1.10.12", "dropzone": "^4.3.0", "enumify": "^1.0.4", @@ -51,7 +51,7 @@ "babel-preset-stage-1": "6.5.0", "browser-sync": "2.13.0", "chai": "3.5.0", - "compression": "1.6.2", + "compression": "^1.7.4", "del": "2.2.1", "eslint-config-google": "^0.6.0", "exports-loader": "^0.6.3", @@ -69,7 +69,7 @@ "gulp-if": "2.0.1", "gulp-imagemin": "3.0.1", "gulp-notify": "2.2.0", - "gulp-nunjucks-render": "2.0.0", + "gulp-nunjucks-render": "^2.2.3", "gulp-rename": "1.2.2", "gulp-rev": "7.1.0", "gulp-rev-napkin": "0.1.0", @@ -95,6 +95,6 @@ "sinon-chai": "2.8.0", "webpack": "1.13.1", "webpack-dev-middleware": "1.6.1", - "webpack-hot-middleware": "2.12.1" + "webpack-hot-middleware": "^2.26.1" } } From a20bfe3b17bfb3c70fbd79213e314128145bdc59 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 1 Sep 2024 23:43:38 -0400 Subject: [PATCH 03/76] Added rows to the form so I'm testing it --- .../templates/major_project_submission.html | 197 ++++++++++-------- 1 file changed, 114 insertions(+), 83 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index cb7c5ac0..8dc59378 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,103 +1,134 @@ {% extends "nav.html" %} {% block title %} -Major Project Form + Major Project Form {% endblock %} {% block body %} -
-

Major Project Form

-
-
-
-
- - +
+

Major Project Form

+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
-
-
-
-
- - +
+
+
+ + +
-
-
-
-
- - +
+
+
+ + +
+ + +

All Major Projects

{% if major_projects_len + <=0 %} +
+
+

No Pending Major Projects

+
- - -

All Major Projects

{% if major_projects_len - <=0 %}
-
-

No Pending Major Projects

-
-
-{% else %} + {% else %} - {% for p in major_projects %} -
-
-
-

{{p['proj_name']}}

- - {{p['name']}} ({{p['username']}}) -
-
+ {% for p in major_projects %} +
+
+
+

{{ p['proj_name'] }}

+ + {{ p['name'] }} ({{ p['username'] }}) +
+
- {% if is_eval_director %} + {% if is_eval_director %} -
- - + + {% else %} + {% if p['status'] == 'Passed' %} +
+ {% elif p['status'] == 'Failed' %} +
+ {% else %} +
+ {% endif %} + {% if p.is_owner and p['status'] == 'Pending' %} + + {% endif %} + {% endif %} +
+ +
+ {{ p['description'] }} +
- {% else %} - {% if p['status'] == 'Passed' %} -
- {% elif p['status'] == 'Failed' %} -
- {% else %} -
- {% endif %} - {% if p.is_owner and p['status'] == 'Pending' %} - - {% endif %} - {% endif %}
- -
- {{p['description']}} -
-
-
- {% endfor %} + {% endfor %} -{% endif %} + {% endif %} -
+
{% endblock %} From 7aae71eec14a6e29efe399c2c131c60991f402f0 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 1 Sep 2024 23:58:18 -0400 Subject: [PATCH 04/76] More parts of form --- .../templates/major_project_submission.html | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 8dc59378..35852531 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -8,25 +8,23 @@

Major Project Form

-
-
- - -
-
- - -
+
+ + + +
+ placeholder="A comma-separated list of skills, similar to what you'd put on a resume.">
- + +
From fbfbbb945ef28e9b2650e98a7ea37a72b6c54fe4 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:57:16 -0400 Subject: [PATCH 05/76] Make text larger hopefully --- conditional/templates/major_project_submission.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 35852531..63ca5b81 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -8,21 +8,21 @@

Major Project Form

-
- +
+ - - Short Description / TL;DR +
- +
- +
From eb8531f7ff52f16454938ba851622cc13defffe9 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:21:47 -0400 Subject: [PATCH 06/76] changed font size --- conditional/templates/major_project_submission.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 63ca5b81..da7fa6b0 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -9,20 +9,20 @@

Major Project Form

- + - +
- +
- +
From 7a6d7431521cc38649ec41be3b718d8cba72b8da Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:49:09 -0400 Subject: [PATCH 07/76] push styles --- frontend/stylesheets/pages/_major-project.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 01438510..9b134dd3 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -1,3 +1,9 @@ .major-project-desc { white-space: pre-line; } + +.form-label { + margin-bottom: 0; + margin-top: 1.25rem; + font-size: 1.5rem; +} \ No newline at end of file From 342578d74a6eaca98f7f62138961ed010bec9658 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:30:44 -0400 Subject: [PATCH 08/76] fixed build issue --- Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8097c784..8c38a30a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,15 @@ RUN apt-get -yq update && \ ADD . /opt/conditional -RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ - apt-get -yq update && \ - apt-get -yq install nodejs && \ - npm install && \ - npm run production && \ - rm -rf node_modules && \ +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION v10.24.1 +RUN mkdir -p $NVM_DIR + +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION && npm run production" + +RUN rm -rf node_modules && \ apt-get -yq remove nodejs npm && \ apt-get -yq clean all From 3ded9cf516c44ca505de6e195ab584a61b0df7b8 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:38:51 -0400 Subject: [PATCH 09/76] run npm install --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8c38a30a..88687d8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN mkdir -p $NVM_DIR RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash -RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION && npm run production" +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION && npm install && npm run production" RUN rm -rf node_modules && \ apt-get -yq remove nodejs npm && \ From bbd97e9043a905feb97f4c6835c7edc13baaf203 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:32:42 -0400 Subject: [PATCH 10/76] Added dropzone (but it isnt functioning yet) --- Dockerfile | 11 +++--- .../templates/major_project_submission.html | 34 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 88687d8d..3148d474 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM docker.io/python:3.8-buster -MAINTAINER Devin Matte +MAINTAINER Computer Science House RUN mkdir /opt/conditional @@ -12,18 +12,19 @@ RUN apt-get -yq update && \ pip install -r requirements.txt && \ apt-get -yq clean all -ADD . /opt/conditional - ENV NVM_DIR /usr/local/nvm ENV NODE_VERSION v10.24.1 RUN mkdir -p $NVM_DIR RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash -RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION && npm install && npm run production" +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION" + +ADD . /opt/conditional + +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm use --delete-prefix $NODE_VERSION && npm install && npm run production" RUN rm -rf node_modules && \ - apt-get -yq remove nodejs npm && \ apt-get -yq clean all RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index da7fa6b0..49cb2359 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -6,6 +6,10 @@

Major Project Form

+ +
+

This talks about major project submission guidelines

+
@@ -23,10 +27,38 @@

Major Project Form

-
+ + +
+
+ + +
+
+ +
+
+ +
Upload Photos and Videos
+
Drag files here or click to upload.
+
+
+ +
+
From d0b566a2214af9c5af8547e7f4aabd67a271d80c Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:42:56 -0400 Subject: [PATCH 11/76] edited upload form --- conditional/templates/major_project_submission.html | 6 +++--- frontend/images/photo_video.svg | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 frontend/images/photo_video.svg diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 49cb2359..a03075c3 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -36,16 +36,16 @@

Major Project Form

-
+
-
+
- +
Upload Photos and Videos
Drag files here or click to upload.
diff --git a/frontend/images/photo_video.svg b/frontend/images/photo_video.svg new file mode 100644 index 00000000..871848a5 --- /dev/null +++ b/frontend/images/photo_video.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file From d736f2f5db1e58511362a366dd1462a71c65d5a1 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:50:05 -0400 Subject: [PATCH 12/76] edited photo --- frontend/images/photo_video.svg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/images/photo_video.svg b/frontend/images/photo_video.svg index 871848a5..2324ec73 100644 --- a/frontend/images/photo_video.svg +++ b/frontend/images/photo_video.svg @@ -1,4 +1,6 @@ - + + \ No newline at end of file From 5a1bac7b5cf91cf57ee549e6a47f2c8876c9fe26 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:11:24 -0400 Subject: [PATCH 13/76] form stylistically done --- .../templates/major_project_submission.html | 16 ++++++++++++---- frontend/stylesheets/pages/_major-project.scss | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index a03075c3..b14ad851 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -8,7 +8,15 @@

Major Project Form

-

This talks about major project submission guidelines

+
+

This talks about major project submission guidelines. Rubber cheese chalk and cheese + pepper jack. Lancashire monterey jack say cheese roquefort stinking bishop danish fontina manchego + cheese and wine. Cheeseburger cow dolcelatte feta fondue pecorino roquefort cauliflower cheese. St. + agur blue cheese danish fontina emmental stilton smelly cheese bavarian bergkase fromage cheesy + feet. Paneer stilton halloumi ricotta macaroni cheese stinking bishop cottage cheese camembert de + normandie. Danish fontina cheese triangles squirty cheese cheese on toast camembert de + normandie.

+
@@ -41,10 +49,10 @@

Major Project Form

-
+
-
-
+
+
Upload Photos and Videos
Drag files here or click to upload.
diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 9b134dd3..e95cf073 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -5,5 +5,5 @@ .form-label { margin-bottom: 0; margin-top: 1.25rem; - font-size: 1.5rem; + font-size: 2rem; } \ No newline at end of file From d14312f5fbd1038dbc335b9c326a4e15a34554fd Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:14:28 -0400 Subject: [PATCH 14/76] dropzone should work --- .../templates/major_project_submission.html | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index b14ad851..a1a4ba9e 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -54,39 +54,22 @@

Major Project Form

-
Upload Photos and Videos
+
Upload Media
Drag files here or click to upload.
-
-
-
- - -
-
-
-
-
-
- - -
-
-
From fbfc95947a63277522a7b0dc79d85bdb727c739f Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:46:24 -0400 Subject: [PATCH 15/76] Dropzone.js --- .../templates/major_project_submission.html | 8 ------ frontend/javascript/modules/dropzone.js | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index a1a4ba9e..90e38fff 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -58,14 +58,6 @@

Major Project Form

Drag files here or click to upload.
-
diff --git a/frontend/javascript/modules/dropzone.js b/frontend/javascript/modules/dropzone.js index 2b04a87e..d9da7c50 100644 --- a/frontend/javascript/modules/dropzone.js +++ b/frontend/javascript/modules/dropzone.js @@ -1,18 +1,26 @@ import Dropzone from "dropzone"; + Dropzone.autoDiscover = false; export default class DropzoneUpload { - constructor(element) { - this.element = element; - this.render(); - } + constructor(element) { + this.element = element; + this.render(); + } - render() { - const dz = new Dropzone(this.element); // eslint-disable-line new-cap - dz.on("complete", () => window.location.reload()); - } + render() { + const dz = new Dropzone(this.element); // eslint-disable-line new-cap + dz.on("complete", () => window.location.reload()); + } } Dropzone.options.uploadUser = { - acceptedFiles: ".csv" + acceptedFiles: ".csv" }; + +Dropzone.options.projectFiles = { + // autoQueue:false, + parallelUploads: 2, + uploadMultiple: true, + acceptedFiles: "audio/*,photo/*,video/*,.pdf,.doc,.docx" +} From 9adfbd8eabb0b48b4073578a00635233d938b4f8 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:55:26 -0400 Subject: [PATCH 16/76] dropzone doesn't like me --- conditional/templates/major_project_submission.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 90e38fff..4ea8dd52 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -50,8 +50,7 @@

Major Project Form

placeholder="If you have links (GitHub, YouTube, etc.), put them here!">
- -
+
Upload Media
From adf5e5db8d8515661e5bd6c36fc3baf82fb0df61 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:51:06 -0400 Subject: [PATCH 17/76] conditional/templates/major_project_submission.html --- conditional/templates/major_project_submission.html | 4 ++-- frontend/javascript/modules/dropzone.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 4ea8dd52..2152beab 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -50,8 +50,8 @@

Major Project Form

placeholder="If you have links (GitHub, YouTube, etc.), put them here!">
-
-
+
+
Upload Media
Drag files here or click to upload.
diff --git a/frontend/javascript/modules/dropzone.js b/frontend/javascript/modules/dropzone.js index d9da7c50..ee0d55df 100644 --- a/frontend/javascript/modules/dropzone.js +++ b/frontend/javascript/modules/dropzone.js @@ -20,7 +20,9 @@ Dropzone.options.uploadUser = { Dropzone.options.projectFiles = { // autoQueue:false, + url: "/major_project/upload", parallelUploads: 2, uploadMultiple: true, acceptedFiles: "audio/*,photo/*,video/*,.pdf,.doc,.docx" } +console.load("loaded") \ No newline at end of file From 1aefd4c538c002585dc8ad0cb4f6e1b05ebb42d9 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:51:36 -0400 Subject: [PATCH 18/76] I don't know what commands do what --- conditional/templates/major_project_submission.html | 12 ++++++++++++ frontend/javascript/modules/dropzone.js | 9 --------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 2152beab..9073aaa7 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,4 +1,7 @@ {% extends "nav.html" %} +{% block extraHeader %} + +{% endblock %} {% block title %} Major Project Form {% endblock %} @@ -64,6 +67,15 @@

Major Project Form

+

All Major Projects

{% if major_projects_len <=0 %}
diff --git a/frontend/javascript/modules/dropzone.js b/frontend/javascript/modules/dropzone.js index ee0d55df..959f2b19 100644 --- a/frontend/javascript/modules/dropzone.js +++ b/frontend/javascript/modules/dropzone.js @@ -17,12 +17,3 @@ export default class DropzoneUpload { Dropzone.options.uploadUser = { acceptedFiles: ".csv" }; - -Dropzone.options.projectFiles = { - // autoQueue:false, - url: "/major_project/upload", - parallelUploads: 2, - uploadMultiple: true, - acceptedFiles: "audio/*,photo/*,video/*,.pdf,.doc,.docx" -} -console.load("loaded") \ No newline at end of file From d5f968be4682388c33e849554999c7ab4b1d7fd3 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:26:47 -0400 Subject: [PATCH 19/76] S3 support added --- .../blueprints/major_project_submission.py | 35 ++++++++++++++++++- .../templates/major_project_submission.html | 30 ++++++++-------- config.env.py | 4 +++ .../stylesheets/pages/_major-project.scss | 4 +++ requirements.in | 2 ++ requirements.txt | 2 ++ 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index f8278d16..bb3e2f85 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -1,5 +1,9 @@ import json +import os + import requests +import boto3 +from botocore.exceptions import ClientError from flask import Blueprint from flask import request @@ -9,6 +13,7 @@ from sqlalchemy import desc import structlog +from werkzeug.utils import secure_filename from conditional.util.context_processors import get_member_name @@ -53,6 +58,25 @@ def display_major_project(user_dict=None): major_projects_len=major_projects_len, username=user_dict['username']) +@major_project_bp.route('/major_project/upload', methods=['POST']) +@auth.oidc_auth +@get_user +def upload_major_project_files(user_dict=None): + log = logger.new(request=request, auth_dict=user_dict) + log.info('Uploading Major Project File(s)') + + file = request.files['file'] + if not file: + return "No file", 400 + + # Temporarily save files to a place, to be uploaded on submit + + safe_name = secure_filename(file.filename) + filename = f"/tmp/{user_dict['username']}/{safe_name}" + + os.makedirs(os.path.dirname(filename), exist_ok=True) + file.save(filename) + @major_project_bp.route('/major_project/submit', methods=['POST']) @auth.oidc_auth @@ -65,10 +89,19 @@ def submit_major_project(user_dict=None): name = post_data['projectName'] description = post_data['projectDescription'] - if name == "" or description == "": + if name == "" or len(description.strip().split()) < 50: return jsonify({"success": False}), 400 project = MajorProject(user_dict['username'], name, description) + # Acquire S3 Bucket instance + s3 = boto3.resource("s3") + bucket = s3.create_bucket(Bucket="major-project-media") + # Collect all the locally cached files and put them in the bucket + for file in os.listdir(f"/tmp/{user_dict['username']}"): + bucket.upload_file(file, file.split("/")[-1]) + os.remove(file) + os.rmdir(f"/tmp/{user_dict['username']}") + username = user_dict['username'] send_slack_ping({"text":f" *{get_member_name(username)}* ({username})" f" submitted their major project, *{name}*!"}) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 9073aaa7..cf3f39dd 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,6 +1,6 @@ {% extends "nav.html" %} {% block extraHeader %} - + {% endblock %} {% block title %} Major Project Form @@ -12,13 +12,13 @@

Major Project Form

-

This talks about major project submission guidelines. Rubber cheese chalk and cheese - pepper jack. Lancashire monterey jack say cheese roquefort stinking bishop danish fontina manchego - cheese and wine. Cheeseburger cow dolcelatte feta fondue pecorino roquefort cauliflower cheese. St. - agur blue cheese danish fontina emmental stilton smelly cheese bavarian bergkase fromage cheesy - feet. Paneer stilton halloumi ricotta macaroni cheese stinking bishop cottage cheese camembert de - normandie. Danish fontina cheese triangles squirty cheese cheese on toast camembert de - normandie.

+

Hi! Welcome to the Major Project submission form. We're excited to read about your major + project. For us(E-Board) to best evaluate your project, please give us as much detail as possible. + Don't feel pressured to write full paragraphs though, good bullet points are plenty!
Generally, + major projects are most likely to pass when they meet three of the 4 Major Project Pillars - + spending considerable time on your project, benefiting House, learning new things, and applying hard + skills. And of course, after you submit, please try to talk to E-Board members (in-person or over + Slack) so we are familiar with your project and can ask you questions!

@@ -33,23 +33,25 @@

Major Project Form

- +
- + placeholder="We're looking for a considerable amount of time spent on your major project, we suggest ~5 hours minimum.">
-
-
diff --git a/config.env.py b/config.env.py index 44e0464e..29c0689b 100644 --- a/config.env.py +++ b/config.env.py @@ -25,6 +25,10 @@ LDAP_BIND_DN = env.get("CONDITIONAL_LDAP_BIND_DN", "cn=conditional,ou=Apps,dc=csh,dc=rit,dc=edu") LDAP_BIND_PW = env.get("CONDITIONAL_LDAP_BIND_PW", "") +# S3 information +AWS_ACCESS_KEY_ID = env.get("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = env.get("AWS_SECRET_ACCESS_KEY", "") + # Sentry config # Not required for local development, but if you set it, make sure the # SENTRY_ENV is 'local-development' diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index e95cf073..aefea190 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -6,4 +6,8 @@ margin-bottom: 0; margin-top: 1.25rem; font-size: 2rem; +} + +.form-textarea { + resize: vertical; } \ No newline at end of file diff --git a/requirements.in b/requirements.in index 5a7b246c..5636cd6d 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,8 @@ alembic~=0.9.8 astroid~=2.4.0 blinker~=1.4 +boto3==1.35.13 +botocore==1.35.13 click~=7.1 csh_ldap>=2.3.1 ddtrace~=1.1.4 diff --git a/requirements.txt b/requirements.txt index e6cff61e..3bc182c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,8 @@ blinker==1.4 # via # -r requirements.in # sentry-sdk +boto3==1.35.13 +botocore==1.35.13 certifi==2022.5.18.1 # via # requests From 73d04492d0ae16f98b992735756286dc16130aae Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:46:18 -0400 Subject: [PATCH 20/76] debug --- conditional/blueprints/major_project_submission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index bb3e2f85..892a10fe 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -65,6 +65,7 @@ def upload_major_project_files(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Uploading Major Project File(s)') + print(request.files) file = request.files['file'] if not file: return "No file", 400 From 7a69ff773f61cda5c7afe9e157d286e49db01873 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:00:44 -0400 Subject: [PATCH 21/76] ImmutableMultiDict is not a dict. changed accordingly? --- conditional/blueprints/major_project_submission.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 892a10fe..7ad51cbd 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -66,17 +66,17 @@ def upload_major_project_files(user_dict=None): log.info('Uploading Major Project File(s)') print(request.files) - file = request.files['file'] - if not file: + if len(request.files.keys()) < 1: return "No file", 400 # Temporarily save files to a place, to be uploaded on submit - safe_name = secure_filename(file.filename) - filename = f"/tmp/{user_dict['username']}/{safe_name}" + for _, file in request.files.iterlists(): + safe_name = secure_filename(file.filename) + filename = f"/tmp/{user_dict['username']}/{safe_name}" - os.makedirs(os.path.dirname(filename), exist_ok=True) - file.save(filename) + os.makedirs(os.path.dirname(filename), exist_ok=True) + file.save(filename) @major_project_bp.route('/major_project/submit', methods=['POST']) From 921b8b46af69d61f4cf6b6c34e514a3d10a3eae3 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:06:31 -0400 Subject: [PATCH 22/76] You need to wrap keys in a list to read its length --- conditional/blueprints/major_project_submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 7ad51cbd..c1ab84b0 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -66,7 +66,7 @@ def upload_major_project_files(user_dict=None): log.info('Uploading Major Project File(s)') print(request.files) - if len(request.files.keys()) < 1: + if len(list(request.files.keys())) < 1: return "No file", 400 # Temporarily save files to a place, to be uploaded on submit From c0b9c48fe08487c0ff6a75f6e73e021b4fa843dd Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:11:07 -0400 Subject: [PATCH 23/76] You need to wrap keys in a list to read its length --- conditional/blueprints/major_project_submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index c1ab84b0..67c6601e 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -71,7 +71,7 @@ def upload_major_project_files(user_dict=None): # Temporarily save files to a place, to be uploaded on submit - for _, file in request.files.iterlists(): + for _, file in request.files.lists(): safe_name = secure_filename(file.filename) filename = f"/tmp/{user_dict['username']}/{safe_name}" From d6daa6452f4942408fd6b3695ed1ae8360039b46 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:18:19 -0400 Subject: [PATCH 24/76] debug 2 --- conditional/blueprints/major_project_submission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 67c6601e..fa0d6150 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -71,6 +71,7 @@ def upload_major_project_files(user_dict=None): # Temporarily save files to a place, to be uploaded on submit + print(request.files.lists()) for _, file in request.files.lists(): safe_name = secure_filename(file.filename) filename = f"/tmp/{user_dict['username']}/{safe_name}" From 29e1fcdb6411fa74b432d1931e6828cd89ddda51 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:34:00 -0400 Subject: [PATCH 25/76] more debug --- conditional/blueprints/major_project_submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index fa0d6150..5299e6bc 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -71,8 +71,8 @@ def upload_major_project_files(user_dict=None): # Temporarily save files to a place, to be uploaded on submit - print(request.files.lists()) for _, file in request.files.lists(): + print(file) safe_name = secure_filename(file.filename) filename = f"/tmp/{user_dict['username']}/{safe_name}" From fc12aa2fd0d94ba6cd7ba63006cc77f2c85a0b4d Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:38:24 -0400 Subject: [PATCH 26/76] should be able to obtain files now --- conditional/blueprints/major_project_submission.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 5299e6bc..3979a516 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -72,8 +72,7 @@ def upload_major_project_files(user_dict=None): # Temporarily save files to a place, to be uploaded on submit for _, file in request.files.lists(): - print(file) - safe_name = secure_filename(file.filename) + safe_name = secure_filename(file[0].filename) filename = f"/tmp/{user_dict['username']}/{safe_name}" os.makedirs(os.path.dirname(filename), exist_ok=True) From 2f42078429abd723aa59c2ed6a1104578c846422 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:53:15 -0400 Subject: [PATCH 27/76] whoever designed this confuses me immensely --- conditional/blueprints/major_project_submission.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 3979a516..d23eaea9 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -72,7 +72,8 @@ def upload_major_project_files(user_dict=None): # Temporarily save files to a place, to be uploaded on submit for _, file in request.files.lists(): - safe_name = secure_filename(file[0].filename) + file = file[0] # remove it from the list because this is not the best + safe_name = secure_filename(file.filename) filename = f"/tmp/{user_dict['username']}/{safe_name}" os.makedirs(os.path.dirname(filename), exist_ok=True) From b9a2f3344146384b04ceb769f7097a364e02cfb6 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:16:43 -0400 Subject: [PATCH 28/76] s3 endpoint added --- conditional/blueprints/major_project_submission.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index d23eaea9..352acc0f 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -65,7 +65,6 @@ def upload_major_project_files(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Uploading Major Project File(s)') - print(request.files) if len(list(request.files.keys())) < 1: return "No file", 400 @@ -79,6 +78,9 @@ def upload_major_project_files(user_dict=None): os.makedirs(os.path.dirname(filename), exist_ok=True) file.save(filename) + return jsonify({"success": True}), 200 + + @major_project_bp.route('/major_project/submit', methods=['POST']) @auth.oidc_auth @@ -91,12 +93,12 @@ def submit_major_project(user_dict=None): name = post_data['projectName'] description = post_data['projectDescription'] - if name == "" or len(description.strip().split()) < 50: + if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum return jsonify({"success": False}), 400 project = MajorProject(user_dict['username'], name, description) # Acquire S3 Bucket instance - s3 = boto3.resource("s3") + s3 = boto3.resource("s3", endpoint_url="https://s3.csh.rit.edu") bucket = s3.create_bucket(Bucket="major-project-media") # Collect all the locally cached files and put them in the bucket for file in os.listdir(f"/tmp/{user_dict['username']}"): From b73a8b90b6c893b024f9424096b43d87aeb48c42 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:26:11 -0400 Subject: [PATCH 29/76] os file debug --- conditional/blueprints/major_project_submission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 352acc0f..1a4cd0e8 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -102,6 +102,7 @@ def submit_major_project(user_dict=None): bucket = s3.create_bucket(Bucket="major-project-media") # Collect all the locally cached files and put them in the bucket for file in os.listdir(f"/tmp/{user_dict['username']}"): + print(file) bucket.upload_file(file, file.split("/")[-1]) os.remove(file) os.rmdir(f"/tmp/{user_dict['username']}") From b78f86abf3d9328b0338b706caab77d8fc2997f7 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:46:11 -0400 Subject: [PATCH 30/76] 99% sure it will upload to the bucket now --- conditional/blueprints/major_project_submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 1a4cd0e8..e142e4ec 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -103,7 +103,7 @@ def submit_major_project(user_dict=None): # Collect all the locally cached files and put them in the bucket for file in os.listdir(f"/tmp/{user_dict['username']}"): print(file) - bucket.upload_file(file, file.split("/")[-1]) + bucket.upload_file(f"/tmp/{user_dict['username']}/{file}", file) os.remove(file) os.rmdir(f"/tmp/{user_dict['username']}") From 788b8f159b0c84bf7bf848e966b0ac1b3a34d804 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:18:50 -0400 Subject: [PATCH 31/76] remove files from tmp --- conditional/blueprints/major_project_submission.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index e142e4ec..47fbf434 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -102,9 +102,10 @@ def submit_major_project(user_dict=None): bucket = s3.create_bucket(Bucket="major-project-media") # Collect all the locally cached files and put them in the bucket for file in os.listdir(f"/tmp/{user_dict['username']}"): - print(file) - bucket.upload_file(f"/tmp/{user_dict['username']}/{file}", file) - os.remove(file) + filepath = f"/tmp/{user_dict['username']}/{file}" + print(filepath) + bucket.upload_file(filepath, f"{project.id}-{file}") + os.remove(filepath) os.rmdir(f"/tmp/{user_dict['username']}") username = user_dict['username'] From d4970977a8fd015b679236fca5bde501139e830e Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:37:50 -0400 Subject: [PATCH 32/76] Semantic rewording --- conditional/templates/major_project_submission.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index cf3f39dd..b999e49a 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -12,11 +12,12 @@

Major Project Form

-

Hi! Welcome to the Major Project submission form. We're excited to read about your major - project. For us(E-Board) to best evaluate your project, please give us as much detail as possible. - Don't feel pressured to write full paragraphs though, good bullet points are plenty!
Generally, - major projects are most likely to pass when they meet three of the 4 Major Project Pillars - - spending considerable time on your project, benefiting House, learning new things, and applying hard +

Welcome to the Major Project submission form! We're excited to read about what you've + been working on. For us (E-Board) to best evaluate your project, please give us as much detail as + possible. Don't feel pressured to write full paragraphs though, good bullet points are plenty! +
Generally, major projects are most likely to pass when they meet at least 3 of the 4 + Major Project Pillars - + considerable time on your project, benefiting House, learning new things, and applying hard skills. And of course, after you submit, please try to talk to E-Board members (in-person or over Slack) so we are familiar with your project and can ask you questions!

From 9d877957703e449053e5e811970379aeffc7c98e Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sat, 14 Sep 2024 17:31:21 -0400 Subject: [PATCH 33/76] Start of tag skills, more backend updates --- .../blueprints/major_project_submission.py | 29 +++++++++++++------ conditional/models/models.py | 8 +++-- .../templates/major_project_submission.html | 17 ++++++----- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 47fbf434..59d87bb7 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -92,27 +92,38 @@ def submit_major_project(user_dict=None): post_data = request.get_json() name = post_data['projectName'] description = post_data['projectDescription'] + user_id = user_dict['username'] if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum return jsonify({"success": False}), 400 - project = MajorProject(user_dict['username'], name, description) + project = MajorProject(user_id, name, description) + + db.session.add(project) + db.session.commit() + + # This allows us to get a project with a database ID + project = MajorProject.query.filter( + MajorProject.name == name and MajorProject.uid == user_id + ).first() + + if project is None: + return jsonify({"success": False}), 500 + + # Don't send slack ping until after we are sure the DB worked fine + send_slack_ping({"text":f" *{get_member_name(user_id)}* ({user_id})" + f" submitted their major project, *{name}*!"}) # Acquire S3 Bucket instance s3 = boto3.resource("s3", endpoint_url="https://s3.csh.rit.edu") bucket = s3.create_bucket(Bucket="major-project-media") # Collect all the locally cached files and put them in the bucket - for file in os.listdir(f"/tmp/{user_dict['username']}"): - filepath = f"/tmp/{user_dict['username']}/{file}" + for file in os.listdir(f"/tmp/{user_id}"): + filepath = f"/tmp/{user_id}/{file}" print(filepath) bucket.upload_file(filepath, f"{project.id}-{file}") os.remove(filepath) - os.rmdir(f"/tmp/{user_dict['username']}") + os.rmdir(f"/tmp/{user_id}") - username = user_dict['username'] - send_slack_ping({"text":f" *{get_member_name(username)}* ({username})" - f" submitted their major project, *{name}*!"}) - db.session.add(project) - db.session.commit() return jsonify({"success": True}), 200 diff --git a/conditional/models/models.py b/conditional/models/models.py index 59ed4337..fec3036a 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -135,16 +135,20 @@ class MajorProject(db.Model): date = Column(Date, nullable=False) uid = Column(String(32), nullable=False) name = Column(String(64), nullable=False) - description = Column(Text) + tldr = Column(String(128), nullable=False) + time = Column(Text, nullable=False) + description = Column(Text, nullable=False) active = Column(Boolean, nullable=False) status = Column(Enum('Pending', 'Passed', 'Failed', name="major_project_enum"), nullable=False) - def __init__(self, uid, name, desc): + def __init__(self, uid, name, tldr, time, desc): self.uid = uid self.date = datetime.now() self.name = name + self.tldr = tldr + self.time = time self.description = desc self.status = 'Pending' self.active = True diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index b999e49a..4b9f9c6a 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -9,17 +9,18 @@

Major Project Form

-

Welcome to the Major Project submission form! We're excited to read about what you've been working on. For us (E-Board) to best evaluate your project, please give us as much detail as possible. Don't feel pressured to write full paragraphs though, good bullet points are plenty! -
Generally, major projects are most likely to pass when they meet at least 3 of the 4 +
Generally, a major project is something that you make with the goal of challenging yourself, + learning new things, and doing something you would be proud of. Major projects are most likely to + pass when they meet at least 2 of the 3 Major Project Pillars - - considerable time on your project, benefiting House, learning new things, and applying hard - skills. And of course, after you submit, please try to talk to E-Board members (in-person or over - Slack) so we are familiar with your project and can ask you questions!

+ considerable time on your project, benefiting House, and meaningfully applying skills. And of course, + after you submit, please try to talk to E-Board members (in-person or over Slack) so we are familiar + with your project and can ask you questions!

@@ -34,9 +35,11 @@

Major Project Form

- + placeholder="List what skills you meaningfully used while working on this project (at least 2!)">Test
From 02d8a5d201ac2b16b1dc942bb05617b3a232ad2a Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 00:23:05 -0400 Subject: [PATCH 34/76] db migration 1 --- .../versions/05126dcdf40e_add_mp_fields.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 migrations/versions/05126dcdf40e_add_mp_fields.py diff --git a/migrations/versions/05126dcdf40e_add_mp_fields.py b/migrations/versions/05126dcdf40e_add_mp_fields.py new file mode 100644 index 00000000..b35155f1 --- /dev/null +++ b/migrations/versions/05126dcdf40e_add_mp_fields.py @@ -0,0 +1,89 @@ +"""add major project fields + +Revision ID: 05126dcdf40e +Revises: 757e18146d16 +Create Date: 2024-09-15 00:20:50.617251 + +""" + +# revision identifiers, used by Alembic. +revision = '05126dcdf40e' +down_revision = '757e18146d16' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('freshman_batch_pulls') + op.drop_table('batch_conditions') + op.drop_table('freshman_batch_users') + op.drop_table('batch') + op.drop_table('member_batch_users') + op.drop_table('member_batch_pulls') + op.alter_column('freshman_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=True) + op.add_column('major_projects', sa.Column('time', sa.Text(), nullable=False)) + op.add_column('major_projects', sa.Column('tldr', sa.String(length=128), nullable=False)) + op.alter_column('major_projects', 'description', + existing_type=sa.TEXT(), + nullable=False) + op.alter_column('member_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('member_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=False) + op.alter_column('major_projects', 'description', + existing_type=sa.TEXT(), + nullable=True) + op.drop_column('major_projects', 'tldr') + op.drop_column('major_projects', 'time') + op.alter_column('freshman_hm_attendance', 'attendance_status', + existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), + nullable=False) + op.create_table('member_batch_pulls', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=False), + sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('reason', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False) + ) + op.create_table('member_batch_users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) + ) + op.create_table('batch', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False) + ) + op.create_table('freshman_batch_users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) + ) + op.create_table('batch_conditions', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('value', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('condition', postgresql.ENUM('packet', 'seminar', 'committee', 'house', name='batch_ctype_enum'), autoincrement=False, nullable=False), + sa.Column('comparison', postgresql.ENUM('less', 'equal', 'greater', name='batch_comparison'), autoincrement=False, nullable=False), + sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) + ) + op.create_table('freshman_batch_pulls', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('reason', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False) + ) + # ### end Alembic commands ### From 94ee197e038d660167e5f419990fccd62063a8b9 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 01:27:19 -0400 Subject: [PATCH 35/76] added v1 skill tagger --- .../blueprints/major_project_submission.py | 5 ++++- .../templates/major_project_submission.html | 10 ++++++---- .../javascript/modules/majorProjectForm.js | 20 +++++++++++++++++++ .../stylesheets/pages/_major-project.scss | 11 ++++++++++ .../versions/05126dcdf40e_add_mp_fields.py | 4 ++-- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 59d87bb7..a462e4c5 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -91,12 +91,15 @@ def submit_major_project(user_dict=None): post_data = request.get_json() name = post_data['projectName'] + tldr = post_data['projectTldr'] + time_spent = post_data['projectTimeSpent'] description = post_data['projectDescription'] + user_id = user_dict['username'] if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum return jsonify({"success": False}), 400 - project = MajorProject(user_id, name, description) + project = MajorProject(user_id, name, tldr, time_spent, description) db.session.add(project) db.session.commit() diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 4b9f9c6a..f569b7f0 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -35,11 +35,13 @@

Major Project Form

-
- List what skills you meaningfully used while working on this project (at least 2!)
- + placeholder="List what skills you meaningfully used while working on this project (at least 2!)">Test-->
diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index fc9969c1..f7af37c5 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -1,6 +1,9 @@ import FetchUtil from "../utils/fetchUtil"; export default class MajorProjectForm { + + tags_written = false; + constructor(form) { this.form = form; this.endpoint = '/major_project/submit'; @@ -10,6 +13,21 @@ export default class MajorProjectForm { render() { this.form.querySelector('input[type=submit]') .addEventListener('click', e => this._submitForm(e)); + this.form.querySelector('input[id=skill-input]') + .addEventListener('focusout') + } + + onWriteSkill(e) { + let input = document.getElementById("skill-input") + if (!this.tags_written) { + this.tags_written = true + document.getElementsByClassName("placeholder").item(0).remove() + } + let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\#]/g, ''); // allowed characters list + if (txt) input.before('' + txt + ''); + input.value = ""; + input.focus(); + } _submitForm(e) { @@ -17,6 +35,8 @@ export default class MajorProjectForm { let payload = { projectName: this.form.querySelector('input[name=name]').value, + projectTldr: this.form.querySelector('input[name=tldr]').value, + projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, projectDescription: this.form.querySelector('textarea[name=description]').value }; diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index aefea190..4bb264d3 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -10,4 +10,15 @@ .form-textarea { resize: vertical; +} + +.form-skilltags { + box-shadow: inset 0 -1px 0 #ddd; + border: none; + padding: 0; +} + +.placeholder { + color: #bbb; + font-size: 16px; } \ No newline at end of file diff --git a/migrations/versions/05126dcdf40e_add_mp_fields.py b/migrations/versions/05126dcdf40e_add_mp_fields.py index b35155f1..9eafb64f 100644 --- a/migrations/versions/05126dcdf40e_add_mp_fields.py +++ b/migrations/versions/05126dcdf40e_add_mp_fields.py @@ -25,8 +25,8 @@ def upgrade(): op.alter_column('freshman_hm_attendance', 'attendance_status', existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), nullable=True) - op.add_column('major_projects', sa.Column('time', sa.Text(), nullable=False)) - op.add_column('major_projects', sa.Column('tldr', sa.String(length=128), nullable=False)) + op.add_column('major_projects', sa.Column('time', sa.Text(), nullable=False, server_default='N/A')) + op.add_column('major_projects', sa.Column('tldr', sa.String(length=128), nullable=False, server_default='N/A')) op.alter_column('major_projects', 'description', existing_type=sa.TEXT(), nullable=False) From f2825ee8ddd54353cc934393fb5d110ba684b272 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 01:56:34 -0400 Subject: [PATCH 36/76] fixed? issue --- frontend/javascript/modules/majorProjectForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index f7af37c5..3fc80d06 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -14,7 +14,7 @@ export default class MajorProjectForm { this.form.querySelector('input[type=submit]') .addEventListener('click', e => this._submitForm(e)); this.form.querySelector('input[id=skill-input]') - .addEventListener('focusout') + .addEventListener('focusout', e => this.onWriteSkill(e)); } onWriteSkill(e) { From 60074cf28018fc19daa6af4ef5008d6c1840b7bd Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:39:02 -0400 Subject: [PATCH 37/76] javascript before is odd --- frontend/javascript/modules/majorProjectForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 3fc80d06..28c545a4 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -23,8 +23,8 @@ export default class MajorProjectForm { this.tags_written = true document.getElementsByClassName("placeholder").item(0).remove() } - let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\#]/g, ''); // allowed characters list - if (txt) input.before('' + txt + ''); + let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list + if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); input.value = ""; input.focus(); From 62d6d4809c350d1299ae65a634f2762973d971f4 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:59:17 -0400 Subject: [PATCH 38/76] styling --- frontend/stylesheets/pages/_major-project.scss | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 4bb264d3..4b6e098f 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -13,9 +13,22 @@ } .form-skilltags { + display: block; + float: left; + background: #d979e3; + padding: 5px 10px; box-shadow: inset 0 -1px 0 #ddd; border: none; - padding: 0; +} + +.form-skilltags:after { + position: absolute; + content: "×"; + border: 1px solid; + border-radius: 10px; + padding: 0 4px; + margin: 3px 0 10px 7px; + font-size: 10px; } .placeholder { From 7c9f8cf829ecf5907b00e99b8327b4da09687307 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 16:27:08 -0400 Subject: [PATCH 39/76] css fixes and js fixes to tags --- .../javascript/modules/majorProjectForm.js | 96 ++++++++++--------- .../stylesheets/pages/_major-project.scss | 22 +++-- 2 files changed, 66 insertions(+), 52 deletions(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 28c545a4..93752a0a 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -2,49 +2,59 @@ import FetchUtil from "../utils/fetchUtil"; export default class MajorProjectForm { - tags_written = false; - - constructor(form) { - this.form = form; - this.endpoint = '/major_project/submit'; - this.render(); - } - - render() { - this.form.querySelector('input[type=submit]') - .addEventListener('click', e => this._submitForm(e)); - this.form.querySelector('input[id=skill-input]') - .addEventListener('focusout', e => this.onWriteSkill(e)); - } - - onWriteSkill(e) { - let input = document.getElementById("skill-input") - if (!this.tags_written) { - this.tags_written = true - document.getElementsByClassName("placeholder").item(0).remove() + + constructor(form) { + this.form = form; + this.endpoint = '/major_project/submit'; + this.tags_written = false; + this.tag_keys = ["Enter", "Comma", "Tab"]; + this.render(); + } + + render() { + this.form.querySelector('input[type=submit]') + .addEventListener('click', e => this._submitForm(e)); + this.form.querySelector('input[id=skill-input]') + .addEventListener('focusout', e => this.onWriteSkill(e)); + this.form.querySelector('input[id=skill-input]') + .addEventListener('keyup', e => this.onKeyPress(e)); + } + + onKeyPress(e) { + if (this.tag_keys.includes(e.code)) { + e.preventDefault(); + this.onWriteSkill(e); + } } + + onWriteSkill(e) { + let input = document.getElementById("skill-input") + if (!this.tags_written) { + this.tags_written = true + document.getElementsByClassName("placeholder").item(0).remove() + } let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list - if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); - input.value = ""; - input.focus(); - - } - - _submitForm(e) { - e.preventDefault(); - - let payload = { - projectName: this.form.querySelector('input[name=name]').value, - projectTldr: this.form.querySelector('input[name=tldr]').value, - projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, - projectDescription: - this.form.querySelector('textarea[name=description]').value - }; - - FetchUtil.postWithWarning(this.endpoint, payload, { - warningText: "You will not be able to edit your " + - "project once it has been submitted.", - successText: "Your project has been submitted." - }); - } + if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); + input.value = ""; + input.focus(); + + } + + _submitForm(e) { + e.preventDefault(); + + let payload = { + projectName: this.form.querySelector('input[name=name]').value, + projectTldr: this.form.querySelector('input[name=tldr]').value, + projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, + projectDescription: + this.form.querySelector('textarea[name=description]').value + }; + + FetchUtil.postWithWarning(this.endpoint, payload, { + warningText: "You will not be able to edit your " + + "project once it has been submitted.", + successText: "Your project has been submitted." + }); + } } diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 4b6e098f..7bb8992b 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -13,15 +13,24 @@ } .form-skilltags { - display: block; + box-shadow: inset 0 -1px 0 #ddd; + border: none; + padding: 0; +} + +.placeholder { + color: #bbb; + font-size: 16px; +} + +.skill-tag { + display: block; float: left; background: #d979e3; padding: 5px 10px; - box-shadow: inset 0 -1px 0 #ddd; - border: none; } -.form-skilltags:after { +.skill-tag:after { position: absolute; content: "×"; border: 1px solid; @@ -29,9 +38,4 @@ padding: 0 4px; margin: 3px 0 10px 7px; font-size: 10px; -} - -.placeholder { - color: #bbb; - font-size: 16px; } \ No newline at end of file From 180d658cd9047bbc2de6f9e080b84778dc4777fb Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:03:57 -0400 Subject: [PATCH 40/76] no border radius --- frontend/stylesheets/pages/_major-project.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index 7bb8992b..ab0a42b6 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -24,10 +24,13 @@ } .skill-tag { - display: block; + display: block; float: left; background: #d979e3; - padding: 5px 10px; + padding: 4px 30px 4px 8px; + margin: 2px 5px; + color: #444; + border-radius: 5px; } .skill-tag:after { From b9402cb4310e48c97e5e3a42f62d6d91f5769579 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:35:13 -0400 Subject: [PATCH 41/76] fix more tags bugs --- frontend/javascript/modules/majorProjectForm.js | 1 + frontend/stylesheets/pages/_major-project.scss | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 93752a0a..e2dcfc24 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -25,6 +25,7 @@ export default class MajorProjectForm { e.preventDefault(); this.onWriteSkill(e); } + return false; } onWriteSkill(e) { diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index ab0a42b6..b2e02b3b 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -15,7 +15,9 @@ .form-skilltags { box-shadow: inset 0 -1px 0 #ddd; border: none; - padding: 0; + border-radius: 0; + padding: 0 0 10px; + height: fit-content; } .placeholder { From 8ad332c92cad40d9af3ddd199efd70f36b54936c Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:44:55 -0400 Subject: [PATCH 42/76] keypress instead of keydown --- frontend/javascript/modules/majorProjectForm.js | 2 +- frontend/stylesheets/pages/_major-project.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index e2dcfc24..b1cce6bc 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -17,7 +17,7 @@ export default class MajorProjectForm { this.form.querySelector('input[id=skill-input]') .addEventListener('focusout', e => this.onWriteSkill(e)); this.form.querySelector('input[id=skill-input]') - .addEventListener('keyup', e => this.onKeyPress(e)); + .addEventListener('keypress', e => this.onKeyPress(e)); } onKeyPress(e) { diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index b2e02b3b..c880076a 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -30,7 +30,7 @@ float: left; background: #d979e3; padding: 4px 30px 4px 8px; - margin: 2px 5px; + margin: 2px 3px; color: #444; border-radius: 5px; } From 00c44458aab886f97534b7a7fceae1c0b1bf4f67 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:02:05 -0400 Subject: [PATCH 43/76] added a how-to-use --- conditional/templates/major_project_submission.html | 6 +----- frontend/javascript/modules/majorProjectForm.js | 2 +- frontend/stylesheets/pages/_major-project.scss | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index f569b7f0..256688b7 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -34,14 +34,10 @@

Major Project Form

placeholder="A quick, one-sentence pitch for your project and what it does.">
- +
- List what skills you meaningfully used while working on this project (at least 2!)
-
diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index b1cce6bc..b0b105f1 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -7,7 +7,7 @@ export default class MajorProjectForm { this.form = form; this.endpoint = '/major_project/submit'; this.tags_written = false; - this.tag_keys = ["Enter", "Comma", "Tab"]; + this.tag_keys = ["Enter", "Comma"]; this.render(); } diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index c880076a..c1d06296 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -8,6 +8,10 @@ font-size: 2rem; } +.form-label span { + font-size: 1rem; +} + .form-textarea { resize: vertical; } From 74e0511e74670a11170d510c325f3e11f2980cf2 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:24:38 -0400 Subject: [PATCH 44/76] make delete tag work --- frontend/javascript/modules/majorProjectForm.js | 8 +++++++- frontend/stylesheets/pages/_major-project.scss | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index b0b105f1..76c37a76 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -35,12 +35,18 @@ export default class MajorProjectForm { document.getElementsByClassName("placeholder").item(0).remove() } let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list - if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); + if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); + let skills = document.getElementsByClassName("skill-tag") + skills[skills.length - 1].addEventListener('click', e => this.onRemoveTag(e)); input.value = ""; input.focus(); } + onRemoveTag(e) { + e.target.remove(); + } + _submitForm(e) { e.preventDefault(); diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index c1d06296..ca0667ce 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -37,6 +37,7 @@ margin: 2px 3px; color: #444; border-radius: 5px; + transition: .5s all; } .skill-tag:after { @@ -47,4 +48,8 @@ padding: 0 4px; margin: 3px 0 10px 7px; font-size: 10px; +} + +.skill-tag:hover { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } \ No newline at end of file From 860b50f9ca4afd8c31515267934e2491a4718bf7 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:43:29 -0400 Subject: [PATCH 45/76] test skills --- conditional/blueprints/major_project_submission.py | 3 +++ frontend/javascript/modules/majorProjectForm.js | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index a462e4c5..831b8666 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -93,10 +93,13 @@ def submit_major_project(user_dict=None): name = post_data['projectName'] tldr = post_data['projectTldr'] time_spent = post_data['projectTimeSpent'] + skills = post_data['projectSkills'] description = post_data['projectDescription'] user_id = user_dict['username'] + print(skills) + if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum return jsonify({"success": False}), 400 project = MajorProject(user_id, name, tldr, time_spent, description) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 76c37a76..949d9241 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -50,12 +50,18 @@ export default class MajorProjectForm { _submitForm(e) { e.preventDefault(); + let skills = []; + + for (const skill in document.getElementsByClassName('span[class=skill-tag]')) { + skills.push(skill.innerText) + } + let payload = { projectName: this.form.querySelector('input[name=name]').value, projectTldr: this.form.querySelector('input[name=tldr]').value, projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, - projectDescription: - this.form.querySelector('textarea[name=description]').value + projectSkills: skills, + projectDescription: this.form.querySelector('textarea[name=description]').value }; FetchUtil.postWithWarning(this.endpoint, payload, { From 4a6c291c4a4af895defa57d4359522721ff96048 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:58:41 -0400 Subject: [PATCH 46/76] debug --- conditional/blueprints/major_project_submission.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 831b8666..dfe8de0a 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -90,6 +90,7 @@ def submit_major_project(user_dict=None): log.info('Submit Major Project') post_data = request.get_json() + print(post_data) name = post_data['projectName'] tldr = post_data['projectTldr'] time_spent = post_data['projectTimeSpent'] From 7713bb5dc1ce53821d8257ba2d551111a48ab85c Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:04:10 -0400 Subject: [PATCH 47/76] debug 2 --- frontend/javascript/modules/majorProjectForm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 949d9241..dfe6dec2 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -37,7 +37,7 @@ export default class MajorProjectForm { let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); let skills = document.getElementsByClassName("skill-tag") - skills[skills.length - 1].addEventListener('click', e => this.onRemoveTag(e)); + skills.item(skills.length - 1).addEventListener('click', e => this.onRemoveTag(e)); input.value = ""; input.focus(); @@ -63,6 +63,8 @@ export default class MajorProjectForm { projectSkills: skills, projectDescription: this.form.querySelector('textarea[name=description]').value }; + + console.log(payload) FetchUtil.postWithWarning(this.endpoint, payload, { warningText: "You will not be able to edit your " + From d41f64d2f84bc851ff0e55079be543d3982f0bf8 Mon Sep 17 00:00:00 2001 From: Mobmaker <45888585+Mobmaker55@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:25:48 -0400 Subject: [PATCH 48/76] javascript HTMLElement collection confusing --- frontend/javascript/modules/majorProjectForm.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index dfe6dec2..89a210dd 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -52,9 +52,7 @@ export default class MajorProjectForm { let skills = []; - for (const skill in document.getElementsByClassName('span[class=skill-tag]')) { - skills.push(skill.innerText) - } + Array.from(document.getElementsByClassName('skill-tag')).forEach(tag => skills.push(tag.firstChild.data)) let payload = { projectName: this.form.querySelector('input[name=name]').value, @@ -63,7 +61,7 @@ export default class MajorProjectForm { projectSkills: skills, projectDescription: this.form.querySelector('textarea[name=description]').value }; - + console.log(payload) FetchUtil.postWithWarning(this.endpoint, payload, { From 8fea3ca74b4ddc664264ac28f135becfd5966226 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Sat, 21 Sep 2024 00:33:16 +0000 Subject: [PATCH 49/76] Update requirements.txt --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3bc182c3..14e014f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,8 +42,6 @@ cryptography==37.0.2 csh-ldap==2.3.1 # via -r requirements.in ddsketch==2.0.3 - # via ddtrace -ddtrace==1.1.4 # via -r requirements.in dnspython==2.2.1 # via srvlookup From 1529cf0cfc6a544cbe690a6831cf7962c583b839 Mon Sep 17 00:00:00 2001 From: Cecilia Lau Date: Mon, 18 Nov 2024 15:55:49 -0500 Subject: [PATCH 50/76] Revert "Update requirements.txt" This reverts commit 8fea3ca74b4ddc664264ac28f135becfd5966226. --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 14e014f9..3bc182c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,8 @@ cryptography==37.0.2 csh-ldap==2.3.1 # via -r requirements.in ddsketch==2.0.3 + # via ddtrace +ddtrace==1.1.4 # via -r requirements.in dnspython==2.2.1 # via srvlookup From fbdd139e56d6d87b31733a50585058140071463a Mon Sep 17 00:00:00 2001 From: Ella S <112343747+pikachu0542@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:40:33 -0400 Subject: [PATCH 51/76] Update dependency versions (#393) * I DID IT * Removed unnecessary commented lines, changed dockerfile to call ddtrace again * Bump python in CI * Fixed formatting for linting * More fstr fixes * Fixed more lint errors. There are some lint errors hat i just cant fix, like line length in some log statements, and too many args * Fixed more linter issues Part of doing this was that I removed 'bad-continuation' from the disable list in the .pylintrc bc the linting action said that option has been removed from pylint. I left it commented out for now to make sure the lint passes, but I can delete it entirely before this pr is merged assuming this fix passes the check * Removed commented line --------- Co-authored-by: Max Meinhold --- .github/workflows/python-app.yml | 2 +- .pylintrc | 1 - Dockerfile | 4 +- conditional/__init__.py | 13 +- conditional/blueprints/attendance.py | 76 ++++----- conditional/blueprints/cache_management.py | 4 +- conditional/blueprints/co_op.py | 12 +- conditional/blueprints/conditional.py | 14 +- conditional/blueprints/dashboard.py | 6 +- conditional/blueprints/housing.py | 22 +-- conditional/blueprints/intro_evals.py | 2 +- conditional/blueprints/intro_evals_form.py | 4 +- conditional/blueprints/logs.py | 2 +- .../blueprints/major_project_submission.py | 18 +- conditional/blueprints/member_management.py | 80 ++++----- conditional/blueprints/slideshow.py | 16 +- conditional/blueprints/spring_evals.py | 2 +- conditional/models/models.py | 11 +- conditional/util/context_processors.py | 10 +- requirements.in | 34 ++-- requirements.txt | 159 +++++++++++------- 21 files changed, 259 insertions(+), 233 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f8ae97f9..980b1ab3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - python-version: [3.8, 3.9] + python-version: [3.12] steps: - name: Install ldap dependencies diff --git a/.pylintrc b/.pylintrc index 407369b7..22e31bcb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,6 @@ disable = duplicate-code, no-member, parse-error, - bad-continuation, too-few-public-methods, global-statement, cyclic-import, diff --git a/Dockerfile b/Dockerfile index 3148d474..5fa9b20b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/python:3.8-buster +FROM docker.io/python:3.12-bookworm MAINTAINER Computer Science House RUN mkdir /opt/conditional @@ -8,7 +8,7 @@ ADD requirements.txt /opt/conditional WORKDIR /opt/conditional RUN apt-get -yq update && \ - apt-get -yq install libsasl2-dev libldap2-dev libssl-dev gcc g++ make && \ + apt-get -yq install libsasl2-dev libldap2-dev libldap-common libssl-dev gcc g++ make && \ pip install -r requirements.txt && \ apt-get -yq clean all diff --git a/conditional/__init__.py b/conditional/__init__.py index 041218ae..1c2f5ee9 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -7,6 +7,7 @@ from flask_migrate import Migrate from flask_gzip import Gzip from flask_pyoidc.flask_pyoidc import OIDCAuthentication +from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata from flask_sqlalchemy import SQLAlchemy import sentry_sdk @@ -39,8 +40,10 @@ app.config['LDAP_BIND_PW'], ro=app.config['LDAP_RO']) -auth = OIDCAuthentication(app, issuer=app.config["OIDC_ISSUER"], - client_registration_info=app.config["OIDC_CLIENT_CONFIG"]) +client_metadata = ClientMetadata(app.config["OIDC_CLIENT_CONFIG"]) +provider_config = ProviderConfiguration(issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata) + +auth = OIDCAuthentication({'default': provider_config}, app) app.secret_key = app.config["SECRET_KEY"] @@ -137,7 +140,7 @@ def static_proxy(path): @app.route('/') -@auth.oidc_auth +@auth.oidc_auth("default") def default_route(): return redirect('/dashboard') @@ -158,10 +161,10 @@ def health(): @app.errorhandler(404) @app.errorhandler(500) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def route_errors(error, user_dict=None): - data = dict() + data = {} # Handle the case where the header isn't present if user_dict['username'] is not None: diff --git a/conditional/blueprints/attendance.py b/conditional/blueprints/attendance.py index 7adbf995..22475703 100644 --- a/conditional/blueprints/attendance.py +++ b/conditional/blueprints/attendance.py @@ -29,7 +29,7 @@ @attendance_bp.route('/attendance/ts_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_all_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -57,7 +57,7 @@ def get_all_members(user_dict=None): @attendance_bp.route('/attendance/hm_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_non_alumni_non_coop(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -102,7 +102,7 @@ def get_non_alumni_non_coop(internal=False, user_dict=None): @attendance_bp.route('/attendance/cm_members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_non_alumni(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -130,7 +130,7 @@ def get_non_alumni(user_dict=None): @attendance_bp.route('/attendance_cm') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_cm(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -142,7 +142,7 @@ def display_attendance_cm(user_dict=None): @attendance_bp.route('/attendance_ts') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_ts(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -154,7 +154,7 @@ def display_attendance_ts(user_dict=None): @attendance_bp.route('/attendance_hm') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_attendance_hm(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -170,7 +170,7 @@ def display_attendance_hm(user_dict=None): @attendance_bp.route('/attendance/submit/cm', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_committee_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -183,7 +183,7 @@ def submit_committee_attendance(user_dict=None): f_attendees = post_data['freshmen'] timestamp = post_data['timestamp'] - log.info('Submit {} Meeting Attendance'.format(committee)) + log.info(f'Submit {committee} Meeting Attendance') timestamp = datetime.strptime(timestamp, "%Y-%m-%d") meeting = CommitteeMeeting(committee, timestamp, approved) @@ -193,11 +193,11 @@ def submit_committee_attendance(user_dict=None): db.session.refresh(meeting) for m in m_attendees: - log.info('Gave Attendance to {} for {}'.format(m, committee)) + log.info(f'Gave Attendance to {m} for {committee}') db.session.add(MemberCommitteeAttendance(m, meeting.id)) for f in f_attendees: - log.info('Gave Attendance to freshman-{} for {}'.format(f, committee)) + log.info(f'Gave Attendance to freshman-{f} for {committee}') db.session.add(FreshmanCommitteeAttendance(f, meeting.id)) db.session.commit() @@ -205,7 +205,7 @@ def submit_committee_attendance(user_dict=None): @attendance_bp.route('/attendance/submit/ts', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_seminar_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -228,11 +228,11 @@ def submit_seminar_attendance(user_dict=None): db.session.refresh(seminar) for m in m_attendees: - log.info('Gave Attendance to {} for {}'.format(m, seminar_name)) + log.info(f'Gave Attendance to {m} for {seminar_name}') db.session.add(MemberSeminarAttendance(m, seminar.id)) for f in f_attendees: - log.info('Gave Attendance to freshman-{} for {}'.format(f, seminar_name)) + log.info(f'Gave Attendance to freshman-{f} for {seminar_name}') db.session.add(FreshmanSeminarAttendance(f, seminar.id)) db.session.commit() @@ -240,7 +240,7 @@ def submit_seminar_attendance(user_dict=None): @attendance_bp.route('/attendance/submit/hm', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_house_attendance(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -263,10 +263,7 @@ def submit_house_attendance(user_dict=None): if "members" in post_data: for m in post_data['members']: - log.info('Marked {} {} for House Meeting on {}'.format( - m['uid'], - m['status'], - timestamp.strftime("%Y-%m-%d"))) + log.info(f'Marked {m['uid']} {m['status']} for House Meeting on {timestamp.strftime("%Y-%m-%d")}') db.session.add(MemberHouseMeetingAttendance( m['uid'], meeting.id, @@ -275,10 +272,7 @@ def submit_house_attendance(user_dict=None): if "freshmen" in post_data: for f in post_data['freshmen']: - log.info('Marked freshman-{} {} for House Meeting on {}'.format( - f['id'], - f['status'], - timestamp.strftime("%Y-%m-%d"))) + log.info(f'Marked freshman-{f['id']} {f['status']} for House Meeting on {timestamp.strftime("%Y-%m-%d")}') db.session.add(FreshmanHouseMeetingAttendance( f['id'], meeting.id, @@ -290,7 +284,7 @@ def submit_house_attendance(user_dict=None): @attendance_bp.route('/attendance/alter/hm//', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_house_attendance(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -299,7 +293,7 @@ def alter_house_attendance(uid, hid, user_dict=None): return "must be evals", 403 if not uid.isdigit(): - log.info('Mark {} Present for House Meeting ID: {}'.format(uid, hid)) + log.info(f'Mark {uid} Present for House Meeting ID: {hid}') member_meeting = MemberHouseMeetingAttendance.query.filter( MemberHouseMeetingAttendance.uid == uid, MemberHouseMeetingAttendance.meeting_id == hid @@ -308,7 +302,7 @@ def alter_house_attendance(uid, hid, user_dict=None): db.session.commit() return jsonify({"success": True}), 200 - log.info('Mark freshman-{} Present for House Meeting ID: {}'.format(uid, hid)) + log.info(f'Mark freshman-{uid} Present for House Meeting ID: {hid}') freshman_meeting = FreshmanHouseMeetingAttendance.query.filter( FreshmanHouseMeetingAttendance.fid == uid, FreshmanHouseMeetingAttendance.meeting_id == hid @@ -320,7 +314,7 @@ def alter_house_attendance(uid, hid, user_dict=None): @attendance_bp.route('/attendance/alter/hm//', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_house_excuse(uid, hid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -333,7 +327,7 @@ def alter_house_excuse(uid, hid, user_dict=None): hm_excuse = post_data['excuse'] if not uid.isdigit(): - log.info('Mark {} as {} for HM ID: {}'.format(uid, hm_status, hid)) + log.info(f'Mark {uid} as {hm_status} for HM ID: {hid}') MemberHouseMeetingAttendance.query.filter( MemberHouseMeetingAttendance.uid == uid, MemberHouseMeetingAttendance.meeting_id == hid @@ -342,7 +336,7 @@ def alter_house_excuse(uid, hid, user_dict=None): 'attendance_status': hm_status }) else: - log.info('Mark {} as {} for HM ID: {}'.format(uid, hm_status, hid)) + log.info(f'Mark {uid} as {hm_status} for HM ID: {hid}') FreshmanHouseMeetingAttendance.query.filter( FreshmanHouseMeetingAttendance.fid == uid, FreshmanHouseMeetingAttendance.meeting_id == hid @@ -357,7 +351,7 @@ def alter_house_excuse(uid, hid, user_dict=None): @attendance_bp.route('/attendance/history', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def attendance_history(user_dict=None): @@ -431,9 +425,9 @@ def get_seminar_attendees(meeting_id): TechnicalSeminar.approved == False).all()] # pylint: disable=singleton-comparison all_meetings = sorted((all_cm + all_ts), key=lambda k: k['dt_obj'], reverse=True)[offset:limit] if len(all_cm) % 10 != 0: - total_pages = (int(len(all_cm) / 10) + 1) + total_pages = int(len(all_cm) / 10) + 1 else: - total_pages = (int(len(all_cm) / 10)) + total_pages = int(len(all_cm) / 10) return render_template('attendance_history.html', username=user_dict['username'], history=all_meetings, @@ -444,7 +438,7 @@ def get_seminar_attendees(meeting_id): @attendance_bp.route('/attendance/alter/cm/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_committee_attendance(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -476,7 +470,7 @@ def alter_committee_attendance(cid, user_dict=None): @attendance_bp.route('/attendance/alter/ts/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def alter_seminar_attendance(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -508,7 +502,7 @@ def alter_seminar_attendance(sid, user_dict=None): @attendance_bp.route('/attendance/ts/', methods=['GET', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_cm_attendees(sid, user_dict=None): if request.method == 'GET': @@ -526,7 +520,7 @@ def get_cm_attendees(sid, user_dict=None): return jsonify({"attendees": attendees}), 200 log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Technical Seminar {}'.format(sid)) + log.info(f'Delete Technical Seminar {sid}') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -545,7 +539,7 @@ def get_cm_attendees(sid, user_dict=None): @attendance_bp.route('/attendance/cm/', methods=['GET', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_ts_attendees(cid, user_dict=None): if request.method == 'GET': @@ -563,7 +557,7 @@ def get_ts_attendees(cid, user_dict=None): return jsonify({"attendees": attendees}), 200 log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Committee Meeting {}'.format(cid)) + log.info(f'Delete Committee Meeting {cid}') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -582,11 +576,11 @@ def get_ts_attendees(cid, user_dict=None): @attendance_bp.route('/attendance/cm//approve', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def approve_cm(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Approve Committee Meeting {} Attendance'.format(cid)) + log.info(f'Approve Committee Meeting {cid} Attendance') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 @@ -600,11 +594,11 @@ def approve_cm(cid, user_dict=None): @attendance_bp.route('/attendance/ts//approve', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def approve_ts(sid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Approve Technical Seminar {} Attendance'.format(sid)) + log.info(f'Approve Technical Seminar {sid} Attendance') if not ldap_is_eboard(user_dict['account']): return jsonify({"success": False, "error": "Not EBoard"}), 403 diff --git a/conditional/blueprints/cache_management.py b/conditional/blueprints/cache_management.py index fc4a1866..14290a51 100644 --- a/conditional/blueprints/cache_management.py +++ b/conditional/blueprints/cache_management.py @@ -21,7 +21,7 @@ @cache_bp.route('/restart') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def restart_app(user_dict=None): if not ldap_is_rtp(user_dict['account']): @@ -34,7 +34,7 @@ def restart_app(user_dict=None): @cache_bp.route('/clearcache') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_cache(user_dict=None): if not ldap_is_eval_director(user_dict['account']) and not ldap_is_rtp(user_dict['account']): diff --git a/conditional/blueprints/co_op.py b/conditional/blueprints/co_op.py index 27b86dce..94bf107a 100644 --- a/conditional/blueprints/co_op.py +++ b/conditional/blueprints/co_op.py @@ -17,7 +17,7 @@ @co_op_bp.route('/co_op/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_co_op_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -33,7 +33,7 @@ def display_co_op_form(user_dict=None): @co_op_bp.route('/co_op/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_co_op_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -46,7 +46,7 @@ def submit_co_op_form(user_dict=None): if not ldap_is_current_student(user_dict['account']): return "Must be current student", 403 - log.info('Submit {} Co-Op'.format(semester)) + log.info(f'Submit {semester} Co-Op') if CurrentCoops.query.filter(CurrentCoops.uid == user_dict['username'], CurrentCoops.date_created > start_of_year()).first(): @@ -65,7 +65,7 @@ def submit_co_op_form(user_dict=None): @co_op_bp.route('/co_op/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def delete_co_op(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -73,7 +73,7 @@ def delete_co_op(uid, user_dict=None): if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 - log.info('Delete {}\'s Co-Op'.format(uid)) + log.info(f'Delete {uid}\'s Co-Op') # Remove from corresponding co-op ldap group if ldap_is_member_of_group(user_dict['account'], 'fall_coop'): @@ -91,7 +91,7 @@ def delete_co_op(uid, user_dict=None): @co_op_bp.route('/co_op/manage') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_co_op_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/conditional.py b/conditional/blueprints/conditional.py index a5bd005c..1082ca4f 100644 --- a/conditional/blueprints/conditional.py +++ b/conditional/blueprints/conditional.py @@ -15,7 +15,7 @@ @conditionals_bp.route('/conditionals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_conditionals(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -39,7 +39,7 @@ def display_conditionals(user_dict=None): @conditionals_bp.route('/conditionals/create', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def create_conditional(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -52,7 +52,7 @@ def create_conditional(user_dict=None): uid = post_data['uid'] description = post_data['description'] due_date = datetime.strptime(post_data['dueDate'], "%Y-%m-%d") - log.info('Create a new conditional for {}'.format(uid)) + log.info(f'Create a new conditional for {uid}') if post_data['evaluation'] == 'spring': current_eval = SpringEval.query.filter(SpringEval.status == "Pending", SpringEval.uid == uid, @@ -76,7 +76,7 @@ def create_conditional(user_dict=None): @conditionals_bp.route('/conditionals/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def conditional_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -88,7 +88,7 @@ def conditional_review(user_dict=None): cid = post_data['id'] status = post_data['status'] - log.info('Updated conditional-{} to {}'.format(cid, status)) + log.info(f'Updated conditional-{cid} to {status}') conditional = Conditional.query.filter(Conditional.id == cid) cond_obj = conditional.first() @@ -113,11 +113,11 @@ def conditional_review(user_dict=None): @conditionals_bp.route('/conditionals/delete/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def conditional_delete(cid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete conditional-{}'.format(cid)) + log.info(f'Delete conditional-{cid}') if ldap_is_eval_director(user_dict['account']): Conditional.query.filter( diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index 6028c454..527e8dd8 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -29,7 +29,7 @@ def is_seminar_attendance_valid(attendance): # pylint: disable=too-many-statements @dashboard_bp.route('/dashboard/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_dashboard(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -38,7 +38,7 @@ def display_dashboard(user_dict=None): # Get the list of voting members. can_vote = get_voting_members() - data = dict() + data = {} data['username'] = user_dict['account'].uid data['active'] = ldap_is_active(user_dict['account']) data['bad_standing'] = ldap_is_bad_standing(user_dict['account']) @@ -71,7 +71,7 @@ def display_dashboard(user_dict=None): # only show housing if member has onfloor status if ldap_is_onfloor(user_dict['account']): - housing = dict() + housing = {} housing['points'] = user_dict['account'].housingPoints housing['room'] = user_dict['account'].roomNumber housing['queue_pos'] = get_queue_position(user_dict['account'].uid) diff --git a/conditional/blueprints/housing.py b/conditional/blueprints/housing.py index b4ea5466..45e29f90 100644 --- a/conditional/blueprints/housing.py +++ b/conditional/blueprints/housing.py @@ -20,7 +20,7 @@ @housing_bp.route('/housing') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_housing(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -62,7 +62,7 @@ def display_housing(user_dict=None): @housing_bp.route('/housing/in_queue', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def change_queue_state(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -75,11 +75,11 @@ def change_queue_state(user_dict=None): if uid: if post_data.get('inQueue', False): - log.info('Add {} to Housing Queue'.format(uid)) + log.info(f'Add {uid} to Housing Queue') queue_obj = InHousingQueue(uid=uid) db.session.add(queue_obj) else: - log.info('Remove {} from Housing Queue'.format(uid)) + log.info(f'Remove {uid} from Housing Queue') InHousingQueue.query.filter_by(uid=uid).delete() db.session.flush() @@ -88,7 +88,7 @@ def change_queue_state(user_dict=None): @housing_bp.route('/housing/update/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def change_room_numbers(rmnumber, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -107,21 +107,21 @@ def change_room_numbers(rmnumber, user_dict=None): if occupant != "": account = ldap_get_member(occupant) account.roomNumber = rmnumber - log.info('{} assigned to room {}'.format(occupant, rmnumber)) + log.info(f'{occupant} assigned to room {rmnumber}') ldap_set_active(account) - log.info('{} marked as active because of room assignment'.format(occupant)) + log.info(f'{occupant} marked as active because of room assignment') # Delete any old occupants that are no longer in room. for old_occupant in [account for account in current_students if ldap_get_roomnumber(account) == str(rmnumber) and account.uid not in update["occupants"]]: - log.info('{} removed from room {}'.format(old_occupant.uid, old_occupant.roomNumber)) + log.info(f'{old_occupant.uid} removed from room {old_occupant.roomNumber}') old_occupant.roomNumber = None return jsonify({"success": True}), 200 @housing_bp.route('/housing/room/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") def get_occupants(rmnumber): # Get the current list of people living on-floor. @@ -134,7 +134,7 @@ def get_occupants(rmnumber): @housing_bp.route('/housing', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_all_rooms(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -146,6 +146,6 @@ def clear_all_rooms(user_dict=None): # Find the current occupants and clear them. for occupant in current_students: - log.info('{} removed from room {}'.format(occupant.uid, occupant.roomNumber)) + log.info(f'{occupant.uid} removed from room {occupant.roomNumber}') occupant.roomNumber = None return jsonify({"success": True}), 200 diff --git a/conditional/blueprints/intro_evals.py b/conditional/blueprints/intro_evals.py index 7cd276b0..3349bcb5 100644 --- a/conditional/blueprints/intro_evals.py +++ b/conditional/blueprints/intro_evals.py @@ -25,7 +25,7 @@ @intro_evals_bp.route('/intro_evals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_intro_evals(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/intro_evals_form.py b/conditional/blueprints/intro_evals_form.py index 94efa60b..d90f161a 100644 --- a/conditional/blueprints/intro_evals_form.py +++ b/conditional/blueprints/intro_evals_form.py @@ -15,7 +15,7 @@ @intro_evals_form_bp.route('/intro_evals_form/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_intro_evals_form(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -36,7 +36,7 @@ def display_intro_evals_form(user_dict=None): @intro_evals_form_bp.route('/intro_evals/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_intro_evals(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/logs.py b/conditional/blueprints/logs.py index 1e78fc18..92b671be 100644 --- a/conditional/blueprints/logs.py +++ b/conditional/blueprints/logs.py @@ -14,7 +14,7 @@ @log_bp.route('/logs') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_logs(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index dfe8de0a..6fb9649f 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -3,7 +3,6 @@ import requests import boto3 -from botocore.exceptions import ClientError from flask import Blueprint from flask import request @@ -31,7 +30,7 @@ @major_project_bp.route('/major_project/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -59,12 +58,13 @@ def display_major_project(user_dict=None): username=user_dict['username']) @major_project_bp.route('/major_project/upload', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def upload_major_project_files(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) log.info('Uploading Major Project File(s)') + if len(list(request.files.keys())) < 1: return "No file", 400 @@ -83,7 +83,7 @@ def upload_major_project_files(user_dict=None): @major_project_bp.route('/major_project/submit', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def submit_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -135,7 +135,7 @@ def submit_major_project(user_dict=None): @major_project_bp.route('/major_project/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def major_project_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -147,7 +147,7 @@ def major_project_review(user_dict=None): pid = post_data['id'] status = post_data['status'] - log.info('{} Major Project ID: {}'.format(status, pid)) + log.info(f'{status} Major Project ID: {pid}') print(post_data) MajorProject.query.filter( @@ -162,11 +162,11 @@ def major_project_review(user_dict=None): @major_project_bp.route('/major_project/delete/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def major_project_delete(pid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete Major Project ID: {}'.format(pid)) + log.info(f'Delete Major Project ID: {pid}') major_project = MajorProject.query.filter( MajorProject.id == pid @@ -184,4 +184,4 @@ def major_project_delete(pid, user_dict=None): return "Must be project owner to delete!", 401 def send_slack_ping(payload): - requests.post(app.config['WEBHOOK_URL'], json.dumps(payload)) + requests.post(app.config['WEBHOOK_URL'], json.dumps(payload), timeout=120) diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 3f5d3d90..1927e440 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -1,7 +1,7 @@ import csv import io from datetime import datetime -from distutils.util import strtobool # pylint: disable=no-name-in-module,import-error +from distutils.util import strtobool # pylint: disable=no-name-in-module,import-error,deprecated-module import structlog from flask import Blueprint, request, jsonify, make_response @@ -52,7 +52,7 @@ @member_management_bp.route('/manage') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_member_management(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -100,7 +100,7 @@ def display_member_management(user_dict=None): @member_management_bp.route('/manage/settings', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_eval(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -111,14 +111,14 @@ def member_management_eval(user_dict=None): post_data = request.get_json() if 'siteLockdown' in post_data: - log.info('Changed Site Lockdown: {}'.format(post_data['siteLockdown'])) + log.info(f'Changed Site Lockdown: {post_data['siteLockdown']}') EvalSettings.query.update( { 'site_lockdown': post_data['siteLockdown'] }) if 'introForm' in post_data: - log.info('Changed Intro Form: {}'.format(post_data['introForm'])) + log.info(f'Changed Intro Form: {post_data['introForm']}') EvalSettings.query.update( { 'intro_form_active': post_data['introForm'] @@ -130,7 +130,7 @@ def member_management_eval(user_dict=None): @member_management_bp.route('/manage/accept_dues_until', methods=['PUT']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_financial(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -142,7 +142,7 @@ def member_management_financial(user_dict=None): if 'acceptDuesUntil' in post_data: date = datetime.strptime(post_data['acceptDuesUntil'], "%Y-%m-%d") - log.info('Changed Dues Accepted Until: {}'.format(date)) + log.info(f'Changed Dues Accepted Until: {date}') EvalSettings.query.update( { 'accept_dues_until': date @@ -154,7 +154,7 @@ def member_management_financial(user_dict=None): @member_management_bp.route('/manage/user', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_adduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -167,7 +167,7 @@ def member_management_adduser(user_dict=None): name = post_data['name'] onfloor_status = post_data['onfloor'] room_number = post_data['roomNumber'] - log.info('Create Freshman Account for {}'.format(name)) + log.info(f'Create Freshman Account for {name}') # empty room numbers should be NULL if room_number == "": @@ -180,7 +180,7 @@ def member_management_adduser(user_dict=None): @member_management_bp.route('/manage/user/upload', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_uploaduser(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -210,7 +210,7 @@ def member_management_uploaduser(user_dict=None): else: rit_username = None - log.info('Create Freshman Account for {} via CSV Upload'.format(name)) + log.info(f'Create Freshman Account for {name} via CSV Upload') db.session.add(FreshmanAccount(name, onfloor_status, room_number, None, rit_username)) db.session.flush() @@ -221,7 +221,7 @@ def member_management_uploaduser(user_dict=None): @member_management_bp.route('/manage/user/', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_edituser(uid, user_dict=None): if not ldap_is_eval_director(user_dict['account']) and not ldap_is_financial_director(user_dict['account']): @@ -248,11 +248,7 @@ def edit_uid(uid, flask_request, username): room_number = post_data['roomNumber'] onfloor_status = post_data['onfloorStatus'] housing_points = post_data['housingPoints'] - log.info('Edit {} - Room: {} On-Floor: {} Points: {}'.format( - uid, - post_data['roomNumber'], - post_data['onfloorStatus'], - post_data['housingPoints'])) + log.info(f'Edit {uid} - Room: {post_data['roomNumber']} On-Floor: {post_data['onfloorStatus']} Points: {post_data['housingPoints']}') #pylint: disable=line-too-long ldap_set_roomnumber(account, room_number) if onfloor_status: @@ -271,7 +267,7 @@ def edit_uid(uid, flask_request, username): ldap_set_housingpoints(account, housing_points) # Only update if there's a diff - log.info('Set {} Active: {}'.format(uid, active_member)) + log.info(f'Set {uid} Active: {active_member}') if ldap_is_active(account) != active_member: if active_member: ldap_set_active(account) @@ -293,12 +289,9 @@ def edit_uid(uid, flask_request, username): def edit_fid(uid, flask_request): log = logger.new(request=flask_request, auth_dict={'username': uid}) post_data = flask_request.get_json() - log.info('Edit freshman-{} - Room: {} On-Floor: {} Eval: {} SigMiss: {}'.format( - uid, - post_data['roomNumber'], - post_data['onfloorStatus'], - post_data['evalDate'], - post_data['sigMissed'])) + + log.info(f'Edit freshman-{uid} - Room: {post_data['roomNumber']} On-Floor: {post_data['onfloorStatus']} Eval: {post_data['evalDate']} SigMiss: {post_data['sigMissed']}') #pylint: disable=line-too-long + name = post_data['name'] @@ -325,11 +318,11 @@ def edit_fid(uid, flask_request): @member_management_bp.route('/manage/user/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_getuserinfo(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Get {}\'s Information'.format(uid)) + log.info(f'Get {uid}\'s Information') if not ldap_is_eval_director(user_dict['account']) and not ldap_is_financial_director(user_dict['account']): return "must be eval or financial director", 403 @@ -410,11 +403,11 @@ def get_hm_date(hm_id): @member_management_bp.route('/manage/user/', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_deleteuser(fid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Delete freshman-{}'.format(fid)) + log.info(f'Delete freshman-{fid}') if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 @@ -422,7 +415,7 @@ def member_management_deleteuser(fid, user_dict=None): if not fid.isdigit(): return "can only delete freshman accounts", 400 - log.info('backend', action="delete freshman account %s" % fid) + log.info('backend', action=f"delete freshman account {fid}") for fca in FreshmanCommitteeAttendance.query.filter(FreshmanCommitteeAttendance.fid == fid): db.session.delete(fca) @@ -444,7 +437,7 @@ def member_management_deleteuser(fid, user_dict=None): # user creation script. There's no reason that the evals director should ever # manually need to do this @member_management_bp.route('/manage/upgrade_user', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_upgrade_user(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -458,7 +451,7 @@ def member_management_upgrade_user(user_dict=None): uid = post_data['uid'] signatures_missed = post_data['sigsMissed'] - log.info('Upgrade freshman-{} to Account: {}'.format(fid, uid)) + log.info(f'Upgrade freshman-{fid} to Account: {uid}') acct = FreshmanAccount.query.filter( FreshmanAccount.id == fid).first() @@ -483,10 +476,7 @@ def member_management_upgrade_user(user_dict=None): db.session.add(MemberHouseMeetingAttendance( uid, fhm.meeting_id, fhm.excuse, fhm.attendance_status)) else: - log.info('Duplicate house meeting attendance! fid: {}, uid: {}, id: {}'.format( - fid, - uid, - fhm.meeting_id)) + log.info(f'Duplicate house meeting attendance! fid: {fid}, uid: {uid}, id: {fhm.meeting_id}') db.session.delete(fhm) new_account = ldap_get_member(uid) @@ -508,7 +498,7 @@ def member_management_upgrade_user(user_dict=None): @member_management_bp.route('/manage/make_user_active', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def member_management_make_user_active(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -519,18 +509,18 @@ def member_management_make_user_active(user_dict=None): return "must be current student, not in bad standing and not active", 403 ldap_set_active(user_dict['account']) - log.info("Make user {} active".format(user_dict['username'])) + log.info(f"Make user {user_dict['username']} active") clear_members_cache() return jsonify({"success": True}), 200 @member_management_bp.route('/member/', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def get_member(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Get {}\'s Information'.format(uid)) + log.info(f'Get {uid}\'s Information') if not ldap_is_eval_director(user_dict['account']): return "must be eval director", 403 @@ -546,7 +536,7 @@ def get_member(uid, user_dict=None): @member_management_bp.route('/manage/active', methods=['DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def clear_active_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -559,7 +549,7 @@ def clear_active_members(user_dict=None): # Clear the active group. for account in members: if account.uid != user_dict['username']: - log.info('Remove {} from Active Status'.format(account.uid)) + log.info(f'Remove {account.uid} from Active Status') ldap_set_inactive(account) return jsonify({"success": True}), 200 @@ -592,7 +582,7 @@ def export_active_list(): @member_management_bp.route('/manage/current/', methods=['POST', 'DELETE']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def remove_current_student(uid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -602,16 +592,16 @@ def remove_current_student(uid, user_dict=None): member = ldap_get_member(uid) if request.method == 'DELETE': - log.info('Remove {} from Current Student'.format(uid)) + log.info(f'Remove {uid} from Current Student') ldap_set_non_current_student(member) elif request.method == 'POST': - log.info('Add {} to Current Students'.format(uid)) + log.info(f'Add {uid} to Current Students') ldap_set_current_student(member) return jsonify({"success": True}), 200 @member_management_bp.route('/manage/new', methods=['GET']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def new_year(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/blueprints/slideshow.py b/conditional/blueprints/slideshow.py index c745e24c..1ea953d3 100644 --- a/conditional/blueprints/slideshow.py +++ b/conditional/blueprints/slideshow.py @@ -20,7 +20,7 @@ @slideshow_bp.route('/slideshow/intro') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -36,7 +36,7 @@ def slideshow_intro_display(user_dict=None): @slideshow_bp.route('/slideshow/intro/members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -49,7 +49,7 @@ def slideshow_intro_members(user_dict=None): @slideshow_bp.route('/slideshow/intro/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_intro_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -61,7 +61,7 @@ def slideshow_intro_review(user_dict=None): uid = post_data['uid'] status = post_data['status'] - log.info('Intro Eval for {}: {}'.format(uid, status)) + log.info(f'Intro Eval for {uid}: {status}') FreshmanEvalData.query.filter( FreshmanEvalData.uid == uid and FreshmanEvalData.active). \ @@ -76,7 +76,7 @@ def slideshow_intro_review(user_dict=None): @slideshow_bp.route('/slideshow/spring') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_display(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -92,7 +92,7 @@ def slideshow_spring_display(user_dict=None): @slideshow_bp.route('/slideshow/spring/members') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_members(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -105,7 +105,7 @@ def slideshow_spring_members(user_dict=None): @slideshow_bp.route('/slideshow/spring/review', methods=['POST']) -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def slideshow_spring_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) @@ -117,7 +117,7 @@ def slideshow_spring_review(user_dict=None): uid = post_data['uid'] status = post_data['status'] - log.info('Spring Eval for {}: {}'.format(uid, status)) + log.info(f'Spring Eval for {uid}: {status}') SpringEval.query.filter( SpringEval.uid == uid and diff --git a/conditional/blueprints/spring_evals.py b/conditional/blueprints/spring_evals.py index e8c6e1db..212ca88e 100644 --- a/conditional/blueprints/spring_evals.py +++ b/conditional/blueprints/spring_evals.py @@ -17,7 +17,7 @@ @spring_evals_bp.route('/spring_evals/') -@auth.oidc_auth +@auth.oidc_auth("default") @get_user def display_spring_evals(internal=False, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) diff --git a/conditional/models/models.py b/conditional/models/models.py index fec3036a..423795ea 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -18,7 +18,7 @@ class FreshmanAccount(db.Model): signatures_missed = Column(Integer) rit_username = Column(String(10), nullable=True) - def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): + def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): # pylint: disable=too-many-positional-arguments self.name = name today = date.fromtimestamp(time.time()) self.eval_date = today + timedelta(weeks=6) @@ -32,8 +32,7 @@ class FreshmanEvalData(db.Model): __tablename__ = 'freshman_eval_data' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - freshman_project = Column(Enum('Pending', 'Passed', 'Failed', - name="freshman_project_enum"), nullable=True) + freshman_project = Column(Enum('Pending', 'Passed', 'Failed', name="freshman_project_enum"), nullable=True) eval_date = Column(DateTime, nullable=False) signatures_missed = Column(Integer, nullable=False) social_events = Column(Text) @@ -143,7 +142,7 @@ class MajorProject(db.Model): name="major_project_enum"), nullable=False) - def __init__(self, uid, name, tldr, time, desc): + def __init__(self, uid, name, tldr, time, desc): # pylint: disable=too-many-positional-arguments,redefined-outer-name self.uid = uid self.date = datetime.now() self.name = name @@ -233,7 +232,7 @@ class Conditional(db.Model): s_evaluation = Column(ForeignKey('spring_evals.id')) i_evaluation = Column(ForeignKey('freshman_eval_data.id')) - def __init__(self, uid, description, due, s_eval=None, i_eval=None): + def __init__(self, uid, description, due, s_eval=None, i_eval=None): # pylint: disable=too-many-positional-arguments self.uid = uid self.description = description self.date_due = due @@ -293,7 +292,7 @@ class UserLog(db.Model): path = Column(String(128), nullable=False) description = Column(String(128), nullable=False) - def __init__(self, ipaddr, user, method, blueprint, path, description): + def __init__(self, ipaddr, user, method, blueprint, path, description): # pylint: disable=too-many-positional-arguments self.ipaddr = ipaddr self.timestamp = datetime.now() self.uid = user diff --git a/conditional/util/context_processors.py b/conditional/util/context_processors.py index feecb107..766e4d82 100644 --- a/conditional/util/context_processors.py +++ b/conditional/util/context_processors.py @@ -33,7 +33,9 @@ def check_current_student(username): @app.context_processor def utility_processor(): - return dict( - get_csh_name=get_csh_name, get_freshman_name=get_freshman_name, get_member_name=get_member_name, - check_current_student=check_current_student - ) + return { + "get_csh_name": get_csh_name, + "get_freshman_name": get_freshman_name, + "get_member_name": get_member_name, + "check_current_student": check_current_student + } diff --git a/requirements.in b/requirements.in index 5636cd6d..7ba4ae4f 100644 --- a/requirements.in +++ b/requirements.in @@ -1,33 +1,33 @@ -alembic~=0.9.8 -astroid~=2.4.0 +alembic~=1.15.1 +astroid~=3.3.9 blinker~=1.4 boto3==1.35.13 botocore==1.35.13 -click~=7.1 +click~=8.1.8 csh_ldap>=2.3.1 -ddtrace~=1.1.4 -Flask~=1.1.4 +ddtrace~=3.3.0 +Flask~=3.1.0 Flask-Migrate~=2.1.1 Flask-Gzip~=0.2 -Flask-pyoidc~=1.3.0 -Flask-SQLAlchemy~=2.3.2 +Flask-pyoidc~=3.14.3 +Flask-SQLAlchemy~=3.1.1 gunicorn~=20.1.0 isort~=4.3.4 -itsdangerous~=0.24 -Jinja2~=2.10 +itsdangerous~=2.2.0 +Jinja2~=3.1.6 lazy-object-proxy~=1.4.0 Mako~=1.0.7 -MarkupSafe~=1.0 +MarkupSafe~=3.0.2 mccabe~=0.6.1 -oic~=0.11.0 -pip-tools~=6.6.2 +oic~=1.6.1 +pip-tools~=7.4.1 psycopg2-binary~=2.9.3 -pylint~=2.6.0 +pylint~=3.3.6 python-dateutil~=2.6.1 python-editor~=1.0.3 -sentry-sdk[flask]~=0.19.5 -six~=1.12.0 -SQLAlchemy~=1.3.22 +sentry-sdk[flask]~=2.24.1 +six~=1.17.0 +SQLAlchemy~=2.0.40 structlog~=18.1.0 -Werkzeug~=0.15.3 +Werkzeug~=3.1.3 wrapt~=1.11.0 diff --git a/requirements.txt b/requirements.txt index 3bc182c3..ba5d4ba2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,35 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # # pip-compile requirements.in # -alabaster==0.7.12 - # via oic -alembic==0.9.10 +alembic==1.15.1 # via # -r requirements.in # flask-migrate -astroid==2.4.2 +annotated-types==0.7.0 + # via pydantic +astroid==3.3.9 # via # -r requirements.in # pylint -attrs==21.4.0 - # via ddtrace -beaker==1.11.0 - # via oic -blinker==1.4 +blinker==1.9.0 # via # -r requirements.in + # flask # sentry-sdk boto3==1.35.13 + # via -r requirements.in botocore==1.35.13 + # via + # -r requirements.in + # boto3 + # s3transfer +build==1.2.2.post1 + # via pip-tools +bytecode==0.16.1 + # via ddtrace certifi==2022.5.18.1 # via # requests @@ -32,22 +38,28 @@ cffi==1.15.0 # via cryptography charset-normalizer==2.0.12 # via requests -click==7.1.2 +click==8.1.8 # via # -r requirements.in # flask # pip-tools cryptography==37.0.2 - # via pyopenssl + # via oic csh-ldap==2.3.1 # via -r requirements.in -ddsketch==2.0.3 - # via ddtrace -ddtrace==1.1.4 +ddtrace==3.3.0 # via -r requirements.in +defusedxml==0.7.1 + # via oic +deprecated==1.2.18 + # via opentelemetry-api +dill==0.3.9 + # via pylint dnspython==2.2.1 # via srvlookup -flask==1.1.4 +envier==0.6.1 + # via ddtrace +flask==3.1.0 # via # -r requirements.in # flask-gzip @@ -59,64 +71,74 @@ flask-gzip==0.2 # via -r requirements.in flask-migrate==2.1.1 # via -r requirements.in -flask-pyoidc==1.3.0 +flask-pyoidc==3.14.3 # via -r requirements.in -flask-sqlalchemy==2.3.2 +flask-sqlalchemy==3.1.1 # via # -r requirements.in # flask-migrate future==0.18.2 - # via - # oic - # pyjwkest + # via pyjwkest +greenlet==3.1.1 + # via sqlalchemy gunicorn==20.1.0 # via -r requirements.in idna==3.3 # via requests +importlib-metadata==8.6.1 + # via opentelemetry-api +importlib-resources==6.5.2 + # via flask-pyoidc isort==4.3.21 # via # -r requirements.in # pylint -itsdangerous==0.24 +itsdangerous==2.2.0 # via # -r requirements.in # flask -jinja2==2.11.3 +jinja2==3.1.6 # via # -r requirements.in # flask -lazy-object-proxy==1.4.3 +jmespath==1.0.1 # via - # -r requirements.in - # astroid + # boto3 + # botocore +lazy-object-proxy==1.4.3 + # via -r requirements.in +legacy-cgi==2.6.3 + # via ddtrace mako==1.0.14 # via # -r requirements.in # alembic # oic -markupsafe==1.1.1 +markupsafe==3.0.2 # via # -r requirements.in # jinja2 # mako + # sentry-sdk + # werkzeug mccabe==0.6.1 # via # -r requirements.in # pylint -oic==0.11.0.1 +oic==1.6.1 # via # -r requirements.in # flask-pyoidc -packaging==21.3 +opentelemetry-api==1.31.1 # via ddtrace -pep517==0.12.0 - # via pip-tools -pip-tools==6.6.2 +packaging==24.2 + # via build +pip-tools==7.4.1 # via -r requirements.in +platformdirs==4.3.7 + # via pylint protobuf==3.20.1 - # via - # ddsketch - # ddtrace + # via ddtrace psycopg2-binary==2.9.3 # via -r requirements.in pyasn1==0.4.8 @@ -131,41 +153,46 @@ pycryptodomex==3.14.1 # via # oic # pyjwkest +pydantic==2.11.0 + # via pydantic-settings +pydantic-core==2.33.0 + # via pydantic +pydantic-settings==2.8.1 + # via oic pyjwkest==1.4.2 # via oic -pylint==2.6.2 +pylint==3.3.6 # via -r requirements.in -pyopenssl==22.0.0 - # via oic -pyparsing==3.0.9 - # via packaging +pyproject-hooks==1.2.0 + # via + # build + # pip-tools python-dateutil==2.6.1 # via # -r requirements.in - # alembic + # botocore +python-dotenv==1.1.0 + # via pydantic-settings python-editor==1.0.4 - # via - # -r requirements.in - # alembic + # via -r requirements.in python-ldap==3.0.0 # via csh-ldap requests==2.27.1 # via + # flask-pyoidc # oic # pyjwkest -sentry-sdk[flask]==0.19.5 +s3transfer==0.10.4 + # via boto3 +sentry-sdk[flask]==2.24.1 # via -r requirements.in -six==1.12.0 +six==1.17.0 # via # -r requirements.in - # astroid - # ddsketch - # ddtrace - # oic # pyjwkest # python-dateutil # structlog -sqlalchemy==1.3.24 +sqlalchemy==2.0.40 # via # -r requirements.in # alembic @@ -174,17 +201,24 @@ srvlookup==2.0.0 # via csh-ldap structlog==18.1.0 # via -r requirements.in -tenacity==8.0.1 - # via ddtrace -toml==0.10.2 +tomlkit==0.13.2 # via pylint -tomli==2.0.1 - # via pep517 -urllib3==1.26.9 +typing-extensions==4.13.0 # via + # alembic + # ddtrace + # pydantic + # pydantic-core + # sqlalchemy + # typing-inspection +typing-inspection==0.4.0 + # via pydantic +urllib3==1.26.20 + # via + # botocore # requests # sentry-sdk -werkzeug==0.15.6 +werkzeug==3.1.3 # via # -r requirements.in # flask @@ -193,7 +227,12 @@ wheel==0.37.1 wrapt==1.11.2 # via # -r requirements.in - # astroid + # ddtrace + # deprecated +xmltodict==0.14.2 + # via ddtrace +zipp==3.21.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip From d3a02802d345e28a6c2855f9b99c99b67b6cff62 Mon Sep 17 00:00:00 2001 From: Ella S <112343747+pikachu0542@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:11:37 -0400 Subject: [PATCH 52/76] Updated wrapt, downgraded ddtrace, fixed the ddtrace problems (#395) --- requirements.in | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.in b/requirements.in index 7ba4ae4f..49cd221c 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ boto3==1.35.13 botocore==1.35.13 click~=8.1.8 csh_ldap>=2.3.1 -ddtrace~=3.3.0 +ddtrace~=3.2.1 Flask~=3.1.0 Flask-Migrate~=2.1.1 Flask-Gzip~=0.2 @@ -30,4 +30,4 @@ six~=1.17.0 SQLAlchemy~=2.0.40 structlog~=18.1.0 Werkzeug~=3.1.3 -wrapt~=1.11.0 +wrapt~=1.17.2 diff --git a/requirements.txt b/requirements.txt index ba5d4ba2..44225432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ cryptography==37.0.2 # via oic csh-ldap==2.3.1 # via -r requirements.in -ddtrace==3.3.0 +ddtrace==3.2.2 # via -r requirements.in defusedxml==0.7.1 # via oic @@ -224,7 +224,7 @@ werkzeug==3.1.3 # flask wheel==0.37.1 # via pip-tools -wrapt==1.11.2 +wrapt==1.17.2 # via # -r requirements.in # ddtrace From c54f351484572d6a6d6c90d5b1e08c573aedd5d1 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 9 Sep 2025 17:56:37 -0400 Subject: [PATCH 53/76] packet glue --- .github/workflows/python-app.yml | 2 +- conditional/__init__.py | 111 +++-- conditional/blueprints/packet.py | 423 ++++++++++++++++++ conditional/models/models.py | 374 +++++++++++++--- conditional/templates/active_packets.html | 117 +++++ conditional/templates/admin_freshmen.html | 27 ++ conditional/templates/admin_packets.html | 30 ++ conditional/templates/error.html | 20 + conditional/templates/extend/base.html | 29 ++ conditional/templates/extend/email.html | 17 + .../templates/include/admin/all_freshmen.html | 33 ++ .../templates/include/admin/new_packets.html | 23 + .../templates/include/admin/open_packets.html | 39 ++ .../include/admin/sync_freshmen.html | 22 + conditional/templates/include/footer.html | 6 + conditional/templates/include/head.html | 98 ++++ conditional/templates/include/nav.html | 67 +++ conditional/templates/include/scripts.html | 15 + conditional/templates/mail/packet_start.html | 15 + conditional/templates/mail/packet_start.txt | 14 + conditional/templates/mail/report.html | 10 + conditional/templates/mail/report.txt | 7 + conditional/templates/not_found.html | 17 + conditional/templates/packet.html | 194 ++++++++ conditional/templates/packet_stats.html | 84 ++++ conditional/templates/upperclassman.html | 59 +++ .../templates/upperclassmen_totals.html | 90 ++++ conditional/util/auth.py | 99 +++- conditional/util/ldap.py | 110 ++++- conditional/util/mail.py | 53 +++ conditional/util/packet.py | 197 ++++++++ conditional/util/stats.py | 161 +++++++ requirements.in | 1 + requirements.txt | 4 + 34 files changed, 2428 insertions(+), 140 deletions(-) create mode 100644 conditional/blueprints/packet.py create mode 100644 conditional/templates/active_packets.html create mode 100644 conditional/templates/admin_freshmen.html create mode 100644 conditional/templates/admin_packets.html create mode 100644 conditional/templates/error.html create mode 100644 conditional/templates/extend/base.html create mode 100644 conditional/templates/extend/email.html create mode 100644 conditional/templates/include/admin/all_freshmen.html create mode 100644 conditional/templates/include/admin/new_packets.html create mode 100644 conditional/templates/include/admin/open_packets.html create mode 100644 conditional/templates/include/admin/sync_freshmen.html create mode 100644 conditional/templates/include/footer.html create mode 100644 conditional/templates/include/head.html create mode 100644 conditional/templates/include/nav.html create mode 100644 conditional/templates/include/scripts.html create mode 100644 conditional/templates/mail/packet_start.html create mode 100644 conditional/templates/mail/packet_start.txt create mode 100644 conditional/templates/mail/report.html create mode 100644 conditional/templates/mail/report.txt create mode 100644 conditional/templates/not_found.html create mode 100644 conditional/templates/packet.html create mode 100644 conditional/templates/packet_stats.html create mode 100644 conditional/templates/upperclassman.html create mode 100644 conditional/templates/upperclassmen_totals.html create mode 100644 conditional/util/mail.py create mode 100644 conditional/util/packet.py create mode 100644 conditional/util/stats.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 980b1ab3..38ed5fa9 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -31,4 +31,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with pylint run: | - pylint conditional + pylint conditional --disable=logging-fstring-interpolation diff --git a/conditional/__init__.py b/conditional/__init__.py index 1c2f5ee9..4ccbc5a5 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -31,19 +31,26 @@ # Sentry setup sentry_sdk.init( - dsn=app.config['SENTRY_DSN'], + dsn=app.config["SENTRY_DSN"], integrations=[FlaskIntegration(), SqlalchemyIntegration()], - environment=app.config['SENTRY_ENV'], + environment=app.config["SENTRY_ENV"], ) -ldap = CSHLDAP(app.config['LDAP_BIND_DN'], - app.config['LDAP_BIND_PW'], - ro=app.config['LDAP_RO']) +ldap = CSHLDAP( + app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PW"], ro=app.config["LDAP_RO"] +) client_metadata = ClientMetadata(app.config["OIDC_CLIENT_CONFIG"]) -provider_config = ProviderConfiguration(issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata) +provider_config = ProviderConfiguration( + issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata +) +frosh_provider_config = ProviderConfiguration( + issuer=app.config["FROSH_OIDC_ISSUER"], client_registration_info=client_metadata +) -auth = OIDCAuthentication({'default': provider_config}, app) +auth = OIDCAuthentication( + {"default": provider_config, "frosh": frosh_provider_config}, app +) app.secret_key = app.config["SECRET_KEY"] @@ -60,42 +67,48 @@ def start_of_year(): # Configure Logging -def request_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name - if 'request' in event_dict: - flask_request = event_dict['request'] - event_dict['ip'] = flask_request.remote_addr - event_dict['method'] = flask_request.method - event_dict['blueprint'] = flask_request.blueprint - event_dict['path'] = flask_request.full_path - if 'auth_dict' in event_dict: - auth_dict = event_dict['auth_dict'] - event_dict['user'] = auth_dict['username'] +def request_processor( + logger, log_method, event_dict +): # pylint: disable=unused-argument, redefined-outer-name + if "request" in event_dict: + flask_request = event_dict["request"] + event_dict["ip"] = flask_request.remote_addr + event_dict["method"] = flask_request.method + event_dict["blueprint"] = flask_request.blueprint + event_dict["path"] = flask_request.full_path + if "auth_dict" in event_dict: + auth_dict = event_dict["auth_dict"] + event_dict["user"] = auth_dict["username"] return event_dict -def database_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name - if 'request' in event_dict: - if event_dict['method'] != 'GET': +def database_processor( + logger, log_method, event_dict +): # pylint: disable=unused-argument, redefined-outer-name + if "request" in event_dict: + if event_dict["method"] != "GET": log = UserLog( - ipaddr=event_dict['ip'], - user=event_dict['user'], - method=event_dict['method'], - blueprint=event_dict['blueprint'], - path=event_dict['path'], - description=event_dict['event'] + ipaddr=event_dict["ip"], + user=event_dict["user"], + method=event_dict["method"], + blueprint=event_dict["blueprint"], + path=event_dict["path"], + description=event_dict["event"], ) db.session.add(log) db.session.flush() db.session.commit() - del event_dict['request'] + del event_dict["request"] return event_dict -structlog.configure(processors=[ - request_processor, - database_processor, - structlog.processors.KeyValueRenderer() -]) +structlog.configure( + processors=[ + request_processor, + database_processor, + structlog.processors.KeyValueRenderer(), + ] +) logger = structlog.get_logger() @@ -133,16 +146,16 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse from .util.ldap import ldap_get_member -@app.route('/') +@app.route("/") def static_proxy(path): # send_static_file will guess the correct MIME type return app.send_static_file(path) -@app.route('/') +@app.route("/") @auth.oidc_auth("default") def default_route(): - return redirect('/dashboard') + return redirect("/dashboard") @app.route("/logout") @@ -156,7 +169,7 @@ def health(): """ Shows an ok status if the application is up and running """ - return {'status': 'ok'} + return {"status": "ok"} @app.errorhandler(404) @@ -167,17 +180,17 @@ def route_errors(error, user_dict=None): data = {} # Handle the case where the header isn't present - if user_dict['username'] is not None: - data['username'] = user_dict['account'].uid - data['name'] = user_dict['account'].cn + if user_dict["username"] is not None: + data["username"] = user_dict["account"].uid + data["name"] = user_dict["account"].cn else: - data['username'] = "unknown" - data['name'] = "Unknown" + data["username"] = "unknown" + data["name"] = "Unknown" # Figure out what kind of error was passed if isinstance(error, int): code = error - elif hasattr(error, 'code'): + elif hasattr(error, "code"): code = error.code else: # Unhandled exception @@ -189,11 +202,13 @@ def route_errors(error, user_dict=None): else: error_desc = type(error).__name__ - return render_template('errors.html', - error=error_desc, - error_code=code, - event_id=sentry_sdk.last_event_id(), - **data), int(code) + return render_template( + "errors.html", + error=error_desc, + error_code=code, + event_id=sentry_sdk.last_event_id(), + **data + ), int(code) -logger.info('conditional started') +logger.info("conditional started") diff --git a/conditional/blueprints/packet.py b/conditional/blueprints/packet.py new file mode 100644 index 00000000..a37a837e --- /dev/null +++ b/conditional/blueprints/packet.py @@ -0,0 +1,423 @@ +import json +from datetime import datetime +from operator import itemgetter + +import structlog +from flask import Blueprint, redirect, render_template, request, session + +from conditional import auth, app, db +from conditional.util import stats as stats_module +from conditional.util.context_processors import get_freshman_name +from conditional.util.mail import send_report_mail +from conditional.util.auth import get_user, needs_auth +from conditional.util.ldap import ldap_is_eval_director +from conditional.util.packet import ( + create_new_packets, + sync_freshman_list, + sync_with_ldap, +) +from conditional.models.models import ( + MiscSignature, + Packet, + Freshman, +) + +logger = structlog.get_logger() + +packet_bp = Blueprint("packet_bp", __name__) + + +class POSTFreshman: + def __init__(self, freshman): + self.name = freshman["name"].strip() + self.rit_username = freshman["rit_username"].strip() + self.onfloor = freshman["onfloor"].strip() == "TRUE" + + +@packet_bp.route("/admin/packets") +@auth.oidc_auth("default") +@get_user +def admin_packets(user_dict=None): + if not ldap_is_eval_director(user_dict["account"]): + return redirect("/dashboard") + + open_packets = Packet.open_packets() + + # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() + for packet in open_packets: + packet.did_sign_result = packet.did_sign( + user_dict["username"], app.config["REALM"] == "csh" + ) + packet.signatures_received_result = packet.signatures_received() + packet.signatures_required_result = packet.signatures_required() + + open_packets.sort(key=packet_sort_key, reverse=True) + + return render_template( + "admin_packets.html", open_packets=open_packets, info=user_dict + ) + + +@packet_bp.route("/admin/freshmen") +@auth.oidc_auth("default") +@get_user +def admin_freshmen(user_dict=None): + if not ldap_is_eval_director(user_dict["account"]): + return redirect("/dashboard") + + all_freshmen = Freshman.get_all() + + return render_template( + "admin_freshmen.html", all_freshmen=all_freshmen, info=user_dict + ) + + +@packet_bp.route("/api/v1/freshmen", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def sync_freshman(user_dict=None): + """ + Create or update freshmen entries from a list + + Body parameters: [ + { + rit_username: string + name: string + onfloor: boolean + } + ] + """ + + # Only allow evals to create new frosh + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + + freshmen_in_post = { + freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json) + } + sync_freshman_list(freshmen_in_post) + return json.dumps("Done"), 200 + + +@packet_bp.route("/api/v1/packets", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def create_packet(user_dict=None): + """ + Create a new packet. + + Body parameters: { + start_date: the start date of the packets in MM/DD/YYYY format + freshmen: [ + { + rit_username: string + name: string + onfloor: boolean + } + ] + } + """ + + # Only allow evals to create new packets + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + + base_date = datetime.strptime(request.json["start_date"], "%m/%d/%Y").date() + + freshmen_in_post = { + freshman.rit_username: freshman + for freshman in map(POSTFreshman, request.json["freshmen"]) + } + + create_new_packets(base_date, freshmen_in_post) + + return json.dumps("Done"), 201 + + +@packet_bp.route("/api/v1/sync", methods=["POST"]) +@auth.oidc_auth("default") +@get_user +def sync_ldap(user_dict=None): + # Only allow evals to sync ldap + if not ldap_is_eval_director(user_dict["account"]): + redirect("/dashboard") + sync_with_ldap() + return json.dumps("Done"), 201 + + +@packet_bp.route("/api/v1/packets/", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_packets_by_user(username: str, user_dict=None) -> dict: + """ + Return a dictionary of packets for a freshman by username, giving packet start and end date by packet id + """ + + if user_dict["ritdn"] != username: + redirect("/dashboard") + frosh = Freshman.by_username(username) + + return { + packet.id: { + "start": packet.start, + "end": packet.end, + } + for packet in frosh.packets + } + + +@packet_bp.route("/api/v1/packets//newest", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_newest_packet_by_user(username: str, user_dict=None) -> dict: + """ + Return a user's newest packet + """ + + if not user_dict["is_upper"] and user_dict["ritdn"] != username: + redirect("/dashboard") + + frosh = Freshman.by_username(username) + + packet = frosh.packets[-1] + + return { + packet.id: { + "start": packet.start, + "end": packet.end, + "required": vars(packet.signatures_required()), + "received": vars(packet.signatures_received()), + } + } + + +@packet_bp.route("/api/v1/packet/", methods=["GET"]) +@auth.oidc_auth("default") +@get_user +def get_packet_by_id(packet_id: int, user_dict=None) -> dict: + """ + Return the scores of the packet in question + """ + + packet = Packet.by_id(packet_id) + + if user_dict["ritdn"] != packet.freshman.rit_username: + redirect("/dashboard") + + return { + "required": vars(packet.signatures_required()), + "received": vars(packet.signatures_received()), + } + + +@packet_bp.route("/api/v1/sign//", methods=["POST"]) +@needs_auth +def sign(packet_id, user_dict=None): + packet = Packet.by_id(packet_id) + + if packet is not None and packet.is_open(): + if session["provider"] == "csh": + # Check if the CSHer is an upperclassman and if so, sign that row + for sig in filter( + lambda sig: sig.member == user_dict["uid"], packet.upper_signatures + ): + sig.signed = True + app.logger.info( + f"Member {user_dict['uid']} signed packet {packet_id} as an upperclassman" + ) + return commit_sig(packet) + + # The CSHer is a misc so add a new row + db.session.add(MiscSignature(packet=packet, member=user_dict["uid"])) + app.logger.info( + f"Member {user_dict['uid']} signed packet {packet_id} as a misc" + ) + return commit_sig(packet) + if session["provider"] == "frosh": + # Check if the freshman is onfloor and if so, sign that row + for sig in filter( + lambda sig: sig.freshman_username == user_dict["uid"], + packet.fresh_signatures, + ): + sig.signed = True + app.logger.info( + f"Freshman {user_dict['uid']} signed packet {packet_id}" + ) + return commit_sig(packet) + + app.logger.warning( + f"Failed to add {user_dict['uid']}'s signature to packet {packet_id}" + ) + return "Error: Signature not valid. Reason: Unknown" + + +@packet_bp.route("/api/v1/report/", methods=["POST"]) +@needs_auth +def report(user_dict=None): + if session["provider"] != "frosh": + return "Failure", 403 + + form_results = request.form + send_report_mail(form_results, get_freshman_name(user_dict["username"])) + return "Success: " + get_freshman_name(user_dict["username"]) + " sent a report" + + +@packet_bp.route("/api/v1/stats/packet/") +@auth.oidc_auth("default") +@get_user +def packet_stats(packet_id, user_dict=None): + if user_dict["ritdn"] != Packet.by_id(packet_id).freshman.rit_username: + return redirect("/dashboard") + return stats_module.packet_stats(packet_id) + + +@packet_bp.route("/api/v1/stats/upperclassman/") +@auth.oidc_auth("default") +@get_user +def upperclassman_stats(uid): + return stats_module.upperclassman_stats(uid) + + +def commit_sig(packet): + db.session.commit() + + return "Success: Signed Packet: " + packet.freshman_username + + +@packet_bp.route("/packet//") +@needs_auth +def freshman_packet(packet_id, user_dict=None): + packet = Packet.by_id(packet_id) + + if packet is None: + return "Invalid packet or freshman", 404 + + # The current user's freshman signature on this packet + fresh_sig = list( + filter( + lambda sig: ( + sig.freshman_username == user_dict["ritdn"] if user_dict else "" + ), + packet.fresh_signatures, + ) + ) + + return render_template( + "packet.html", + info=user_dict, + packet=packet, + did_sign=packet.did_sign(user_dict["uid"], app.config["REALM"] == "csh"), + required=packet.signatures_required(), + received=packet.signatures_received(), + upper=packet.upper_signatures, + fresh_sig=fresh_sig, + ) + + +def packet_sort_key(packet): + """ + Utility function for generating keys for sorting packets + """ + return ( + packet.freshman.name, + -packet.signatures_received_result.total, + not packet.did_sign_result, + ) + + +@packet_bp.route("/packets/") +@needs_auth +def packets(user_dict=None): + open_packets = Packet.open_packets() + + # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() + for packet in open_packets: + packet.did_sign_result = packet.did_sign( + user_dict["uid"], app.config["REALM"] == "csh" + ) + packet.signatures_received_result = packet.signatures_received() + packet.signatures_required_result = packet.signatures_required() + + open_packets.sort(key=packet_sort_key) + + return render_template("active_packets.html", info=user_dict, packets=open_packets) + + +@packet_bp.route("/") +def index(): + return """ +

Hello, world! 2

+ Click here 4 frosh + Click here 4 upper + """ + + +@app.route("/upperclassmen/") +@auth.oidc_auth("default") +@get_user +def upperclassmen_total(user_dict=None): + open_packets = Packet.open_packets() + + # Sum up the signed packets per upperclassman + upperclassmen = {} + misc = {} + for packet in open_packets: + for sig in packet.upper_signatures: + if sig.member not in upperclassmen: + upperclassmen[sig.member] = 0 + + if sig.signed: + upperclassmen[sig.member] += 1 + for sig in packet.misc_signatures: + misc[sig.member] = 1 + misc.get(sig.member, 0) + + return render_template( + "upperclassmen_totals.html", + info=user_dict, + num_open_packets=len(open_packets), + upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True), + misc=sorted(misc.items(), key=itemgetter(1), reverse=True), + ) + + +@app.route("/stats/packet/") +@auth.oidc_auth("default") +@get_user +def packet_graphs(packet_id, user_dict=None): + stats = packet_stats(packet_id) + fresh = [] + misc = [] + upper = [] + + # Make a rolling sum of signatures over time + def agg(l, attr, date): + l.append((l[-1] if l else 0) + len(stats["dates"][date][attr])) + + dates = list(stats["dates"].keys()) + for date in dates: + agg(fresh, "fresh", date) + agg(misc, "misc", date) + agg(upper, "upper", date) + + # Stack misc and upper on top of fresh for a nice stacked line graph + for i in range(len(dates)): + misc[i] = misc[i] + fresh[i] + upper[i] = upper[i] + misc[i] + + return render_template( + "packet_stats.html", + info=user_dict, + data=json.dumps( + { + "dates": dates, + "accum": { + "fresh": fresh, + "misc": misc, + "upper": upper, + }, + "daily": {}, + } + ), + fresh=stats["freshman"], + packet=Packet.by_id(packet_id), + ) diff --git a/conditional/models/models.py b/conditional/models/models.py index 423795ea..5bb3dd64 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -1,15 +1,27 @@ +from typing import cast, Optional import time from datetime import date, timedelta, datetime -from sqlalchemy import Column, Integer, String, Enum, ForeignKey, DateTime, \ - Date, Text, Boolean +from itertools import chain +from sqlalchemy import ( + Column, + Integer, + String, + Enum, + ForeignKey, + DateTime, + Date, + Text, + Boolean, +) from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import relationship from conditional import db -attendance_enum = Enum('Attended', 'Excused', 'Absent', name='attendance_enum') +attendance_enum = Enum("Attended", "Excused", "Absent", name="attendance_enum") class FreshmanAccount(db.Model): - __tablename__ = 'freshman_accounts' + __tablename__ = "freshman_accounts" id = Column(Integer, primary_key=True) name = Column(String(64), nullable=False) eval_date = Column(Date, nullable=False) @@ -18,7 +30,9 @@ class FreshmanAccount(db.Model): signatures_missed = Column(Integer) rit_username = Column(String(10), nullable=True) - def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): # pylint: disable=too-many-positional-arguments + def __init__( + self, name, onfloor, room=None, missed=None, rit_username=None + ): # pylint: disable=too-many-positional-arguments self.name = name today = date.fromtimestamp(time.time()) self.eval_date = today + timedelta(weeks=6) @@ -29,22 +43,25 @@ def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): # class FreshmanEvalData(db.Model): - __tablename__ = 'freshman_eval_data' + __tablename__ = "freshman_eval_data" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - freshman_project = Column(Enum('Pending', 'Passed', 'Failed', name="freshman_project_enum"), nullable=True) + freshman_project = Column( + Enum("Pending", "Passed", "Failed", name="freshman_project_enum"), nullable=True + ) eval_date = Column(DateTime, nullable=False) signatures_missed = Column(Integer, nullable=False) social_events = Column(Text) other_notes = Column(Text) - freshman_eval_result = Column(Enum('Pending', 'Passed', 'Failed', - name="freshman_eval_enum"), nullable=False) + freshman_eval_result = Column( + Enum("Pending", "Passed", "Failed", name="freshman_eval_enum"), nullable=False + ) active = Column(Boolean) def __init__(self, uid, signatures_missed): self.uid = uid self.freshman_project = None - self.freshman_eval_result = 'Pending' + self.freshman_eval_result = "Pending" self.signatures_missed = signatures_missed self.social_events = "" self.other_notes = "" @@ -52,12 +69,24 @@ def __init__(self, uid, signatures_missed): class CommitteeMeeting(db.Model): - __tablename__ = 'committee_meetings' + __tablename__ = "committee_meetings" id = Column(Integer, primary_key=True) - committee = Column(Enum('Evaluations', 'History', 'Social', 'Opcomm', - 'R&D', 'House Improvements', 'Financial', - 'Public Relations', 'Chairman', 'Ad-Hoc', name="committees_enum"), - nullable=False) + committee = Column( + Enum( + "Evaluations", + "History", + "Social", + "Opcomm", + "R&D", + "House Improvements", + "Financial", + "Public Relations", + "Chairman", + "Ad-Hoc", + name="committees_enum", + ), + nullable=False, + ) timestamp = Column(DateTime, nullable=False) approved = Column(Boolean, nullable=False) active = Column(Boolean) @@ -70,10 +99,10 @@ def __init__(self, committee, timestamp, approved): class MemberCommitteeAttendance(db.Model): - __tablename__ = 'member_committee_attendance' + __tablename__ = "member_committee_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) + meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) def __init__(self, uid, meeting_id): self.uid = uid @@ -81,10 +110,10 @@ def __init__(self, uid, meeting_id): class FreshmanCommitteeAttendance(db.Model): - __tablename__ = 'freshman_committee_attendance' + __tablename__ = "freshman_committee_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) def __init__(self, fid, meeting_id): self.fid = fid @@ -92,7 +121,7 @@ def __init__(self, fid, meeting_id): class TechnicalSeminar(db.Model): - __tablename__ = 'technical_seminars' + __tablename__ = "technical_seminars" id = Column(Integer, primary_key=True) name = Column(String(128), nullable=False) timestamp = Column(DateTime, nullable=False) @@ -107,10 +136,10 @@ def __init__(self, name, timestamp, approved): class MemberSeminarAttendance(db.Model): - __tablename__ = 'member_seminar_attendance' + __tablename__ = "member_seminar_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) + seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) def __init__(self, uid, seminar_id): self.uid = uid @@ -118,10 +147,10 @@ def __init__(self, uid, seminar_id): class FreshmanSeminarAttendance(db.Model): - __tablename__ = 'freshman_seminar_attendance' + __tablename__ = "freshman_seminar_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) def __init__(self, fid, seminar_id): self.fid = fid @@ -129,7 +158,7 @@ def __init__(self, fid, seminar_id): class MajorProject(db.Model): - __tablename__ = 'major_projects' + __tablename__ = "major_projects" id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) uid = Column(String(32), nullable=False) @@ -138,23 +167,25 @@ class MajorProject(db.Model): time = Column(Text, nullable=False) description = Column(Text, nullable=False) active = Column(Boolean, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="major_project_enum"), - nullable=False) + status = Column( + Enum("Pending", "Passed", "Failed", name="major_project_enum"), nullable=False + ) - def __init__(self, uid, name, tldr, time, desc): # pylint: disable=too-many-positional-arguments,redefined-outer-name + def __init__( + self, uid, name, tldr, time, desc + ): # pylint: disable=too-many-positional-arguments,redefined-outer-name self.uid = uid self.date = datetime.now() self.name = name self.tldr = tldr self.time = time self.description = desc - self.status = 'Pending' + self.status = "Pending" self.active = True class HouseMeeting(db.Model): - __tablename__ = 'house_meetings' + __tablename__ = "house_meetings" id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) active = Column(Boolean, nullable=False) @@ -165,10 +196,10 @@ def __init__(self, hm_date): class MemberHouseMeetingAttendance(db.Model): - __tablename__ = 'member_hm_attendance' + __tablename__ = "member_hm_attendance" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) + meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -180,10 +211,10 @@ def __init__(self, uid, meeting_id, excuse, status): class FreshmanHouseMeetingAttendance(db.Model): - __tablename__ = 'freshman_hm_attendance' + __tablename__ = "freshman_hm_attendance" id = Column(Integer, primary_key=True) - fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) - meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) + fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) + meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -195,11 +226,11 @@ def __init__(self, fid, meeting_id, excuse, status): class CurrentCoops(db.Model): - __tablename__ = 'current_coops' + __tablename__ = "current_coops" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) date_created = Column(Date, nullable=False) - semester = Column(Enum('Fall', 'Spring', name="co_op_enum"), nullable=False) + semester = Column(Enum("Fall", "Spring", name="co_op_enum"), nullable=False) def __init__(self, uid, semester): self.uid = uid @@ -209,7 +240,7 @@ def __init__(self, uid, semester): class OnFloorStatusAssigned(db.Model): - __tablename__ = 'onfloor_datetime' + __tablename__ = "onfloor_datetime" uid = Column(String(32), primary_key=True) onfloor_granted = Column(DateTime, primary_key=True) @@ -219,20 +250,22 @@ def __init__(self, uid, time_granted): class Conditional(db.Model): - __tablename__ = 'conditional' + __tablename__ = "conditional" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) description = Column(String(512), nullable=False) date_created = Column(Date, nullable=False) date_due = Column(Date, nullable=False) active = Column(Boolean, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="conditional_enum"), - nullable=False) - s_evaluation = Column(ForeignKey('spring_evals.id')) - i_evaluation = Column(ForeignKey('freshman_eval_data.id')) - - def __init__(self, uid, description, due, s_eval=None, i_eval=None): # pylint: disable=too-many-positional-arguments + status = Column( + Enum("Pending", "Passed", "Failed", name="conditional_enum"), nullable=False + ) + s_evaluation = Column(ForeignKey("spring_evals.id")) + i_evaluation = Column(ForeignKey("freshman_eval_data.id")) + + def __init__( + self, uid, description, due, s_eval=None, i_eval=None + ): # pylint: disable=too-many-positional-arguments self.uid = uid self.description = description self.date_due = due @@ -244,7 +277,7 @@ def __init__(self, uid, description, due, s_eval=None, i_eval=None): # pylint: d class EvalSettings(db.Model): - __tablename__ = 'settings' + __tablename__ = "settings" id = Column(Integer, primary_key=True) housing_form_active = Column(Boolean) intro_form_active = Column(Boolean) @@ -259,14 +292,14 @@ def __init__(self): class SpringEval(db.Model): - __tablename__ = 'spring_evals' + __tablename__ = "spring_evals" id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) active = Column(Boolean, nullable=False) date_created = Column(Date, nullable=False) - status = Column(Enum('Pending', 'Passed', 'Failed', - name="spring_eval_enum"), - nullable=False) + status = Column( + Enum("Pending", "Passed", "Failed", name="spring_eval_enum"), nullable=False + ) def __init__(self, uid): self.uid = uid @@ -276,13 +309,26 @@ def __init__(self, uid): class InHousingQueue(db.Model): - __tablename__ = 'in_housing_queue' + __tablename__ = "in_housing_queue" uid = Column(String(32), primary_key=True) -http_enum = Enum('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', name='http_enum') + +http_enum = Enum( + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "OPTIONS", + "TRACE", + "PATCH", + name="http_enum", +) + class UserLog(db.Model): - __tablename__ = 'user_log' + __tablename__ = "user_log" id = Column(Integer, primary_key=True) ipaddr = Column(postgresql.INET, nullable=False) timestamp = Column(DateTime, nullable=False) @@ -292,7 +338,9 @@ class UserLog(db.Model): path = Column(String(128), nullable=False) description = Column(String(128), nullable=False) - def __init__(self, ipaddr, user, method, blueprint, path, description): # pylint: disable=too-many-positional-arguments + def __init__( + self, ipaddr, user, method, blueprint, path, description + ): # pylint: disable=too-many-positional-arguments self.ipaddr = ipaddr self.timestamp = datetime.now() self.uid = user @@ -300,3 +348,213 @@ def __init__(self, ipaddr, user, method, blueprint, path, description): # pylint self.blueprint = blueprint self.path = path self.description = description + + +# The required number of honorary member, advisor, and alumni signatures +REQUIRED_MISC_SIGNATURES = 10 + + +class SigCounts: + """ + Utility class for returning counts of signatures broken out by type + """ + + def __init__(self, upper: int, fresh: int, misc: int): + # Base fields + self.upper = upper + self.fresh = fresh + self.misc = misc + + # Capped version of misc so it will never be greater than REQUIRED_MISC_SIGNATURES + self.misc_capped = ( + misc if misc <= REQUIRED_MISC_SIGNATURES else REQUIRED_MISC_SIGNATURES + ) + + # Totals (calculated using misc_capped) + self.member_total = upper + self.misc_capped + self.total = upper + fresh + self.misc_capped + + +class Freshman(db.Model): + __tablename__ = "freshman" + rit_username = cast(str, Column(String(10), primary_key=True)) + name = cast(str, Column(String(64), nullable=False)) + onfloor = cast(bool, Column(Boolean, nullable=False)) + fresh_signatures = cast("FreshSignature", relationship("FreshSignature")) + + # One freshman can have multiple packets if they repeat the intro process + packets = cast("Packet", relationship("Packet", order_by="desc(Packet.id)")) + + @classmethod + def by_username(cls, username: str) -> "Packet": + """ + Helper method to retrieve a freshman by their RIT username + """ + return cls.query.filter_by(rit_username=username).first() + + @classmethod + def get_all(cls) -> list["Packet"]: + """ + Helper method to get all freshmen easily + """ + return cls.query.all() + + +class Packet(db.Model): + __tablename__ = "packet" + id = cast(int, Column(Integer, primary_key=True, autoincrement=True)) + freshman_username = cast(str, Column(ForeignKey("freshman.rit_username"))) + start = cast(datetime, Column(DateTime, nullable=False)) + end = cast(datetime, Column(DateTime, nullable=False)) + + freshman = cast(Freshman, relationship("Freshman", back_populates="packets")) + + # The `lazy='subquery'` kwarg enables eager loading for signatures which makes signature calculations much faster + # See the docs here for details: https://docs.sqlalchemy.org/en/latest/orm/loading_relationships.html + upper_signatures = cast( + "UpperSignature", + relationship( + "UpperSignature", + lazy="subquery", + order_by="UpperSignature.signed.desc(), UpperSignature.updated", + ), + ) + fresh_signatures = cast( + "FreshSignature", + relationship( + "FreshSignature", + lazy="subquery", + order_by="FreshSignature.signed.desc(), FreshSignature.updated", + ), + ) + misc_signatures = cast( + "MiscSignature", + relationship( + "MiscSignature", lazy="subquery", order_by="MiscSignature.updated" + ), + ) + + def is_open(self) -> bool: + return self.start < datetime.now() < self.end + + def signatures_required(self) -> SigCounts: + """ + :return: A SigCounts instance with the fields set to the number of signatures received by this packet + """ + upper = len(self.upper_signatures) + fresh = len(self.fresh_signatures) + + return SigCounts(upper, fresh, REQUIRED_MISC_SIGNATURES) + + def signatures_received(self) -> SigCounts: + """ + :return: A SigCounts instance with the fields set to the number of required signatures for this packet + """ + upper = sum(map(lambda sig: 1 if sig.signed else 0, self.upper_signatures)) + fresh = sum(map(lambda sig: 1 if sig.signed else 0, self.fresh_signatures)) + + return SigCounts(upper, fresh, len(self.misc_signatures)) + + def did_sign(self, username: str, is_csh: bool) -> bool: + """ + :param username: The CSH or RIT username to check for + :param is_csh: Set to True for CSH accounts and False for freshmen + :return: Boolean value for if the given account signed this packet + """ + if is_csh: + for sig in filter( + lambda sig: sig.member == username, + chain(self.upper_signatures, self.misc_signatures), + ): + if isinstance(sig, MiscSignature): + return True + return sig.signed + else: + for sig in filter( + lambda sig: sig.freshman_username == username, self.fresh_signatures + ): + return sig.signed + + # The user must be a misc CSHer that hasn't signed this packet or an off-floor freshmen + return False + + def is_100(self) -> bool: + """ + Checks if this packet has reached 100% + """ + return self.signatures_required().total == self.signatures_received().total + + @classmethod + def open_packets(cls) -> list["Packet"]: + """ + Helper method for fetching all currently open packets + """ + return cls.query.filter( + cls.start < datetime.now(), cls.end > datetime.now() + ).all() + + @classmethod + def by_id(cls, packet_id: int) -> "Packet": + """ + Helper method for fetching 1 packet by its id + """ + return cls.query.filter_by(id=packet_id).first() + + +class UpperSignature(db.Model): + __tablename__ = "signature_upper" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + member = cast(str, Column(String(36), primary_key=True)) + signed = cast(bool, Column(Boolean, default=False, nullable=False)) + eboard = cast(Optional[str], Column(String(12), nullable=True)) + active_rtp = cast(bool, Column(Boolean, default=False, nullable=False)) + three_da = cast(bool, Column(Boolean, default=False, nullable=False)) + webmaster = cast(bool, Column(Boolean, default=False, nullable=False)) + c_m = cast(bool, Column(Boolean, default=False, nullable=False)) + w_m = cast(bool, Column(Boolean, default=False, nullable=False)) + drink_admin = cast(bool, Column(Boolean, default=False, nullable=False)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="upper_signatures")) + + +class FreshSignature(db.Model): + __tablename__ = "signature_fresh" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + freshman_username = cast( + str, Column(ForeignKey("freshman.rit_username"), primary_key=True) + ) + signed = cast(bool, Column(Boolean, default=False, nullable=False)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="fresh_signatures")) + freshman = cast( + Freshman, relationship("Freshman", back_populates="fresh_signatures") + ) + + +class MiscSignature(db.Model): + __tablename__ = "signature_misc" + packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) + member = cast(str, Column(String(36), primary_key=True)) + updated = cast( + datetime, + Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), + ) + + packet = cast(Packet, relationship("Packet", back_populates="misc_signatures")) + + +class NotificationSubscription(db.Model): + __tablename__ = "notification_subscriptions" + member = cast(str, Column(String(36), nullable=True)) + freshman_username = cast( + str, Column(ForeignKey("freshman.rit_username"), nullable=True) + ) + token = cast(str, Column(String(256), primary_key=True, nullable=False)) diff --git a/conditional/templates/active_packets.html b/conditional/templates/active_packets.html new file mode 100644 index 00000000..3fa65640 --- /dev/null +++ b/conditional/templates/active_packets.html @@ -0,0 +1,117 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

Active Packets

+
+ {% if info.is_upper %} +
+ +
+ {% endif %} +
+
+
+ {% if packets|length > 0 %} +
+
+
+
+ + + + + {% if info.is_upper %} + + + + {% endif %} + + + + + {% for packet in packets %} + + + {% if info.is_upper %} + + + + {% endif %} + + + {% endfor %} + +
NameSignaturesSignaturesSignaturesSign
+ {% if info.is_upper %} + + {% endif %} + {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} + {% if info.is_upper %} + + {% endif %} + + {% if packet.signatures_received_result.member_total == packet.signatures_required_result.member_total %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.member_total }} / + {{ packet.signatures_required_result.member_total }} + {% endif %} + + {% if packet.signatures_received_result.fresh == packet.signatures_required_result.fresh %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.fresh }} / + {{ packet.signatures_required_result.fresh }} + {% endif %} + + {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.total }} / + {{ packet.signatures_required_result.total }} + {% endif %} + + {% if not packet.did_sign_result and info.ritdn != packet.freshman_username %} + + {% elif info.ritdn != packet.freshman_username %} + + {% endif %} +
+
+
+
+
+ {% else %} + + {% endif %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + {% if info.realm == "csh" %} + + {% endif %} +{% endblock %} diff --git a/conditional/templates/admin_freshmen.html b/conditional/templates/admin_freshmen.html new file mode 100644 index 00000000..e922375f --- /dev/null +++ b/conditional/templates/admin_freshmen.html @@ -0,0 +1,27 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

All Freshmen

+
+
+ + {% include 'include/admin/sync_freshmen.html' %} +
+
+
+
+ {% include 'include/admin/all_freshmen.html' %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/conditional/templates/admin_packets.html b/conditional/templates/admin_packets.html new file mode 100644 index 00000000..9db36fd2 --- /dev/null +++ b/conditional/templates/admin_packets.html @@ -0,0 +1,30 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

Active Packets

+
+
+ + + {% include 'include/admin/new_packets.html' %} +
+
+
+
+ {% include 'include/admin/open_packets.html' %} +
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/conditional/templates/error.html b/conditional/templates/error.html new file mode 100644 index 00000000..de33536b --- /dev/null +++ b/conditional/templates/error.html @@ -0,0 +1,20 @@ +{% extends 'extend/base.html' %} + +{% block body %} +
+

Oops!

+
+
+
+ I guess this is what you get when you trust a bunch of college kids. +
+

+

{{ e }}
+

+
+ Do us a favor, try again. If you end up here on the second try, shoot us an email. +
+
+
+
+{% endblock %} diff --git a/conditional/templates/extend/base.html b/conditional/templates/extend/base.html new file mode 100644 index 00000000..418e93d6 --- /dev/null +++ b/conditional/templates/extend/base.html @@ -0,0 +1,29 @@ + + + +{% block head %} + {% include "include/head.html" %} +{% endblock %} + + + +{% block nav %} + {% include "include/nav.html" %} +{% endblock %} + +{% block body %} +{% endblock %} + +{% block footer %} + {% include "include/footer.html" %} +{% endblock %} + +{% block includes %} +{% endblock %} + +{% block scripts %} + {% include "include/scripts.html" %} +{% endblock %} + + + diff --git a/conditional/templates/extend/email.html b/conditional/templates/extend/email.html new file mode 100644 index 00000000..20fb7de7 --- /dev/null +++ b/conditional/templates/extend/email.html @@ -0,0 +1,17 @@ + + + +{% block head %} + + CSH Packet + + +{% endblock %} + + +{% block body %} +{% endblock %} + + diff --git a/conditional/templates/include/admin/all_freshmen.html b/conditional/templates/include/admin/all_freshmen.html new file mode 100644 index 00000000..a3e79e3b --- /dev/null +++ b/conditional/templates/include/admin/all_freshmen.html @@ -0,0 +1,33 @@ +
+
+
+
+ + + + + + + + + {% for freshman in all_freshmen %} + {% set freshman_name = freshman.name + ' (' + freshman.rit_username + ')' %} + + + + + {% endfor %} + +
NameOn-Floor
+ {{ freshman_name }} {{ freshman_name }} + + {{ freshman.onfloor }} +
+
+
+
+
diff --git a/conditional/templates/include/admin/new_packets.html b/conditional/templates/include/admin/new_packets.html new file mode 100644 index 00000000..c6dd3075 --- /dev/null +++ b/conditional/templates/include/admin/new_packets.html @@ -0,0 +1,23 @@ + diff --git a/conditional/templates/include/admin/open_packets.html b/conditional/templates/include/admin/open_packets.html new file mode 100644 index 00000000..2db5d4b8 --- /dev/null +++ b/conditional/templates/include/admin/open_packets.html @@ -0,0 +1,39 @@ +
+
+
+
+ + + + + + + + + {% for packet in open_packets %} + + + + + {% endfor %} + +
NameSignatures
+ + {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} + + + {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} + 💯 {# 100% emoji #} + {% else %} + {{ packet.signatures_received_result.total }} / + {{ packet.signatures_required_result.total }} + {% endif %} +
+
+
+
+
diff --git a/conditional/templates/include/admin/sync_freshmen.html b/conditional/templates/include/admin/sync_freshmen.html new file mode 100644 index 00000000..6f9b4806 --- /dev/null +++ b/conditional/templates/include/admin/sync_freshmen.html @@ -0,0 +1,22 @@ + diff --git a/conditional/templates/include/footer.html b/conditional/templates/include/footer.html new file mode 100644 index 00000000..b1e9bdcf --- /dev/null +++ b/conditional/templates/include/footer.html @@ -0,0 +1,6 @@ + diff --git a/conditional/templates/include/head.html b/conditional/templates/include/head.html new file mode 100644 index 00000000..20d0f420 --- /dev/null +++ b/conditional/templates/include/head.html @@ -0,0 +1,98 @@ + + + + + + + CSH Packet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conditional/templates/include/nav.html b/conditional/templates/include/nav.html new file mode 100644 index 00000000..6573b8bc --- /dev/null +++ b/conditional/templates/include/nav.html @@ -0,0 +1,67 @@ + diff --git a/conditional/templates/include/scripts.html b/conditional/templates/include/scripts.html new file mode 100644 index 00000000..af0b7632 --- /dev/null +++ b/conditional/templates/include/scripts.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + +{% if info.realm == "intro" %} + +{% endif %} diff --git a/conditional/templates/mail/packet_start.html b/conditional/templates/mail/packet_start.html new file mode 100644 index 00000000..722e5768 --- /dev/null +++ b/conditional/templates/mail/packet_start.html @@ -0,0 +1,15 @@ +{% extends "extend/email.html" %} + +{% block body %} +
+

Hello {{ packet.freshman.name }},

+

Welcome to Computer Science House!

+

Soon you'll starting the introductory process for CSH, and the first part of that is Packet.

+

Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }}

+

You can view your packet at {{ config["PACKET_INTRO"] }} with + the credentials you should have been sent.

+

If you don't know your credentials, reach out to an RTP

+

If you have any questions about Packet or the introductory process, email evals@csh.rit.edu

+

If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu

+
+{% endblock %} diff --git a/conditional/templates/mail/packet_start.txt b/conditional/templates/mail/packet_start.txt new file mode 100644 index 00000000..162ec147 --- /dev/null +++ b/conditional/templates/mail/packet_start.txt @@ -0,0 +1,14 @@ +Hello {{ packet.freshman.name }}, + +Welcome to Computer Science House! + +Soon you'll starting the introductory process for CSH, and the first part of that is Packet. + +Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }} + +You can view your packet at {{ config["PROTOCOL"] + config["PACKET_INTRO"] }} with the credentials you should have been sent. +If you don't know your credentials, reach out to an RTP + +If you have any questions about Packet or the introductory process, email evals@csh.rit.edu + +If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu diff --git a/conditional/templates/mail/report.html b/conditional/templates/mail/report.html new file mode 100644 index 00000000..dbfedcbc --- /dev/null +++ b/conditional/templates/mail/report.html @@ -0,0 +1,10 @@ +{% extends "extend/email.html" %} + +{% block body %} +
+

Hello,

+

{{ reporter }} just made a report against {{ person }}

+

The report reads:

+
{{ report }}
+
+{% endblock %} diff --git a/conditional/templates/mail/report.txt b/conditional/templates/mail/report.txt new file mode 100644 index 00000000..7a5576c7 --- /dev/null +++ b/conditional/templates/mail/report.txt @@ -0,0 +1,7 @@ +Hello, + +{{ reporter }} just made a report against {{ person }} + +The report reads: + +{{ report }} diff --git a/conditional/templates/not_found.html b/conditional/templates/not_found.html new file mode 100644 index 00000000..e7cd8765 --- /dev/null +++ b/conditional/templates/not_found.html @@ -0,0 +1,17 @@ +{% extends 'extend/base.html' %} + +{% block body %} +
+

Page Not Found

+
+
+

+ Not sure what you're looking for, but it's not here. +

+
+ Do us a favor, check your spelling. If you can't find what you're looking for, shoot us an email. +
+
+
+
+{% endblock %} diff --git a/conditional/templates/packet.html b/conditional/templates/packet.html new file mode 100644 index 00000000..5445f101 --- /dev/null +++ b/conditional/templates/packet.html @@ -0,0 +1,194 @@ +{% extends "extend/base.html" %} + +{% set packet_end = packet.end.strftime('%m/%d/%Y, %H:%M %Z') %} + +{% block body %} +
+
+
+
+

{{ get_rit_name(packet.freshman_username) }}

+
+
+ {% if not did_sign and info.ritdn != packet.freshman_username %} + + {% elif did_sign %} + + {% endif %} + {% if info.realm == "csh" and info.is_upper %} + + Graphs + + {% endif %} +
+
+ {% if info.is_upper or packet.freshman_username == info.ritdn %} +
+
+
Signatures: {{ received.total }}/{{ required.total }} +
+
+
+
Ends: {{ packet_end }}
+
+
+
+
+
+ {% set total_score = received.total / required.total * 100 %} +
Total Score - {{ '%0.2f' % total_score }}%
+
+
+
+ {% set upper_score = received.member_total / required.member_total * 100 %} +
Upperclassmen Score - {{ '%0.2f' % upper_score }}%
+
+
+
+
+
+
+
+
+
+
+
+ Active Upperclassmen Signatures + {{ received.upper }}/{{ required.upper }} +
+
+
+ + + {% for sig in upper %} + + + + + {% endfor %} + +
+ {% if info.realm == "csh" %} + + {% endif %} + {{ sig.member }} + {{ get_csh_name(sig.member) }} + {% if info.realm == "csh" %} + + {% endif %} + {% for role in get_roles(sig) %} + {{ get_roles(sig)[role] }} + {% endfor %} + + {% if sig.signed %} + + {% else %} + + {% endif %} +
+
+
+
+ {% endif %} + {% if info.is_upper or packet.freshman_username == info.ritdn %} +
+
+ Freshmen Signatures + {% if info.is_upper or packet.freshman_username == info.ritdn %} + {{ received.fresh }}/{{ required.fresh }} + {% else %} + Signed + {% endif %} +
+
+
+ + + {% for sig in (packet.fresh_signatures if info.is_upper or packet.freshman_username == info.ritdn else fresh_sig) %} + + + + + {% endfor %} + +
+ {{ sig.freshman_username }} + {{ get_rit_name(sig.freshman_username) }} + + {% if sig.signed %} + + {% else %} + + {% endif %} +
+
+
+
+ {% endif %} + {% if info.is_upper or packet.freshman_username == info.ritdn %} +
+
+ Alumni & Advisor Signatures + {{ received.misc }}/{{ required.misc }} +
+
+
+ + + {% for sig in packet.misc_signatures %} + + + + + + {% endfor %} + +
+ {{ loop.index }}. + + {% if info.realm == "csh" %} + + {% endif %} + {{ sig.member }} + {{ get_csh_name(sig.member) }} + {% if info.realm == "csh" %} + + {% endif %} + + {% if loop.index <= 10 %} + + {% else %} +

Extra!

+ {% endif %} +
+
+
+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/conditional/templates/packet_stats.html b/conditional/templates/packet_stats.html new file mode 100644 index 00000000..77423975 --- /dev/null +++ b/conditional/templates/packet_stats.html @@ -0,0 +1,84 @@ +{% extends "extend/base.html" %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
Cumulative Signatures Over Time for + + {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} + +
+ + + + +
+ + +
+
+
+{% endblock %} diff --git a/conditional/templates/upperclassman.html b/conditional/templates/upperclassman.html new file mode 100644 index 00000000..07268c5b --- /dev/null +++ b/conditional/templates/upperclassman.html @@ -0,0 +1,59 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+
+

+ {{ member }} + {{ get_csh_name(member) }} +

+
+
+
+
+
+
+
+ {{ signatures }}/{{ open_packets|length }} +
+
+
+ + + {% for packet in open_packets %} + {# Using inline style because of how themeswitcher loads the css theme :( #} + + + + + {% endfor %} + +
+ + {{ get_rit_name(packet.freshman_username) }} + {{ get_rit_name(packet.freshman_username) }} + + + {% if packet.did_sign_result %} + + {% else %} + + {% endif %} +
+
+
+
+
+
+
+{% endblock %} diff --git a/conditional/templates/upperclassmen_totals.html b/conditional/templates/upperclassmen_totals.html new file mode 100644 index 00000000..2b56955c --- /dev/null +++ b/conditional/templates/upperclassmen_totals.html @@ -0,0 +1,90 @@ +{% extends "extend/base.html" %} + +{% block body %} +
+
+
+

Upperclassmen Signatures

+
+
+ +
+ {% if num_open_packets > 0 %} +
+
+
+
+ + + + + + + + + {% for member, signed_count in upperclassmen %} + + + + + {% endfor %} + +
Active MemberSignatures
+ + {{ member }} {{ get_csh_name(member) }} + + + {{ signed_count }}/{{ num_open_packets }} +
+
+
+
+
+
+
+ + + + + + + + + {% for member, signed_count in misc %} + + + + + {% endfor %} + +
Alumni or AdvisorSignatures
+ + {{ member }} {{ get_csh_name(member) }} + + + {{ signed_count }}/{{ num_open_packets }} +
+
+
+
+
+ {% else %} + + {% endif %} +
+
+{% endblock %} diff --git a/conditional/util/auth.py b/conditional/util/auth.py index 11cc826b..1714014c 100644 --- a/conditional/util/auth.py +++ b/conditional/util/auth.py @@ -1,16 +1,24 @@ from functools import wraps -from flask import request, session +from flask import request, session, redirect -from conditional.util.ldap import ldap_is_active, ldap_is_alumni, \ - ldap_is_eboard, ldap_is_eval_director, \ - ldap_is_financial_director, ldap_get_member, ldap_is_current_student +from conditional import auth +from conditional.blueprints.packet import packet_bp +from conditional.util.ldap import ( + ldap_is_active, + ldap_is_alumni, + ldap_is_eboard, + ldap_is_eval_director, + ldap_is_financial_director, + ldap_get_member, + ldap_is_current_student, +) def webauth_request(func): @wraps(func) def wrapped_func(*args, **kwargs): - user_name = request.headers.get('x-webauth-user') + user_name = request.headers.get("x-webauth-user") account = ldap_get_member(user_name) is_active = ldap_is_active(account) is_alumni = ldap_is_alumni(account) @@ -18,12 +26,18 @@ def wrapped_func(*args, **kwargs): is_financial = ldap_is_financial_director(account) is_eval = ldap_is_eval_director(account) - return func({"user_name": user_name, - "is_active": is_active, - "is_alumni": is_alumni, - "is_eboard": is_eboard, - "is_financial": is_financial, - "is_eval": is_eval}, *args, **kwargs) + return func( + { + "user_name": user_name, + "is_active": is_active, + "is_alumni": is_alumni, + "is_eboard": is_eboard, + "is_financial": is_financial, + "is_eval": is_eval, + }, + *args, + **kwargs, + ) return wrapped_func @@ -36,12 +50,69 @@ def wrapped_function(*args, **kwargs): current_student = ldap_is_current_student(account) user_dict = { - 'username': username, - 'account': account, - 'student': current_student + "username": username, + "account": account, + "student": current_student, } kwargs["user_dict"] = user_dict return func(*args, **kwargs) return wrapped_function + + +def needs_auth(func): + @wraps(func) + def wrapped_function(*args, **kwargs): + match session.get("provider"): + case "csh": + csh_auth() + oidc_info = session["userinfo"] + username = oidc_info["preferred_username"] + case "frosh": + frosh_auth() + oidc_info = session["userinfo"] + username = oidc_info["preferred_username"] + case _: + return redirect("/packet", code=301) + + username = str(session["userinfo"].get("preferred_username", "")) + ritdn = str(session["userinfo"].get("ritdn", "")) + account = ldap_get_member(username) + current_student = ldap_is_current_student(account) + + user_dict = { + "username": username, + "account": account, + "student": current_student, + "ritdn": ritdn, + } + + kwargs["user_dict"] = user_dict + return func(*args, **kwargs) + + return wrapped_function + + +@auth.oidc_auth("default") +def csh_auth(): + return + + +@auth.oidc_auth("frosh") +def frosh_auth(): + return + + +@packet_bp.route("/auth/csh") +@auth.oidc_auth("csh") +def csh_login(): + session["provider"] = "csh" + return redirect("/packet", code=301) + + +@packet_bp.route("/auth/frosh") +@auth.oidc_auth("frosh") +def frosh_login(): + session["provider"] = "frosh" + return redirect("/packet", code=301) diff --git a/conditional/util/ldap.py b/conditional/util/ldap.py index 98cb8531..57f81db6 100644 --- a/conditional/util/ldap.py +++ b/conditional/util/ldap.py @@ -1,3 +1,6 @@ +from datetime import date +from typing import Optional + from conditional import ldap from conditional.util.cache import service_cache @@ -58,6 +61,43 @@ def ldap_get_current_students(): return _ldap_get_group_members("current_student") +@service_cache(maxsize=1024) +def ldap_get_rtps(): + return _ldap_get_group_members("active_rtp") + + +@service_cache(maxsize=1024) +def ldap_get_3das(): + return _ldap_get_group_members("3da") + + +@service_cache(maxsize=1024) +def ldap_get_webmasters(): + return _ldap_get_group_members("webmaster") + + +@service_cache(maxsize=1024) +def ldap_get_cms(): + return _ldap_get_group_members("constitutional_maintainers") + + +@service_cache(maxsize=1024) +def ldap_get_wms(): + return _ldap_get_group_members("wiki_maintainers") + + +@service_cache(maxsize=1024) +def ldap_get_drink_admins(): + return _ldap_get_group_members("drink") + + +@service_cache(maxsize=1024) +def ldap_is_on_coop(account): + if date.today().month > 6: + return _ldap_is_member_of_group(account, "fall_coop") + return _ldap_is_member_of_group(account, "spring_coop") + + @service_cache(maxsize=128) def ldap_get_roomnumber(account): try: @@ -68,53 +108,85 @@ def ldap_get_roomnumber(account): @service_cache(maxsize=128) def ldap_is_active(account): - return _ldap_is_member_of_group(account, 'active') + return _ldap_is_member_of_group(account, "active") @service_cache(maxsize=128) def ldap_is_bad_standing(account): - return _ldap_is_member_of_group(account, 'bad_standing') + return _ldap_is_member_of_group(account, "bad_standing") @service_cache(maxsize=128) def ldap_is_alumni(account): # If the user is not active, they are an alumni. - return not _ldap_is_member_of_group(account, 'active') + return not _ldap_is_member_of_group(account, "active") @service_cache(maxsize=128) def ldap_is_eboard(account): - return _ldap_is_member_of_group(account, 'eboard') + return _ldap_is_member_of_group(account, "eboard") @service_cache(maxsize=128) def ldap_is_rtp(account): - return _ldap_is_member_of_group(account, 'rtp') + return _ldap_is_member_of_group(account, "rtp") @service_cache(maxsize=128) def ldap_is_intromember(account): - return _ldap_is_member_of_group(account, 'intromembers') + return _ldap_is_member_of_group(account, "intromembers") @service_cache(maxsize=128) def ldap_is_onfloor(account): - return _ldap_is_member_of_group(account, 'onfloor') + return _ldap_is_member_of_group(account, "onfloor") @service_cache(maxsize=128) def ldap_is_financial_director(account): - return _ldap_is_member_of_directorship(account, 'Financial') + return _ldap_is_member_of_directorship(account, "Financial") @service_cache(maxsize=128) def ldap_is_eval_director(account): - return _ldap_is_member_of_directorship(account, 'Evaluations') + return _ldap_is_member_of_directorship(account, "Evaluations") @service_cache(maxsize=256) def ldap_is_current_student(account): - return _ldap_is_member_of_group(account, 'current_student') + return _ldap_is_member_of_group(account, "current_student") + + +def ldap_get_eboard_role(account) -> Optional[str]: + """ + :param member: A CSHMember instance + :return: A String or None + """ + + return_val = None + + if _ldap_is_member_of_group(account, "eboard-chairman"): + return_val = "Chairperson" + elif _ldap_is_member_of_group(account, "eboard-evaluations"): + return_val = "Evals" + elif _ldap_is_member_of_group(account, "eboard-financial"): + return_val = "Financial" + elif _ldap_is_member_of_group(account, "eboard-history"): + return_val = "History" + elif _ldap_is_member_of_group(account, "eboard-imps"): + return_val = "Imps" + elif _ldap_is_member_of_group(account, "eboard-opcomm"): + return_val = "OpComm" + elif _ldap_is_member_of_group(account, "eboard-research"): + return_val = "R&D" + elif _ldap_is_member_of_group(account, "eboard-social"): + return_val = "Social" + elif _ldap_is_member_of_group(account, "eboard-pr"): + return_val = "PR" + elif _ldap_is_member_of_group(account, "eboard-secretary"): + return_val = "Secretary" + + return return_val def ldap_set_housingpoints(account, housing_points): @@ -132,52 +204,52 @@ def ldap_set_roomnumber(account, room_number): def ldap_set_active(account): - _ldap_add_member_to_group(account, 'active') + _ldap_add_member_to_group(account, "active") ldap_get_active_members.cache_clear() ldap_get_member.cache_clear() def ldap_set_inactive(account): - _ldap_remove_member_from_group(account, 'active') + _ldap_remove_member_from_group(account, "active") ldap_get_active_members.cache_clear() ldap_get_member.cache_clear() def ldap_set_intro_member(account): - _ldap_add_member_to_group(account, 'intromembers') + _ldap_add_member_to_group(account, "intromembers") ldap_get_intro_members().cache_clear() ldap_get_member.cache_clear() def ldap_set_not_intro_member(account): - _ldap_remove_member_from_group(account, 'intromembers') + _ldap_remove_member_from_group(account, "intromembers") ldap_get_intro_members().cache_clear() ldap_get_member.cache_clear() def ldap_set_current_student(account): - _ldap_add_member_to_group(account, 'current_student') + _ldap_add_member_to_group(account, "current_student") ldap_get_current_students.cache_clear() ldap_get_member.cache_clear() def ldap_set_non_current_student(account): - _ldap_remove_member_from_group(account, 'current_student') + _ldap_remove_member_from_group(account, "current_student") ldap_get_current_students.cache_clear() ldap_get_member.cache_clear() def ldap_set_failed(account): - _ldap_add_member_to_group(account, 'failed') + _ldap_add_member_to_group(account, "failed") ldap_get_member.cache_clear() def ldap_set_bad_standing(account): - _ldap_add_member_to_group(account, 'bad_standing') + _ldap_add_member_to_group(account, "bad_standing") ldap_get_member.cache_clear() def ldap_set_onfloor(account): - _ldap_add_member_to_group(account, 'onfloor') + _ldap_add_member_to_group(account, "onfloor") ldap_get_onfloor_members.cache_clear() ldap_get_member.cache_clear() diff --git a/conditional/util/mail.py b/conditional/util/mail.py new file mode 100644 index 00000000..13dc9c83 --- /dev/null +++ b/conditional/util/mail.py @@ -0,0 +1,53 @@ +from typing import TypedDict + +from flask import render_template +from flask_mail import Mail, Message + +from conditional import app +from conditional.models.models import Packet + +mail = Mail(app) + + +class ReportForm(TypedDict): + person: str + report: str + + +def send_start_packet_mail(packet: Packet) -> None: + if app.config["MAIL_PROD"]: + recipients = ["<" + packet.freshman.rit_username + "@rit.edu>"] + msg = Message( + subject="CSH Packet Starts " + packet.start.strftime("%A, %B %-d"), + sender=app.config.get("MAIL_USERNAME"), + recipients=recipients, + ) + + template = "mail/packet_start" + msg.body = render_template(template + ".txt", packet=packet) + msg.html = render_template(template + ".html", packet=packet) + app.logger.info(f"Sending mail to {recipients[0]}") + mail.send(msg) + + +def send_report_mail(form_results: ReportForm, reporter: str) -> None: + if app.config["MAIL_PROD"]: + recipients = [""] + msg = Message( + subject="Packet Report", + sender=app.config.get("MAIL_USERNAME"), + recipients=recipients, + ) + + person = form_results["person"] + report = form_results["report"] + + template = "mail/report" + msg.body = render_template( + template + ".txt", person=person, report=report, reporter=reporter + ) + msg.html = render_template( + template + ".html", person=person, report=report, reporter=reporter + ) + app.logger.info(f"Sending mail to {recipients[0]}") + mail.send(msg) diff --git a/conditional/util/packet.py b/conditional/util/packet.py new file mode 100644 index 00000000..9ff1e53f --- /dev/null +++ b/conditional/util/packet.py @@ -0,0 +1,197 @@ +from datetime import datetime, date, time, timedelta +from typing import Any, cast + +import structlog + +from conditional import db, logger +from conditional.models.models import ( + Freshman, + MiscSignature, + Packet, + FreshSignature, + UpperSignature, +) +from conditional.util.ldap import ( + ldap_get_eboard_role, + ldap_get_rtps, + ldap_get_3das, + ldap_get_webmasters, + ldap_get_cms, + ldap_get_wms, + ldap_get_drink_admins, + ldap_get_active_members, + ldap_is_intromember, + ldap_is_on_coop, +) +from conditional.util.mail import send_start_packet_mail + +logger = structlog.get_logger() + + +def sync_freshman_list(freshmen_list: dict) -> None: + freshmen_in_db = { + freshman.rit_username: freshman for freshman in Freshman.query.all() + } + + for list_freshman in freshmen_list.values(): + if list_freshman.rit_username not in freshmen_in_db: + # This is a new freshman so add them to the DB + freshmen_in_db[list_freshman.rit_username] = Freshman( + rit_username=list_freshman.rit_username, + name=list_freshman.name, + onfloor=list_freshman.onfloor, + ) + db.session.add(freshmen_in_db[list_freshman.rit_username]) + else: + # This freshman is already in the DB so just update them + freshmen_in_db[list_freshman.rit_username].onfloor = list_freshman.onfloor + freshmen_in_db[list_freshman.rit_username].name = list_freshman.name + + # Update all freshmen entries that represent people who are no longer freshmen + for freshman in filter( + lambda freshman: freshman.rit_username not in freshmen_list, + freshmen_in_db.values(), + ): + freshman.onfloor = False + + # Update the freshmen signatures of each open or future packet + for packet in Packet.query.filter(Packet.end > datetime.now()).all(): + # pylint: disable=cell-var-from-loop + current_fresh_sigs = set( + map(lambda fresh_sig: fresh_sig.freshman_username, packet.fresh_signatures) + ) + for list_freshman in filter( + lambda list_freshman: list_freshman.rit_username not in current_fresh_sigs + and list_freshman.rit_username != packet.freshman_username, + freshmen_list.values(), + ): + db.session.add( + FreshSignature( + packet=packet, freshman=freshmen_in_db[list_freshman.rit_username] + ) + ) + + db.session.commit() + + +def create_new_packets(base_date: date, freshmen_list: dict) -> None: + packet_start_time = time(hour=19) + packet_end_time = time(hour=21) + start = datetime.combine(base_date, packet_start_time) + end = datetime.combine(base_date, packet_end_time) + timedelta(days=14) + + logger.info("Fetching data from LDAP...") + all_upper = list( + filter( + lambda member: not ldap_is_intromember(member) + and not ldap_is_on_coop(member), + ldap_get_active_members(), + ) + ) + + rtp = [member.uid for member in ldap_get_rtps()] + three_da = [member.uid for member in ldap_get_3das()] + webmaster = [member.uid for member in ldap_get_webmasters()] + c_m = [member.uid for member in ldap_get_cms()] + w_m = [member.uid for member in ldap_get_wms()] + drink = [member.uid for member in ldap_get_drink_admins()] + + # Create the new packets and the signatures for each freshman in the given CSV + logger.info("Creating DB entries and sending emails...") + for freshman in Freshman.query.filter( + cast(Any, Freshman.rit_username).in_(freshmen_list) + ).all(): + packet = Packet(freshman=freshman, start=start, end=end) + db.session.add(packet) + send_start_packet_mail(packet) + + for member in all_upper: + sig = UpperSignature(packet=packet, member=member.uid) + sig.eboard = ldap_get_eboard_role(member) + sig.active_rtp = member.uid in rtp + sig.three_da = member.uid in three_da + sig.webmaster = member.uid in webmaster + sig.c_m = member.uid in c_m + sig.w_m = member.uid in w_m + sig.drink_admin = member.uid in drink + db.session.add(sig) + + for frosh in Freshman.query.filter( + Freshman.rit_username != freshman.rit_username + ).all(): + db.session.add(FreshSignature(packet=packet, freshman=frosh)) + + db.session.commit() + + +def sync_with_ldap() -> None: + logger.info("Fetching data from LDAP...") + all_upper = { + member.uid: member + for member in filter( + lambda member: not ldap_is_intromember(member) + and not ldap_is_on_coop(member), + ldap_get_active_members(), + ) + } + + rtp = ldap_get_rtps() + three_da = ldap_get_3das() + webmaster = ldap_get_webmasters() + c_m = ldap_get_cms() + w_m = ldap_get_wms() + drink = ldap_get_drink_admins() + + logger.info("Applying updates to the DB...") + for packet in Packet.query.filter(Packet.end > datetime.now()).all(): + # Update the role state of all UpperSignatures + for sig in filter(lambda sig: sig.member in all_upper, packet.upper_signatures): + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.w_m = sig.member in w_m + sig.drink_admin = sig.member in drink + + # Migrate UpperSignatures that are from accounts that are not active anymore + for sig in filter( + lambda sig: sig.member not in all_upper, packet.upper_signatures + ): + UpperSignature.query.filter_by( + packet_id=packet.id, member=sig.member + ).delete() + if sig.signed: + sig = MiscSignature(packet=packet, member=sig.member) + db.session.add(sig) + + # Migrate MiscSignatures that are from accounts that are now active members + for sig in filter(lambda sig: sig.member in all_upper, packet.misc_signatures): + MiscSignature.query.filter_by( + packet_id=packet.id, member=sig.member + ).delete() + sig = UpperSignature(packet=packet, member=sig.member, signed=True) + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.w_m = sig.member in w_m + sig.drink_admin = sig.member in drink + db.session.add(sig) + + # Create UpperSignatures for any new active members + # pylint: disable=cell-var-from-loop + upper_sigs = set(map(lambda sig: sig.member, packet.upper_signatures)) + for member in filter(lambda member: member not in upper_sigs, all_upper): + sig = UpperSignature(packet=packet, member=member) + sig.eboard = ldap_get_eboard_role(all_upper[sig.member]) + sig.active_rtp = sig.member in rtp + sig.three_da = sig.member in three_da + sig.webmaster = sig.member in webmaster + sig.c_m = sig.member in c_m + sig.w_m = sig.member in w_m + sig.drink_admin = sig.member in drink + db.session.add(sig) + + db.session.commit() diff --git a/conditional/util/stats.py b/conditional/util/stats.py new file mode 100644 index 00000000..174923bc --- /dev/null +++ b/conditional/util/stats.py @@ -0,0 +1,161 @@ +from datetime import date as dateType, timedelta +from typing import TypedDict, Union, cast, Callable + +from conditional.models.models import Packet, MiscSignature, UpperSignature + + +# Types +class Freshman(TypedDict): + name: str + rit_username: str + + +class WhoSigned(TypedDict): + upper: list[str] + misc: list[str] + fresh: list[str] + + +class PacketStats(TypedDict): + packet_id: int + freshman: Freshman + dates: dict[str, dict[str, list[str]]] + + +class SimplePacket(TypedDict): + id: int + freshman_username: str + + +class SigDict(TypedDict): + date: dateType + packet: SimplePacket + + +Stats = dict[dateType, list[str]] + + +def packet_stats(packet_id: int) -> PacketStats: + """ + Gather statistics for a packet in the form of number of signatures per day + + Return format: { + packet_id, + freshman: { + name, + rit_username, + }, + dates: { + : { + upper: [ uid ], + misc: [ uid ], + fresh: [ freshman_username ], + }, + }, + } + """ + packet = Packet.by_id(packet_id) + + dates = [ + packet.start.date() + timedelta(days=x) + for x in range(0, (packet.end - packet.start).days + 1) + ] + + print(dates) + + upper_stats: Stats = {date: [] for date in dates} + for uid, date in map( + lambda sig: (sig.member, sig.updated), + filter(lambda sig: sig.signed, packet.upper_signatures), + ): + upper_stats[date.date()].append(uid) + + fresh_stats: Stats = {date: [] for date in dates} + for username, date in map( + lambda sig: (sig.freshman_username, sig.updated), + filter(lambda sig: sig.signed, packet.fresh_signatures), + ): + fresh_stats[date.date()].append(username) + + misc_stats: Stats = {date: [] for date in dates} + for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures): + misc_stats[date.date()].append(uid) + + total_stats = {} + for date in dates: + total_stats[date.isoformat()] = { + "upper": upper_stats[date], + "fresh": fresh_stats[date], + "misc": misc_stats[date], + } + + return { + "packet_id": packet_id, + "freshman": { + "name": packet.freshman.name, + "rit_username": packet.freshman.rit_username, + }, + "dates": total_stats, + } + + +def sig2dict(sig: Union[UpperSignature, MiscSignature]) -> SigDict: + """ + A utility function for upperclassman stats. + Converts an UpperSignature to a dictionary with the date and the packet. + """ + packet = Packet.by_id(sig.packet_id) + return { + "date": sig.updated.date(), + "packet": { + "id": packet.id, + "freshman_username": packet.freshman_username, + }, + } + + +class UpperStats(TypedDict): + member: str + signatures: dict[str, list[SimplePacket]] + + +def upperclassman_stats(uid: str) -> UpperStats: + """ + Gather statistics for an upperclassman's signature habits + + Return format: { + member: , + signautes: { + : [{ + id: , + freshman_username, + }], + }, + } + """ + + sigs = ( + UpperSignature.query.filter( + UpperSignature.signed, UpperSignature.member == uid + ).all() + + MiscSignature.query.filter(MiscSignature.member == uid).all() + ) + + sig_dicts = list(map(sig2dict, sigs)) + + dates = set(map(lambda sd: sd["date"], sig_dicts)) + + return { + "member": uid, + "signatures": { + date.isoformat(): list( + map( + lambda sd: sd["packet"], + filter( + cast(Callable, lambda sig, d=date: sig["date"] == d), sig_dicts + ), + ) + ) + for date in dates + }, + } diff --git a/requirements.in b/requirements.in index 49cd221c..6c641c62 100644 --- a/requirements.in +++ b/requirements.in @@ -7,6 +7,7 @@ click~=8.1.8 csh_ldap>=2.3.1 ddtrace~=3.2.1 Flask~=3.1.0 +Flask-Mail~=0.10.0 Flask-Migrate~=2.1.1 Flask-Gzip~=0.2 Flask-pyoidc~=3.14.3 diff --git a/requirements.txt b/requirements.txt index 44225432..a2b29332 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ blinker==1.9.0 # via # -r requirements.in # flask + # flask-mail # sentry-sdk boto3==1.35.13 # via -r requirements.in @@ -63,12 +64,15 @@ flask==3.1.0 # via # -r requirements.in # flask-gzip + # flask-mail # flask-migrate # flask-pyoidc # flask-sqlalchemy # sentry-sdk flask-gzip==0.2 # via -r requirements.in +flask-mail==0.10.0 + # via -r requirements.in flask-migrate==2.1.1 # via -r requirements.in flask-pyoidc==3.14.3 From baf42dfb8f12d549671948dd5e58edc8107d6947 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 9 Sep 2025 19:06:28 -0400 Subject: [PATCH 54/76] packet glue but again (#397) --- config.env.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/config.env.py b/config.env.py index 29c0689b..7a94c73a 100644 --- a/config.env.py +++ b/config.env.py @@ -10,7 +10,7 @@ # Flask config DEBUG = env.get("CONDITIONAL_DEBUG", "false").lower() == "true" HOST_NAME = env.get("CONDITIONAL_HOST_NAME", "conditional.csh.rit.edu") -SERVER_NAME = env.get('CONDITIONAL_SERVER_NAME', 'conditional.csh.rit.edu') +SERVER_NAME = env.get("CONDITIONAL_SERVER_NAME", "conditional.csh.rit.edu") APP_NAME = "conditional" IP = env.get("CONDITIONAL_IP", "0.0.0.0") PORT = env.get("CONDITIONAL_PORT", 6969) @@ -22,7 +22,9 @@ # LDAP config LDAP_RO = env.get("CONDITIONAL_LDAP_RO", "true").lower() == "true" -LDAP_BIND_DN = env.get("CONDITIONAL_LDAP_BIND_DN", "cn=conditional,ou=Apps,dc=csh,dc=rit,dc=edu") +LDAP_BIND_DN = env.get( + "CONDITIONAL_LDAP_BIND_DN", "cn=conditional,ou=Apps,dc=csh,dc=rit,dc=edu" +) LDAP_BIND_PW = env.get("CONDITIONAL_LDAP_BIND_PW", "") # S3 information @@ -34,21 +36,28 @@ # SENTRY_ENV is 'local-development' SENTRY_DSN = env.get("CONDITIONAL_SENTRY_DSN", "") SENTRY_CONFIG = { - 'dsn': env.get("CONDITIONAL_SENTRY_LEGACY_DSN", ""), - 'release': VERSION, + "dsn": env.get("CONDITIONAL_SENTRY_LEGACY_DSN", ""), + "release": VERSION, } SENTRY_ENV = env.get("CONDITIONAL_SENTRY_ENV", "local-development") # OIDC Config -OIDC_ISSUER = env.get("CONDITIONAL_OIDC_ISSUER", "https://sso.csh.rit.edu/auth/realms/csh") +OIDC_ISSUER = env.get( + "CONDITIONAL_OIDC_ISSUER", "https://sso.csh.rit.edu/auth/realms/csh" +) OIDC_CLIENT_CONFIG = { - 'client_id': env.get("CONDITIONAL_OIDC_CLIENT_ID", "conditional"), - 'client_secret': env.get("CONDITIONAL_OIDC_CLIENT_SECRET", ""), - 'post_logout_redirect_uris': [env.get("CONDITIONAL_OIDC_CLIENT_LOGOUT", "http://0.0.0.0:6969/logout")] + "client_id": env.get("CONDITIONAL_OIDC_CLIENT_ID", "conditional"), + "client_secret": env.get("CONDITIONAL_OIDC_CLIENT_SECRET", ""), + "post_logout_redirect_uris": [ + env.get("CONDITIONAL_OIDC_CLIENT_LOGOUT", "http://0.0.0.0:6969/logout") + ], } +FROSH_OIDC_ISSUER = env.get( + "CONDITIONAL_FROSH_OIDC_ISSUER", "https://sso.csh.rit.edu/auth/realms/csh" +) # Openshift secret -SECRET_KEY = env.get("CONDITIONAL_SECRET_KEY", default=''.join(secrets.token_hex(16))) +SECRET_KEY = env.get("CONDITIONAL_SECRET_KEY", default="".join(secrets.token_hex(16))) # General config DUES_PER_SEMESTER = env.get("CONDITIONAL_DUES_PER_SEMESTER", 80) From e12ddb8939ffb7defcce99ef88c9a1e0d80b0b29 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 9 Sep 2025 19:13:58 -0400 Subject: [PATCH 55/76] packet glue but even more --- conditional/util/auth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/conditional/util/auth.py b/conditional/util/auth.py index 1714014c..e3921b67 100644 --- a/conditional/util/auth.py +++ b/conditional/util/auth.py @@ -2,8 +2,7 @@ from flask import request, session, redirect -from conditional import auth -from conditional.blueprints.packet import packet_bp +from conditional import auth, app from conditional.util.ldap import ( ldap_is_active, ldap_is_alumni, @@ -104,14 +103,14 @@ def frosh_auth(): return -@packet_bp.route("/auth/csh") +@app.route("/auth/csh") @auth.oidc_auth("csh") def csh_login(): session["provider"] = "csh" return redirect("/packet", code=301) -@packet_bp.route("/auth/frosh") +@app.route("/auth/frosh") @auth.oidc_auth("frosh") def frosh_login(): session["provider"] = "frosh" From 55ea7f8369b3a0462ac37a746cab4b05bfbbf73f Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 9 Sep 2025 19:21:43 -0400 Subject: [PATCH 56/76] packet glue but even more (#399) --- conditional/util/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/util/auth.py b/conditional/util/auth.py index e3921b67..cfca8765 100644 --- a/conditional/util/auth.py +++ b/conditional/util/auth.py @@ -104,7 +104,7 @@ def frosh_auth(): @app.route("/auth/csh") -@auth.oidc_auth("csh") +@auth.oidc_auth("default") def csh_login(): session["provider"] = "csh" return redirect("/packet", code=301) From 49c7bab8aed32855b16884bd4d7146bf40b4eae9 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 9 Sep 2025 19:38:29 -0400 Subject: [PATCH 57/76] packet glue but again (navbar yippee) (#400) --- conditional/__init__.py | 2 ++ conditional/templates/nav.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/conditional/__init__.py b/conditional/__init__.py index 4ccbc5a5..5fe72694 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -128,6 +128,7 @@ def database_processor( from .blueprints.cache_management import cache_bp from .blueprints.co_op import co_op_bp from .blueprints.logs import log_bp +from .blueprints.packet import packet_bp app.register_blueprint(dashboard_bp) app.register_blueprint(attendance_bp) @@ -142,6 +143,7 @@ def database_processor( app.register_blueprint(cache_bp) app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) +app.register_blueprint(packet_bp) from .util.ldap import ldap_get_member diff --git a/conditional/templates/nav.html b/conditional/templates/nav.html index 6e4173f7..ffb3ec9d 100644 --- a/conditional/templates/nav.html +++ b/conditional/templates/nav.html @@ -54,6 +54,8 @@ +
  • Packet
  • + {% if is_eboard or is_rtp%} diff --git a/migrations/versions/27b8305621f5_add_packet.py b/migrations/versions/27b8305621f5_add_packet.py new file mode 100644 index 00000000..de74b1ee --- /dev/null +++ b/migrations/versions/27b8305621f5_add_packet.py @@ -0,0 +1,94 @@ +"""add packet + +Revision ID: 27b8305621f5 +Revises: 05126dcdf40e +Create Date: 2025-09-10 09:36:50.644872 + +""" + +# revision identifiers, used by Alembic. +revision = '27b8305621f5' +down_revision = '05126dcdf40e' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('freshman', + sa.Column('rit_username', sa.String(length=10), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('onfloor', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('rit_username') + ) + op.create_table('notification_subscriptions', + sa.Column('member', sa.String(length=36), nullable=True), + sa.Column('freshman_username', sa.String(length=10), nullable=True), + sa.Column('token', sa.String(length=256), nullable=False), + sa.ForeignKeyConstraint(['freshman_username'], ['freshman.rit_username'], ), + sa.PrimaryKeyConstraint('token') + ) + op.create_table('packet', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('freshman_username', sa.String(length=10), nullable=True), + sa.Column('start', sa.DateTime(), nullable=False), + sa.Column('end', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['freshman_username'], ['freshman.rit_username'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('signature_fresh', + sa.Column('packet_id', sa.Integer(), nullable=False), + sa.Column('freshman_username', sa.String(length=10), nullable=False), + sa.Column('signed', sa.Boolean(), nullable=False), + sa.Column('updated', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['freshman_username'], ['freshman.rit_username'], ), + sa.ForeignKeyConstraint(['packet_id'], ['packet.id'], ), + sa.PrimaryKeyConstraint('packet_id', 'freshman_username') + ) + op.create_table('signature_misc', + sa.Column('packet_id', sa.Integer(), nullable=False), + sa.Column('member', sa.String(length=36), nullable=False), + sa.Column('updated', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['packet_id'], ['packet.id'], ), + sa.PrimaryKeyConstraint('packet_id', 'member') + ) + op.create_table('signature_upper', + sa.Column('packet_id', sa.Integer(), nullable=False), + sa.Column('member', sa.String(length=36), nullable=False), + sa.Column('signed', sa.Boolean(), nullable=False), + sa.Column('eboard', sa.String(length=12), nullable=True), + sa.Column('active_rtp', sa.Boolean(), nullable=False), + sa.Column('three_da', sa.Boolean(), nullable=False), + sa.Column('webmaster', sa.Boolean(), nullable=False), + sa.Column('c_m', sa.Boolean(), nullable=False), + sa.Column('w_m', sa.Boolean(), nullable=False), + sa.Column('drink_admin', sa.Boolean(), nullable=False), + sa.Column('updated', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['packet_id'], ['packet.id'], ), + sa.PrimaryKeyConstraint('packet_id', 'member') + ) + with op.batch_alter_table('freshman_accounts', schema=None) as batch_op: + batch_op.drop_column('real_signatures_missed') + + with op.batch_alter_table('freshman_eval_data', schema=None) as batch_op: + batch_op.drop_column('real_signatures_missed') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('freshman_eval_data', schema=None) as batch_op: + batch_op.add_column(sa.Column('real_signatures_missed', sa.INTEGER(), server_default=sa.text("'-1'::integer"), autoincrement=False, nullable=False)) + + with op.batch_alter_table('freshman_accounts', schema=None) as batch_op: + batch_op.add_column(sa.Column('real_signatures_missed', sa.INTEGER(), server_default=sa.text("'-1'::integer"), autoincrement=False, nullable=False)) + + op.drop_table('signature_upper') + op.drop_table('signature_misc') + op.drop_table('signature_fresh') + op.drop_table('packet') + op.drop_table('notification_subscriptions') + op.drop_table('freshman') + # ### end Alembic commands ### From ee6fc4113ff316e33778122d44db7156e85552ef Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 16 Sep 2025 12:54:30 -0400 Subject: [PATCH 61/76] packet glue (#404) * migration, url fixes * chom --- conditional/util/context_processors.py | 101 +++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/conditional/util/context_processors.py b/conditional/util/context_processors.py index 766e4d82..28971a2f 100644 --- a/conditional/util/context_processors.py +++ b/conditional/util/context_processors.py @@ -1,7 +1,13 @@ # pylint: disable=bare-except -from conditional import app -from conditional.models.models import FreshmanAccount +import hashlib +import urllib +from functools import lru_cache +from datetime import datetime +from typing import Callable + +from conditional import app, packet_bp +from conditional.models.models import FreshmanAccount, Freshman, UpperSignature from conditional.util.cache import service_cache from conditional.util.ldap import ldap_get_member, ldap_is_current_student @@ -34,8 +40,89 @@ def check_current_student(username): @app.context_processor def utility_processor(): return { - "get_csh_name": get_csh_name, - "get_freshman_name": get_freshman_name, - "get_member_name": get_member_name, - "check_current_student": check_current_student - } + "get_csh_name": get_csh_name, + "get_freshman_name": get_freshman_name, + "get_member_name": get_member_name, + "check_current_student": check_current_student, + } + + +# pylint: disable=bare-except +@lru_cache(maxsize=128) +def packet_get_csh_name(username: str) -> str: + try: + member = ldap_get_member(username) + return member.cn + " (" + member.uid + ")" + except: + return username + + +def get_roles(sig: UpperSignature) -> dict[str, str]: + """ + Converts a signature's role fields to a dict for ease of access. + :return: A dictionary of role short names to role long names + """ + out = {} + if sig.eboard: + out["eboard"] = sig.eboard + if sig.active_rtp: + out["rtp"] = "RTP" + if sig.three_da: + out["three_da"] = "3DA" + if sig.w_m: + out["wm"] = "Wiki Maintainer" + if sig.webmaster: + out["webmaster"] = "Webmaster" + if sig.c_m: + out["cm"] = "Constitutional Maintainer" + if sig.drink_admin: + out["drink"] = "Drink Admin" + return out + + +# pylint: disable=bare-except +@lru_cache(maxsize=256) +def get_rit_name(username: str) -> str: + try: + freshman = Freshman.query.filter_by(rit_username=username).first() + return freshman.name + " (" + username + ")" + except: + return username + + +# pylint: disable=bare-except +@lru_cache(maxsize=256) +def get_rit_image(username: str) -> str: + if username: + addresses = [username + "@rit.edu", username + "@g.rit.edu"] + for addr in addresses: + url = ( + "https://gravatar.com/avatar/" + + hashlib.md5(addr.encode("utf8")).hexdigest() + + ".jpg?d=404&s=250" + ) + try: + with urllib.request.urlopen(url) as gravatar: + if gravatar.getcode() == 200: + return url + except: + continue + return "https://www.gravatar.com/avatar/freshmen?d=mp&f=y" + + +def log_time(label: str) -> None: + """ + Used during debugging to log timestamps while rendering templates + """ + print(label, datetime.now()) + + +@packet_bp.context_processor +def packet_utility_processor() -> dict[str, Callable]: + return { + "get_csh_name": packet_get_csh_name, + "get_rit_name": get_rit_name, + "get_rit_image": get_rit_image, + "log_time": log_time, + "get_roles": get_roles, + } From a904044400cf5afc3a13944f99311b53a08a7681 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 16 Sep 2025 13:08:01 -0400 Subject: [PATCH 62/76] chom (#405) --- conditional/util/context_processors.py | 91 +----------------- conditional/util/packet_context_processors.py | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+), 89 deletions(-) create mode 100644 conditional/util/packet_context_processors.py diff --git a/conditional/util/context_processors.py b/conditional/util/context_processors.py index 28971a2f..b26492ff 100644 --- a/conditional/util/context_processors.py +++ b/conditional/util/context_processors.py @@ -1,13 +1,7 @@ # pylint: disable=bare-except -import hashlib -import urllib -from functools import lru_cache -from datetime import datetime -from typing import Callable - -from conditional import app, packet_bp -from conditional.models.models import FreshmanAccount, Freshman, UpperSignature +from conditional import app +from conditional.models.models import FreshmanAccount from conditional.util.cache import service_cache from conditional.util.ldap import ldap_get_member, ldap_is_current_student @@ -45,84 +39,3 @@ def utility_processor(): "get_member_name": get_member_name, "check_current_student": check_current_student, } - - -# pylint: disable=bare-except -@lru_cache(maxsize=128) -def packet_get_csh_name(username: str) -> str: - try: - member = ldap_get_member(username) - return member.cn + " (" + member.uid + ")" - except: - return username - - -def get_roles(sig: UpperSignature) -> dict[str, str]: - """ - Converts a signature's role fields to a dict for ease of access. - :return: A dictionary of role short names to role long names - """ - out = {} - if sig.eboard: - out["eboard"] = sig.eboard - if sig.active_rtp: - out["rtp"] = "RTP" - if sig.three_da: - out["three_da"] = "3DA" - if sig.w_m: - out["wm"] = "Wiki Maintainer" - if sig.webmaster: - out["webmaster"] = "Webmaster" - if sig.c_m: - out["cm"] = "Constitutional Maintainer" - if sig.drink_admin: - out["drink"] = "Drink Admin" - return out - - -# pylint: disable=bare-except -@lru_cache(maxsize=256) -def get_rit_name(username: str) -> str: - try: - freshman = Freshman.query.filter_by(rit_username=username).first() - return freshman.name + " (" + username + ")" - except: - return username - - -# pylint: disable=bare-except -@lru_cache(maxsize=256) -def get_rit_image(username: str) -> str: - if username: - addresses = [username + "@rit.edu", username + "@g.rit.edu"] - for addr in addresses: - url = ( - "https://gravatar.com/avatar/" - + hashlib.md5(addr.encode("utf8")).hexdigest() - + ".jpg?d=404&s=250" - ) - try: - with urllib.request.urlopen(url) as gravatar: - if gravatar.getcode() == 200: - return url - except: - continue - return "https://www.gravatar.com/avatar/freshmen?d=mp&f=y" - - -def log_time(label: str) -> None: - """ - Used during debugging to log timestamps while rendering templates - """ - print(label, datetime.now()) - - -@packet_bp.context_processor -def packet_utility_processor() -> dict[str, Callable]: - return { - "get_csh_name": packet_get_csh_name, - "get_rit_name": get_rit_name, - "get_rit_image": get_rit_image, - "log_time": log_time, - "get_roles": get_roles, - } diff --git a/conditional/util/packet_context_processors.py b/conditional/util/packet_context_processors.py new file mode 100644 index 00000000..7c8d235e --- /dev/null +++ b/conditional/util/packet_context_processors.py @@ -0,0 +1,92 @@ +# pylint: disable=bare-except + +import hashlib +import urllib +from functools import lru_cache +from datetime import datetime +from typing import Callable + +from conditional import packet_bp +from conditional.models.models import Freshman, UpperSignature +from conditional.util.ldap import ldap_get_member + + +# pylint: disable=bare-except +@lru_cache(maxsize=128) +def packet_get_csh_name(username: str) -> str: + try: + member = ldap_get_member(username) + return member.cn + " (" + member.uid + ")" + except: + return username + + +def get_roles(sig: UpperSignature) -> dict[str, str]: + """ + Converts a signature's role fields to a dict for ease of access. + :return: A dictionary of role short names to role long names + """ + out = {} + if sig.eboard: + out["eboard"] = sig.eboard + if sig.active_rtp: + out["rtp"] = "RTP" + if sig.three_da: + out["three_da"] = "3DA" + if sig.w_m: + out["wm"] = "Wiki Maintainer" + if sig.webmaster: + out["webmaster"] = "Webmaster" + if sig.c_m: + out["cm"] = "Constitutional Maintainer" + if sig.drink_admin: + out["drink"] = "Drink Admin" + return out + + +# pylint: disable=bare-except +@lru_cache(maxsize=256) +def get_rit_name(username: str) -> str: + try: + freshman = Freshman.query.filter_by(rit_username=username).first() + return freshman.name + " (" + username + ")" + except: + return username + + +# pylint: disable=bare-except +@lru_cache(maxsize=256) +def get_rit_image(username: str) -> str: + if username: + addresses = [username + "@rit.edu", username + "@g.rit.edu"] + for addr in addresses: + url = ( + "https://gravatar.com/avatar/" + + hashlib.md5(addr.encode("utf8")).hexdigest() + + ".jpg?d=404&s=250" + ) + try: + with urllib.request.urlopen(url) as gravatar: + if gravatar.getcode() == 200: + return url + except: + continue + return "https://www.gravatar.com/avatar/freshmen?d=mp&f=y" + + +def log_time(label: str) -> None: + """ + Used during debugging to log timestamps while rendering templates + """ + print(label, datetime.now()) + + +@packet_bp.context_processor +def packet_utility_processor() -> dict[str, Callable]: + return { + "get_csh_name": packet_get_csh_name, + "get_rit_name": get_rit_name, + "get_rit_image": get_rit_image, + "log_time": log_time, + "get_roles": get_roles, + } From 8a6b501de722381664ae2fa2dd21a16930a05192 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 16 Sep 2025 13:23:00 -0400 Subject: [PATCH 63/76] chom (#406) --- conditional/util/context_processors.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/conditional/util/context_processors.py b/conditional/util/context_processors.py index b26492ff..58adfdaa 100644 --- a/conditional/util/context_processors.py +++ b/conditional/util/context_processors.py @@ -1,5 +1,8 @@ # pylint: disable=bare-except +import hashlib +import urllib + from conditional import app from conditional.models.models import FreshmanAccount from conditional.util.cache import service_cache @@ -31,11 +34,31 @@ def check_current_student(username): return ldap_is_current_student(member) +@service_cache(maxsize=256) +def get_rit_image(username: str) -> str: + if username: + addresses = [username + "@rit.edu", username + "@g.rit.edu"] + for addr in addresses: + url = ( + "https://gravatar.com/avatar/" + + hashlib.md5(addr.encode("utf8")).hexdigest() + + ".jpg?d=404&s=250" + ) + try: + with urllib.request.urlopen(url) as gravatar: + if gravatar.getcode() == 200: + return url + except: + continue + return "https://www.gravatar.com/avatar/freshmen?d=mp&f=y" + + @app.context_processor def utility_processor(): return { "get_csh_name": get_csh_name, "get_freshman_name": get_freshman_name, + "get_rit_image": get_rit_image, "get_member_name": get_member_name, "check_current_student": check_current_student, } From 2b4bc44bdb35b70601ef23a85ecdc880e3a4efb8 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 16 Sep 2025 13:37:42 -0400 Subject: [PATCH 64/76] packet glue (#407) * chom * chom --- conditional/blueprints/packet.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conditional/blueprints/packet.py b/conditional/blueprints/packet.py index 2b40f78c..3a2a5337 100644 --- a/conditional/blueprints/packet.py +++ b/conditional/blueprints/packet.py @@ -435,3 +435,9 @@ def csh_login(): def frosh_login(): session["provider"] = "frosh" return redirect("/packet", code=301) + + +@packet_bp.route("/logout") +@auth.oidc_logout +def logout(): + return redirect("/", 302) From 25d43f29d0c4fb602600f663f4af71b2fa23b231 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Sun, 5 Oct 2025 21:02:27 -0400 Subject: [PATCH 65/76] gatekeep --- conditional/__init__.py | 37 +++++++++++++++++++++++++++++++++++-- config.env.py | 3 +++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index 1c2f5ee9..d4d4c943 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -3,7 +3,7 @@ import structlog from csh_ldap import CSHLDAP -from flask import Flask, redirect, render_template, g +from flask import Flask, redirect, render_template, request, g from flask_migrate import Migrate from flask_gzip import Gzip from flask_pyoidc.flask_pyoidc import OIDCAuthentication @@ -56,7 +56,14 @@ def start_of_year(): # pylint: disable=C0413 -from .models.models import UserLog +from .models.models import ( + CommitteeMeeting, + MemberCommitteeAttendance, + MemberHouseMeetingAttendance, + MemberSeminarAttendance, + TechnicalSeminar, + UserLog, +) # Configure Logging @@ -159,6 +166,32 @@ def health(): return {'status': 'ok'} +@app.route("/gatekeep/") +def gatekeep_status(username): + token = request.headers.get("X-VOTE-TOKEN","") + if token != app.config["VOTE_TOKEN"]: + return "Users cannot access this page", 403 + # number of committee meetings attended + c_meetings = len([m.meeting_id for m in + MemberCommitteeAttendance.query.filter( + MemberCommitteeAttendance.uid == username + ) if CommitteeMeeting.query.filter( + CommitteeMeeting.id == m.meeting_id).first().approved]) + # technical seminar total + t_seminars = len([s.seminar_id for s in + MemberSeminarAttendance.query.filter( + MemberSeminarAttendance.uid == username + ) if TechnicalSeminar.query.filter( + TechnicalSeminar.id == s.seminar_id).first().approved]) + # house meeting total + h_meetings = len([(m.meeting_id, m.attendance_status) for m in + MemberHouseMeetingAttendance.query.filter( + MemberHouseMeetingAttendance.uid == username)]) + result = c_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6 + return {"result": result}, 200 + + + @app.errorhandler(404) @app.errorhandler(500) @auth.oidc_auth("default") diff --git a/config.env.py b/config.env.py index 29c0689b..f8c419e4 100644 --- a/config.env.py +++ b/config.env.py @@ -52,3 +52,6 @@ # General config DUES_PER_SEMESTER = env.get("CONDITIONAL_DUES_PER_SEMESTER", 80) + +# Vote config +VOTE_TOKEN = env.get("CONDITIONAL_VOTE_TOKEN", "") From b9cb4fcedb2d55de862da2fcebe58ae7d136b6d0 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Fri, 10 Oct 2025 23:42:46 -0400 Subject: [PATCH 66/76] voting status on dashboard --- conditional/util/member.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conditional/util/member.py b/conditional/util/member.py index 1c4ae8c5..5b5c9c4d 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -1,6 +1,6 @@ from datetime import datetime -from conditional import start_of_year +from conditional import gatekeep_status, start_of_year from conditional.models.models import CommitteeMeeting from conditional.models.models import CurrentCoops from conditional.models.models import FreshmanEvalData @@ -32,8 +32,8 @@ def get_voting_members(): on_coop = set(member.uid for member in CurrentCoops.query.filter( CurrentCoops.date_created > start_of_year(), CurrentCoops.semester == semester).all()) - voting_list = list(active_members - intro_members - on_coop) + voting_list = list(username for username in voting_list if gatekeep_status(username)) passed_fall = FreshmanEvalData.query.filter( FreshmanEvalData.freshman_eval_result == "Passed", From e7617e867cdf90ff347bd9a8b929ce835662ba3d Mon Sep 17 00:00:00 2001 From: shaeespring Date: Sun, 12 Oct 2025 07:03:39 -0400 Subject: [PATCH 67/76] adding more return information for vote to use --- conditional/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index d4d4c943..556c0543 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -171,8 +171,8 @@ def gatekeep_status(username): token = request.headers.get("X-VOTE-TOKEN","") if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 - # number of committee meetings attended - c_meetings = len([m.meeting_id for m in + # number of directorship meetings attended + d_meetings = len([m.meeting_id for m in MemberCommitteeAttendance.query.filter( MemberCommitteeAttendance.uid == username ) if CommitteeMeeting.query.filter( @@ -187,8 +187,11 @@ def gatekeep_status(username): h_meetings = len([(m.meeting_id, m.attendance_status) for m in MemberHouseMeetingAttendance.query.filter( MemberHouseMeetingAttendance.uid == username)]) - result = c_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6 - return {"result": result}, 200 + result = d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6 + return {"result": result, + "h_meetings": h_meetings, + "c_meetings": d_meetings, + "t_seminars": t_seminars}, 200 From da6c87df38b67cdb776b57ef3a679c55ca6a18a4 Mon Sep 17 00:00:00 2001 From: Cole Stowell <121599022+costowell@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:19:08 -0400 Subject: [PATCH 68/76] revert Jeremy's attempt to merge packet into conditional (#409) * Revert "packet glue (#407)" This reverts commit 2b4bc44bdb35b70601ef23a85ecdc880e3a4efb8. * Revert "chom (#406)" This reverts commit 8a6b501de722381664ae2fa2dd21a16930a05192. * Revert "chom (#405)" This reverts commit a904044400cf5afc3a13944f99311b53a08a7681. * Revert "packet glue (#404)" This reverts commit ee6fc4113ff316e33778122d44db7156e85552ef. * Revert "migration, url fixes (#403)" This reverts commit 736d203a2646928ed3ee0e9ced395526cfaeaf72. * Revert "Merge pull request #402 from Qelxiros/packet-glue" This reverts commit 0fc55d5bfaf52bb5520d8954e74371eacaf56f3e, reversing changes made to 4fe950fffc6a8780e29352cdb66bd1f285b20b7a. * Revert "glue (#401)" This reverts commit 4fe950fffc6a8780e29352cdb66bd1f285b20b7a. * Revert "packet glue but again (navbar yippee) (#400)" This reverts commit 49c7bab8aed32855b16884bd4d7146bf40b4eae9. * Revert "packet glue but even more (#399)" This reverts commit 55ea7f8369b3a0462ac37a746cab4b05bfbbf73f. * Revert "Merge pull request #398 from Qelxiros/packet-glue" This reverts commit 416bc1041c28e9071de5aa0d147cd19943b56072, reversing changes made to baf42dfb8f12d549671948dd5e58edc8107d6947. * Revert "packet glue but again (#397)" This reverts commit baf42dfb8f12d549671948dd5e58edc8107d6947. * Revert "Merge pull request #396 from Qelxiros/packet-glue" This reverts commit f936afd64cab3acce089a05505aa5991f035264f, reversing changes made to d3a02802d345e28a6c2855f9b99c99b67b6cff62. --- .github/workflows/python-app.yml | 2 +- conditional/__init__.py | 113 ++--- conditional/blueprints/packet.py | 443 ------------------ conditional/models/models.py | 374 +++------------ conditional/templates/active_packets.html | 117 ----- conditional/templates/admin_freshmen.html | 27 -- conditional/templates/admin_packets.html | 30 -- conditional/templates/error.html | 20 - conditional/templates/extend/base.html | 29 -- conditional/templates/extend/email.html | 17 - .../templates/include/admin/all_freshmen.html | 33 -- .../templates/include/admin/new_packets.html | 23 - .../templates/include/admin/open_packets.html | 39 -- .../include/admin/sync_freshmen.html | 22 - conditional/templates/include/footer.html | 6 - conditional/templates/include/head.html | 98 ---- conditional/templates/include/nav.html | 67 --- conditional/templates/include/scripts.html | 15 - conditional/templates/mail/packet_start.html | 15 - conditional/templates/mail/packet_start.txt | 14 - conditional/templates/mail/report.html | 10 - conditional/templates/mail/report.txt | 7 - conditional/templates/nav.html | 2 - conditional/templates/not_found.html | 17 - conditional/templates/packet.html | 194 -------- conditional/templates/packet_stats.html | 84 ---- conditional/templates/upperclassman.html | 59 --- .../templates/upperclassmen_totals.html | 90 ---- conditional/util/auth.py | 84 +--- conditional/util/context_processors.py | 33 +- conditional/util/ldap.py | 110 +---- conditional/util/mail.py | 53 --- conditional/util/packet.py | 197 -------- conditional/util/packet_context_processors.py | 92 ---- conditional/util/stats.py | 161 ------- config.env.py | 27 +- .../versions/27b8305621f5_add_packet.py | 94 ---- requirements.in | 1 - requirements.txt | 4 - 39 files changed, 154 insertions(+), 2669 deletions(-) delete mode 100644 conditional/blueprints/packet.py delete mode 100644 conditional/templates/active_packets.html delete mode 100644 conditional/templates/admin_freshmen.html delete mode 100644 conditional/templates/admin_packets.html delete mode 100644 conditional/templates/error.html delete mode 100644 conditional/templates/extend/base.html delete mode 100644 conditional/templates/extend/email.html delete mode 100644 conditional/templates/include/admin/all_freshmen.html delete mode 100644 conditional/templates/include/admin/new_packets.html delete mode 100644 conditional/templates/include/admin/open_packets.html delete mode 100644 conditional/templates/include/admin/sync_freshmen.html delete mode 100644 conditional/templates/include/footer.html delete mode 100644 conditional/templates/include/head.html delete mode 100644 conditional/templates/include/nav.html delete mode 100644 conditional/templates/include/scripts.html delete mode 100644 conditional/templates/mail/packet_start.html delete mode 100644 conditional/templates/mail/packet_start.txt delete mode 100644 conditional/templates/mail/report.html delete mode 100644 conditional/templates/mail/report.txt delete mode 100644 conditional/templates/not_found.html delete mode 100644 conditional/templates/packet.html delete mode 100644 conditional/templates/packet_stats.html delete mode 100644 conditional/templates/upperclassman.html delete mode 100644 conditional/templates/upperclassmen_totals.html delete mode 100644 conditional/util/mail.py delete mode 100644 conditional/util/packet.py delete mode 100644 conditional/util/packet_context_processors.py delete mode 100644 conditional/util/stats.py delete mode 100644 migrations/versions/27b8305621f5_add_packet.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 38ed5fa9..980b1ab3 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -31,4 +31,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with pylint run: | - pylint conditional --disable=logging-fstring-interpolation + pylint conditional diff --git a/conditional/__init__.py b/conditional/__init__.py index 5fe72694..1c2f5ee9 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -31,26 +31,19 @@ # Sentry setup sentry_sdk.init( - dsn=app.config["SENTRY_DSN"], + dsn=app.config['SENTRY_DSN'], integrations=[FlaskIntegration(), SqlalchemyIntegration()], - environment=app.config["SENTRY_ENV"], + environment=app.config['SENTRY_ENV'], ) -ldap = CSHLDAP( - app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PW"], ro=app.config["LDAP_RO"] -) +ldap = CSHLDAP(app.config['LDAP_BIND_DN'], + app.config['LDAP_BIND_PW'], + ro=app.config['LDAP_RO']) client_metadata = ClientMetadata(app.config["OIDC_CLIENT_CONFIG"]) -provider_config = ProviderConfiguration( - issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata -) -frosh_provider_config = ProviderConfiguration( - issuer=app.config["FROSH_OIDC_ISSUER"], client_registration_info=client_metadata -) +provider_config = ProviderConfiguration(issuer=app.config["OIDC_ISSUER"], client_registration_info=client_metadata) -auth = OIDCAuthentication( - {"default": provider_config, "frosh": frosh_provider_config}, app -) +auth = OIDCAuthentication({'default': provider_config}, app) app.secret_key = app.config["SECRET_KEY"] @@ -67,48 +60,42 @@ def start_of_year(): # Configure Logging -def request_processor( - logger, log_method, event_dict -): # pylint: disable=unused-argument, redefined-outer-name - if "request" in event_dict: - flask_request = event_dict["request"] - event_dict["ip"] = flask_request.remote_addr - event_dict["method"] = flask_request.method - event_dict["blueprint"] = flask_request.blueprint - event_dict["path"] = flask_request.full_path - if "auth_dict" in event_dict: - auth_dict = event_dict["auth_dict"] - event_dict["user"] = auth_dict["username"] +def request_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name + if 'request' in event_dict: + flask_request = event_dict['request'] + event_dict['ip'] = flask_request.remote_addr + event_dict['method'] = flask_request.method + event_dict['blueprint'] = flask_request.blueprint + event_dict['path'] = flask_request.full_path + if 'auth_dict' in event_dict: + auth_dict = event_dict['auth_dict'] + event_dict['user'] = auth_dict['username'] return event_dict -def database_processor( - logger, log_method, event_dict -): # pylint: disable=unused-argument, redefined-outer-name - if "request" in event_dict: - if event_dict["method"] != "GET": +def database_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name + if 'request' in event_dict: + if event_dict['method'] != 'GET': log = UserLog( - ipaddr=event_dict["ip"], - user=event_dict["user"], - method=event_dict["method"], - blueprint=event_dict["blueprint"], - path=event_dict["path"], - description=event_dict["event"], + ipaddr=event_dict['ip'], + user=event_dict['user'], + method=event_dict['method'], + blueprint=event_dict['blueprint'], + path=event_dict['path'], + description=event_dict['event'] ) db.session.add(log) db.session.flush() db.session.commit() - del event_dict["request"] + del event_dict['request'] return event_dict -structlog.configure( - processors=[ - request_processor, - database_processor, - structlog.processors.KeyValueRenderer(), - ] -) +structlog.configure(processors=[ + request_processor, + database_processor, + structlog.processors.KeyValueRenderer() +]) logger = structlog.get_logger() @@ -128,7 +115,6 @@ def database_processor( from .blueprints.cache_management import cache_bp from .blueprints.co_op import co_op_bp from .blueprints.logs import log_bp -from .blueprints.packet import packet_bp app.register_blueprint(dashboard_bp) app.register_blueprint(attendance_bp) @@ -143,21 +129,20 @@ def database_processor( app.register_blueprint(cache_bp) app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) -app.register_blueprint(packet_bp) from .util.ldap import ldap_get_member -@app.route("/") +@app.route('/') def static_proxy(path): # send_static_file will guess the correct MIME type return app.send_static_file(path) -@app.route("/") +@app.route('/') @auth.oidc_auth("default") def default_route(): - return redirect("/dashboard") + return redirect('/dashboard') @app.route("/logout") @@ -171,7 +156,7 @@ def health(): """ Shows an ok status if the application is up and running """ - return {"status": "ok"} + return {'status': 'ok'} @app.errorhandler(404) @@ -182,17 +167,17 @@ def route_errors(error, user_dict=None): data = {} # Handle the case where the header isn't present - if user_dict["username"] is not None: - data["username"] = user_dict["account"].uid - data["name"] = user_dict["account"].cn + if user_dict['username'] is not None: + data['username'] = user_dict['account'].uid + data['name'] = user_dict['account'].cn else: - data["username"] = "unknown" - data["name"] = "Unknown" + data['username'] = "unknown" + data['name'] = "Unknown" # Figure out what kind of error was passed if isinstance(error, int): code = error - elif hasattr(error, "code"): + elif hasattr(error, 'code'): code = error.code else: # Unhandled exception @@ -204,13 +189,11 @@ def route_errors(error, user_dict=None): else: error_desc = type(error).__name__ - return render_template( - "errors.html", - error=error_desc, - error_code=code, - event_id=sentry_sdk.last_event_id(), - **data - ), int(code) + return render_template('errors.html', + error=error_desc, + error_code=code, + event_id=sentry_sdk.last_event_id(), + **data), int(code) -logger.info("conditional started") +logger.info('conditional started') diff --git a/conditional/blueprints/packet.py b/conditional/blueprints/packet.py deleted file mode 100644 index 3a2a5337..00000000 --- a/conditional/blueprints/packet.py +++ /dev/null @@ -1,443 +0,0 @@ -import json -from datetime import datetime -from operator import itemgetter - -import structlog -from flask import Blueprint, redirect, render_template, request, session - -from conditional import auth, app, db -from conditional.util import stats as stats_module -from conditional.util.context_processors import get_freshman_name -from conditional.util.mail import send_report_mail -from conditional.util.auth import get_user, needs_auth -from conditional.util.ldap import ldap_is_eval_director -from conditional.util.packet import ( - create_new_packets, - sync_freshman_list, - sync_with_ldap, -) -from conditional.models.models import ( - MiscSignature, - Packet, - Freshman, -) - -logger = structlog.get_logger() - -packet_bp = Blueprint("packet_bp", __name__, url_prefix="/packet") - - -class POSTFreshman: - def __init__(self, freshman): - self.name = freshman["name"].strip() - self.rit_username = freshman["rit_username"].strip() - self.onfloor = freshman["onfloor"].strip() == "TRUE" - - -@packet_bp.route("/admin/packets") -@auth.oidc_auth("default") -@get_user -def admin_packets(user_dict=None): - if not ldap_is_eval_director(user_dict["account"]): - return redirect("/dashboard") - - open_packets = Packet.open_packets() - - # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() - for packet in open_packets: - packet.did_sign_result = packet.did_sign( - user_dict["username"], session["provider"] == "csh" - ) - packet.signatures_received_result = packet.signatures_received() - packet.signatures_required_result = packet.signatures_required() - - open_packets.sort(key=packet_sort_key, reverse=True) - - return render_template( - "admin_packets.html", open_packets=open_packets, info=user_dict - ) - - -@packet_bp.route("/admin/freshmen") -@auth.oidc_auth("default") -@get_user -def admin_freshmen(user_dict=None): - if not ldap_is_eval_director(user_dict["account"]): - return redirect("/dashboard") - - all_freshmen = Freshman.get_all() - - return render_template( - "admin_freshmen.html", all_freshmen=all_freshmen, info=user_dict - ) - - -@packet_bp.route("/api/v1/freshmen", methods=["POST"]) -@auth.oidc_auth("default") -@get_user -def sync_freshman(user_dict=None): - """ - Create or update freshmen entries from a list - - Body parameters: [ - { - rit_username: string - name: string - onfloor: boolean - } - ] - """ - - # Only allow evals to create new frosh - if not ldap_is_eval_director(user_dict["account"]): - redirect("/dashboard") - - freshmen_in_post = { - freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json) - } - sync_freshman_list(freshmen_in_post) - return json.dumps("Done"), 200 - - -@packet_bp.route("/api/v1/packets", methods=["POST"]) -@auth.oidc_auth("default") -@get_user -def create_packet(user_dict=None): - """ - Create a new packet. - - Body parameters: { - start_date: the start date of the packets in MM/DD/YYYY format - freshmen: [ - { - rit_username: string - name: string - onfloor: boolean - } - ] - } - """ - - # Only allow evals to create new packets - if not ldap_is_eval_director(user_dict["account"]): - redirect("/dashboard") - - base_date = datetime.strptime(request.json["start_date"], "%m/%d/%Y").date() - - freshmen_in_post = { - freshman.rit_username: freshman - for freshman in map(POSTFreshman, request.json["freshmen"]) - } - - create_new_packets(base_date, freshmen_in_post) - - return json.dumps("Done"), 201 - - -@packet_bp.route("/api/v1/sync", methods=["POST"]) -@auth.oidc_auth("default") -@get_user -def sync_ldap(user_dict=None): - # Only allow evals to sync ldap - if not ldap_is_eval_director(user_dict["account"]): - redirect("/dashboard") - sync_with_ldap() - return json.dumps("Done"), 201 - - -@packet_bp.route("/api/v1/packets/", methods=["GET"]) -@auth.oidc_auth("default") -@get_user -def get_packets_by_user(username: str, user_dict=None) -> dict: - """ - Return a dictionary of packets for a freshman by username, giving packet start and end date by packet id - """ - - if user_dict["ritdn"] != username: - redirect("/dashboard") - frosh = Freshman.by_username(username) - - return { - packet.id: { - "start": packet.start, - "end": packet.end, - } - for packet in frosh.packets - } - - -@packet_bp.route("/api/v1/packets//newest", methods=["GET"]) -@auth.oidc_auth("default") -@get_user -def get_newest_packet_by_user(username: str, user_dict=None) -> dict: - """ - Return a user's newest packet - """ - - if not user_dict["is_upper"] and user_dict["ritdn"] != username: - redirect("/dashboard") - - frosh = Freshman.by_username(username) - - packet = frosh.packets[-1] - - return { - packet.id: { - "start": packet.start, - "end": packet.end, - "required": vars(packet.signatures_required()), - "received": vars(packet.signatures_received()), - } - } - - -@packet_bp.route("/api/v1/packet/", methods=["GET"]) -@auth.oidc_auth("default") -@get_user -def get_packet_by_id(packet_id: int, user_dict=None) -> dict: - """ - Return the scores of the packet in question - """ - - packet = Packet.by_id(packet_id) - - if user_dict["ritdn"] != packet.freshman.rit_username: - redirect("/dashboard") - - return { - "required": vars(packet.signatures_required()), - "received": vars(packet.signatures_received()), - } - - -@packet_bp.route("/api/v1/sign//", methods=["POST"]) -@needs_auth -def sign(packet_id, user_dict=None): - packet = Packet.by_id(packet_id) - - if packet is not None and packet.is_open(): - if session["provider"] == "csh": - # Check if the CSHer is an upperclassman and if so, sign that row - for sig in filter( - lambda sig: sig.member == user_dict["uid"], packet.upper_signatures - ): - sig.signed = True - app.logger.info( - f"Member {user_dict['uid']} signed packet {packet_id} as an upperclassman" - ) - return commit_sig(packet) - - # The CSHer is a misc so add a new row - db.session.add(MiscSignature(packet=packet, member=user_dict["uid"])) - app.logger.info( - f"Member {user_dict['uid']} signed packet {packet_id} as a misc" - ) - return commit_sig(packet) - if session["provider"] == "frosh": - # Check if the freshman is onfloor and if so, sign that row - for sig in filter( - lambda sig: sig.freshman_username == user_dict["uid"], - packet.fresh_signatures, - ): - sig.signed = True - app.logger.info( - f"Freshman {user_dict['uid']} signed packet {packet_id}" - ) - return commit_sig(packet) - - app.logger.warning( - f"Failed to add {user_dict['uid']}'s signature to packet {packet_id}" - ) - return "Error: Signature not valid. Reason: Unknown" - - -@packet_bp.route("/api/v1/report/", methods=["POST"]) -@needs_auth -def report(user_dict=None): - if session["provider"] != "frosh": - return "Failure", 403 - - form_results = request.form - send_report_mail(form_results, get_freshman_name(user_dict["username"])) - return "Success: " + get_freshman_name(user_dict["username"]) + " sent a report" - - -@packet_bp.route("/api/v1/stats/packet/") -@auth.oidc_auth("default") -@get_user -def packet_stats(packet_id, user_dict=None): - if user_dict["ritdn"] != Packet.by_id(packet_id).freshman.rit_username: - return redirect("/dashboard") - return stats_module.packet_stats(packet_id) - - -@packet_bp.route("/api/v1/stats/upperclassman/") -@auth.oidc_auth("default") -@get_user -def upperclassman_stats(uid): - return stats_module.upperclassman_stats(uid) - - -def commit_sig(packet): - db.session.commit() - - return "Success: Signed Packet: " + packet.freshman_username - - -@packet_bp.route("/packet//") -@needs_auth -def freshman_packet(packet_id, user_dict=None): - packet = Packet.by_id(packet_id) - - if packet is None: - return "Invalid packet or freshman", 404 - - # The current user's freshman signature on this packet - fresh_sig = list( - filter( - lambda sig: ( - sig.freshman_username == user_dict["ritdn"] if user_dict else "" - ), - packet.fresh_signatures, - ) - ) - - return render_template( - "packet.html", - info=user_dict, - packet=packet, - did_sign=packet.did_sign(user_dict["uid"], session["provider"] == "csh"), - required=packet.signatures_required(), - received=packet.signatures_received(), - upper=packet.upper_signatures, - fresh_sig=fresh_sig, - ) - - -def packet_sort_key(packet): - """ - Utility function for generating keys for sorting packets - """ - return ( - packet.freshman.name, - -packet.signatures_received_result.total, - not packet.did_sign_result, - ) - - -@packet_bp.route("/packets/") -@needs_auth -def packets(user_dict=None): - open_packets = Packet.open_packets() - - # Pre-calculate and store the return values of did_sign(), signatures_received(), and signatures_required() - for packet in open_packets: - packet.did_sign_result = packet.did_sign( - user_dict["uid"], session["provider"] == "csh" - ) - packet.signatures_received_result = packet.signatures_received() - packet.signatures_required_result = packet.signatures_required() - - open_packets.sort(key=packet_sort_key) - - return render_template("active_packets.html", info=user_dict, packets=open_packets) - - -@packet_bp.route("/") -def index(): - return """ -

    Hello, world! 2

    - Click here 4 frosh - Click here 4 upper - """ - - -@packet_bp.route("/upperclassmen/") -@auth.oidc_auth("default") -@get_user -def upperclassmen_total(user_dict=None): - open_packets = Packet.open_packets() - - # Sum up the signed packets per upperclassman - upperclassmen = {} - misc = {} - for packet in open_packets: - for sig in packet.upper_signatures: - if sig.member not in upperclassmen: - upperclassmen[sig.member] = 0 - - if sig.signed: - upperclassmen[sig.member] += 1 - for sig in packet.misc_signatures: - misc[sig.member] = 1 + misc.get(sig.member, 0) - - return render_template( - "upperclassmen_totals.html", - info=user_dict, - num_open_packets=len(open_packets), - upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True), - misc=sorted(misc.items(), key=itemgetter(1), reverse=True), - ) - - -@packet_bp.route("/stats/packet/") -@auth.oidc_auth("default") -@get_user -def packet_graphs(packet_id, user_dict=None): - stats = packet_stats(packet_id) - fresh = [] - misc = [] - upper = [] - - # Make a rolling sum of signatures over time - def agg(l, attr, date): - l.append((l[-1] if l else 0) + len(stats["dates"][date][attr])) - - dates = list(stats["dates"].keys()) - for date in dates: - agg(fresh, "fresh", date) - agg(misc, "misc", date) - agg(upper, "upper", date) - - # Stack misc and upper on top of fresh for a nice stacked line graph - for i in range(len(dates)): - misc[i] = misc[i] + fresh[i] - upper[i] = upper[i] + misc[i] - - return render_template( - "packet_stats.html", - info=user_dict, - data=json.dumps( - { - "dates": dates, - "accum": { - "fresh": fresh, - "misc": misc, - "upper": upper, - }, - "daily": {}, - } - ), - fresh=stats["freshman"], - packet=Packet.by_id(packet_id), - ) - - -@packet_bp.route("/auth/csh") -@auth.oidc_auth("default") -def csh_login(): - session["provider"] = "csh" - return redirect("/packet", code=301) - - -@packet_bp.route("/auth/frosh") -@auth.oidc_auth("frosh") -def frosh_login(): - session["provider"] = "frosh" - return redirect("/packet", code=301) - - -@packet_bp.route("/logout") -@auth.oidc_logout -def logout(): - return redirect("/", 302) diff --git a/conditional/models/models.py b/conditional/models/models.py index 5bb3dd64..423795ea 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -1,27 +1,15 @@ -from typing import cast, Optional import time from datetime import date, timedelta, datetime -from itertools import chain -from sqlalchemy import ( - Column, - Integer, - String, - Enum, - ForeignKey, - DateTime, - Date, - Text, - Boolean, -) +from sqlalchemy import Column, Integer, String, Enum, ForeignKey, DateTime, \ + Date, Text, Boolean from sqlalchemy.dialects import postgresql -from sqlalchemy.orm import relationship from conditional import db -attendance_enum = Enum("Attended", "Excused", "Absent", name="attendance_enum") +attendance_enum = Enum('Attended', 'Excused', 'Absent', name='attendance_enum') class FreshmanAccount(db.Model): - __tablename__ = "freshman_accounts" + __tablename__ = 'freshman_accounts' id = Column(Integer, primary_key=True) name = Column(String(64), nullable=False) eval_date = Column(Date, nullable=False) @@ -30,9 +18,7 @@ class FreshmanAccount(db.Model): signatures_missed = Column(Integer) rit_username = Column(String(10), nullable=True) - def __init__( - self, name, onfloor, room=None, missed=None, rit_username=None - ): # pylint: disable=too-many-positional-arguments + def __init__(self, name, onfloor, room=None, missed=None, rit_username=None): # pylint: disable=too-many-positional-arguments self.name = name today = date.fromtimestamp(time.time()) self.eval_date = today + timedelta(weeks=6) @@ -43,25 +29,22 @@ def __init__( class FreshmanEvalData(db.Model): - __tablename__ = "freshman_eval_data" + __tablename__ = 'freshman_eval_data' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - freshman_project = Column( - Enum("Pending", "Passed", "Failed", name="freshman_project_enum"), nullable=True - ) + freshman_project = Column(Enum('Pending', 'Passed', 'Failed', name="freshman_project_enum"), nullable=True) eval_date = Column(DateTime, nullable=False) signatures_missed = Column(Integer, nullable=False) social_events = Column(Text) other_notes = Column(Text) - freshman_eval_result = Column( - Enum("Pending", "Passed", "Failed", name="freshman_eval_enum"), nullable=False - ) + freshman_eval_result = Column(Enum('Pending', 'Passed', 'Failed', + name="freshman_eval_enum"), nullable=False) active = Column(Boolean) def __init__(self, uid, signatures_missed): self.uid = uid self.freshman_project = None - self.freshman_eval_result = "Pending" + self.freshman_eval_result = 'Pending' self.signatures_missed = signatures_missed self.social_events = "" self.other_notes = "" @@ -69,24 +52,12 @@ def __init__(self, uid, signatures_missed): class CommitteeMeeting(db.Model): - __tablename__ = "committee_meetings" + __tablename__ = 'committee_meetings' id = Column(Integer, primary_key=True) - committee = Column( - Enum( - "Evaluations", - "History", - "Social", - "Opcomm", - "R&D", - "House Improvements", - "Financial", - "Public Relations", - "Chairman", - "Ad-Hoc", - name="committees_enum", - ), - nullable=False, - ) + committee = Column(Enum('Evaluations', 'History', 'Social', 'Opcomm', + 'R&D', 'House Improvements', 'Financial', + 'Public Relations', 'Chairman', 'Ad-Hoc', name="committees_enum"), + nullable=False) timestamp = Column(DateTime, nullable=False) approved = Column(Boolean, nullable=False) active = Column(Boolean) @@ -99,10 +70,10 @@ def __init__(self, committee, timestamp, approved): class MemberCommitteeAttendance(db.Model): - __tablename__ = "member_committee_attendance" + __tablename__ = 'member_committee_attendance' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) + meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) def __init__(self, uid, meeting_id): self.uid = uid @@ -110,10 +81,10 @@ def __init__(self, uid, meeting_id): class FreshmanCommitteeAttendance(db.Model): - __tablename__ = "freshman_committee_attendance" + __tablename__ = 'freshman_committee_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) - meeting_id = Column(ForeignKey("committee_meetings.id"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + meeting_id = Column(ForeignKey('committee_meetings.id'), nullable=False) def __init__(self, fid, meeting_id): self.fid = fid @@ -121,7 +92,7 @@ def __init__(self, fid, meeting_id): class TechnicalSeminar(db.Model): - __tablename__ = "technical_seminars" + __tablename__ = 'technical_seminars' id = Column(Integer, primary_key=True) name = Column(String(128), nullable=False) timestamp = Column(DateTime, nullable=False) @@ -136,10 +107,10 @@ def __init__(self, name, timestamp, approved): class MemberSeminarAttendance(db.Model): - __tablename__ = "member_seminar_attendance" + __tablename__ = 'member_seminar_attendance' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) + seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) def __init__(self, uid, seminar_id): self.uid = uid @@ -147,10 +118,10 @@ def __init__(self, uid, seminar_id): class FreshmanSeminarAttendance(db.Model): - __tablename__ = "freshman_seminar_attendance" + __tablename__ = 'freshman_seminar_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) - seminar_id = Column(ForeignKey("technical_seminars.id"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + seminar_id = Column(ForeignKey('technical_seminars.id'), nullable=False) def __init__(self, fid, seminar_id): self.fid = fid @@ -158,7 +129,7 @@ def __init__(self, fid, seminar_id): class MajorProject(db.Model): - __tablename__ = "major_projects" + __tablename__ = 'major_projects' id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) uid = Column(String(32), nullable=False) @@ -167,25 +138,23 @@ class MajorProject(db.Model): time = Column(Text, nullable=False) description = Column(Text, nullable=False) active = Column(Boolean, nullable=False) - status = Column( - Enum("Pending", "Passed", "Failed", name="major_project_enum"), nullable=False - ) + status = Column(Enum('Pending', 'Passed', 'Failed', + name="major_project_enum"), + nullable=False) - def __init__( - self, uid, name, tldr, time, desc - ): # pylint: disable=too-many-positional-arguments,redefined-outer-name + def __init__(self, uid, name, tldr, time, desc): # pylint: disable=too-many-positional-arguments,redefined-outer-name self.uid = uid self.date = datetime.now() self.name = name self.tldr = tldr self.time = time self.description = desc - self.status = "Pending" + self.status = 'Pending' self.active = True class HouseMeeting(db.Model): - __tablename__ = "house_meetings" + __tablename__ = 'house_meetings' id = Column(Integer, primary_key=True) date = Column(Date, nullable=False) active = Column(Boolean, nullable=False) @@ -196,10 +165,10 @@ def __init__(self, hm_date): class MemberHouseMeetingAttendance(db.Model): - __tablename__ = "member_hm_attendance" + __tablename__ = 'member_hm_attendance' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) - meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) + meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -211,10 +180,10 @@ def __init__(self, uid, meeting_id, excuse, status): class FreshmanHouseMeetingAttendance(db.Model): - __tablename__ = "freshman_hm_attendance" + __tablename__ = 'freshman_hm_attendance' id = Column(Integer, primary_key=True) - fid = Column(ForeignKey("freshman_accounts.id"), nullable=False) - meeting_id = Column(ForeignKey("house_meetings.id"), nullable=False) + fid = Column(ForeignKey('freshman_accounts.id'), nullable=False) + meeting_id = Column(ForeignKey('house_meetings.id'), nullable=False) excuse = Column(Text) attendance_status = Column(attendance_enum) @@ -226,11 +195,11 @@ def __init__(self, fid, meeting_id, excuse, status): class CurrentCoops(db.Model): - __tablename__ = "current_coops" + __tablename__ = 'current_coops' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) date_created = Column(Date, nullable=False) - semester = Column(Enum("Fall", "Spring", name="co_op_enum"), nullable=False) + semester = Column(Enum('Fall', 'Spring', name="co_op_enum"), nullable=False) def __init__(self, uid, semester): self.uid = uid @@ -240,7 +209,7 @@ def __init__(self, uid, semester): class OnFloorStatusAssigned(db.Model): - __tablename__ = "onfloor_datetime" + __tablename__ = 'onfloor_datetime' uid = Column(String(32), primary_key=True) onfloor_granted = Column(DateTime, primary_key=True) @@ -250,22 +219,20 @@ def __init__(self, uid, time_granted): class Conditional(db.Model): - __tablename__ = "conditional" + __tablename__ = 'conditional' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) description = Column(String(512), nullable=False) date_created = Column(Date, nullable=False) date_due = Column(Date, nullable=False) active = Column(Boolean, nullable=False) - status = Column( - Enum("Pending", "Passed", "Failed", name="conditional_enum"), nullable=False - ) - s_evaluation = Column(ForeignKey("spring_evals.id")) - i_evaluation = Column(ForeignKey("freshman_eval_data.id")) - - def __init__( - self, uid, description, due, s_eval=None, i_eval=None - ): # pylint: disable=too-many-positional-arguments + status = Column(Enum('Pending', 'Passed', 'Failed', + name="conditional_enum"), + nullable=False) + s_evaluation = Column(ForeignKey('spring_evals.id')) + i_evaluation = Column(ForeignKey('freshman_eval_data.id')) + + def __init__(self, uid, description, due, s_eval=None, i_eval=None): # pylint: disable=too-many-positional-arguments self.uid = uid self.description = description self.date_due = due @@ -277,7 +244,7 @@ def __init__( class EvalSettings(db.Model): - __tablename__ = "settings" + __tablename__ = 'settings' id = Column(Integer, primary_key=True) housing_form_active = Column(Boolean) intro_form_active = Column(Boolean) @@ -292,14 +259,14 @@ def __init__(self): class SpringEval(db.Model): - __tablename__ = "spring_evals" + __tablename__ = 'spring_evals' id = Column(Integer, primary_key=True) uid = Column(String(32), nullable=False) active = Column(Boolean, nullable=False) date_created = Column(Date, nullable=False) - status = Column( - Enum("Pending", "Passed", "Failed", name="spring_eval_enum"), nullable=False - ) + status = Column(Enum('Pending', 'Passed', 'Failed', + name="spring_eval_enum"), + nullable=False) def __init__(self, uid): self.uid = uid @@ -309,26 +276,13 @@ def __init__(self, uid): class InHousingQueue(db.Model): - __tablename__ = "in_housing_queue" + __tablename__ = 'in_housing_queue' uid = Column(String(32), primary_key=True) - -http_enum = Enum( - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "CONNECT", - "OPTIONS", - "TRACE", - "PATCH", - name="http_enum", -) - +http_enum = Enum('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', name='http_enum') class UserLog(db.Model): - __tablename__ = "user_log" + __tablename__ = 'user_log' id = Column(Integer, primary_key=True) ipaddr = Column(postgresql.INET, nullable=False) timestamp = Column(DateTime, nullable=False) @@ -338,9 +292,7 @@ class UserLog(db.Model): path = Column(String(128), nullable=False) description = Column(String(128), nullable=False) - def __init__( - self, ipaddr, user, method, blueprint, path, description - ): # pylint: disable=too-many-positional-arguments + def __init__(self, ipaddr, user, method, blueprint, path, description): # pylint: disable=too-many-positional-arguments self.ipaddr = ipaddr self.timestamp = datetime.now() self.uid = user @@ -348,213 +300,3 @@ def __init__( self.blueprint = blueprint self.path = path self.description = description - - -# The required number of honorary member, advisor, and alumni signatures -REQUIRED_MISC_SIGNATURES = 10 - - -class SigCounts: - """ - Utility class for returning counts of signatures broken out by type - """ - - def __init__(self, upper: int, fresh: int, misc: int): - # Base fields - self.upper = upper - self.fresh = fresh - self.misc = misc - - # Capped version of misc so it will never be greater than REQUIRED_MISC_SIGNATURES - self.misc_capped = ( - misc if misc <= REQUIRED_MISC_SIGNATURES else REQUIRED_MISC_SIGNATURES - ) - - # Totals (calculated using misc_capped) - self.member_total = upper + self.misc_capped - self.total = upper + fresh + self.misc_capped - - -class Freshman(db.Model): - __tablename__ = "freshman" - rit_username = cast(str, Column(String(10), primary_key=True)) - name = cast(str, Column(String(64), nullable=False)) - onfloor = cast(bool, Column(Boolean, nullable=False)) - fresh_signatures = cast("FreshSignature", relationship("FreshSignature")) - - # One freshman can have multiple packets if they repeat the intro process - packets = cast("Packet", relationship("Packet", order_by="desc(Packet.id)")) - - @classmethod - def by_username(cls, username: str) -> "Packet": - """ - Helper method to retrieve a freshman by their RIT username - """ - return cls.query.filter_by(rit_username=username).first() - - @classmethod - def get_all(cls) -> list["Packet"]: - """ - Helper method to get all freshmen easily - """ - return cls.query.all() - - -class Packet(db.Model): - __tablename__ = "packet" - id = cast(int, Column(Integer, primary_key=True, autoincrement=True)) - freshman_username = cast(str, Column(ForeignKey("freshman.rit_username"))) - start = cast(datetime, Column(DateTime, nullable=False)) - end = cast(datetime, Column(DateTime, nullable=False)) - - freshman = cast(Freshman, relationship("Freshman", back_populates="packets")) - - # The `lazy='subquery'` kwarg enables eager loading for signatures which makes signature calculations much faster - # See the docs here for details: https://docs.sqlalchemy.org/en/latest/orm/loading_relationships.html - upper_signatures = cast( - "UpperSignature", - relationship( - "UpperSignature", - lazy="subquery", - order_by="UpperSignature.signed.desc(), UpperSignature.updated", - ), - ) - fresh_signatures = cast( - "FreshSignature", - relationship( - "FreshSignature", - lazy="subquery", - order_by="FreshSignature.signed.desc(), FreshSignature.updated", - ), - ) - misc_signatures = cast( - "MiscSignature", - relationship( - "MiscSignature", lazy="subquery", order_by="MiscSignature.updated" - ), - ) - - def is_open(self) -> bool: - return self.start < datetime.now() < self.end - - def signatures_required(self) -> SigCounts: - """ - :return: A SigCounts instance with the fields set to the number of signatures received by this packet - """ - upper = len(self.upper_signatures) - fresh = len(self.fresh_signatures) - - return SigCounts(upper, fresh, REQUIRED_MISC_SIGNATURES) - - def signatures_received(self) -> SigCounts: - """ - :return: A SigCounts instance with the fields set to the number of required signatures for this packet - """ - upper = sum(map(lambda sig: 1 if sig.signed else 0, self.upper_signatures)) - fresh = sum(map(lambda sig: 1 if sig.signed else 0, self.fresh_signatures)) - - return SigCounts(upper, fresh, len(self.misc_signatures)) - - def did_sign(self, username: str, is_csh: bool) -> bool: - """ - :param username: The CSH or RIT username to check for - :param is_csh: Set to True for CSH accounts and False for freshmen - :return: Boolean value for if the given account signed this packet - """ - if is_csh: - for sig in filter( - lambda sig: sig.member == username, - chain(self.upper_signatures, self.misc_signatures), - ): - if isinstance(sig, MiscSignature): - return True - return sig.signed - else: - for sig in filter( - lambda sig: sig.freshman_username == username, self.fresh_signatures - ): - return sig.signed - - # The user must be a misc CSHer that hasn't signed this packet or an off-floor freshmen - return False - - def is_100(self) -> bool: - """ - Checks if this packet has reached 100% - """ - return self.signatures_required().total == self.signatures_received().total - - @classmethod - def open_packets(cls) -> list["Packet"]: - """ - Helper method for fetching all currently open packets - """ - return cls.query.filter( - cls.start < datetime.now(), cls.end > datetime.now() - ).all() - - @classmethod - def by_id(cls, packet_id: int) -> "Packet": - """ - Helper method for fetching 1 packet by its id - """ - return cls.query.filter_by(id=packet_id).first() - - -class UpperSignature(db.Model): - __tablename__ = "signature_upper" - packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) - member = cast(str, Column(String(36), primary_key=True)) - signed = cast(bool, Column(Boolean, default=False, nullable=False)) - eboard = cast(Optional[str], Column(String(12), nullable=True)) - active_rtp = cast(bool, Column(Boolean, default=False, nullable=False)) - three_da = cast(bool, Column(Boolean, default=False, nullable=False)) - webmaster = cast(bool, Column(Boolean, default=False, nullable=False)) - c_m = cast(bool, Column(Boolean, default=False, nullable=False)) - w_m = cast(bool, Column(Boolean, default=False, nullable=False)) - drink_admin = cast(bool, Column(Boolean, default=False, nullable=False)) - updated = cast( - datetime, - Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), - ) - - packet = cast(Packet, relationship("Packet", back_populates="upper_signatures")) - - -class FreshSignature(db.Model): - __tablename__ = "signature_fresh" - packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) - freshman_username = cast( - str, Column(ForeignKey("freshman.rit_username"), primary_key=True) - ) - signed = cast(bool, Column(Boolean, default=False, nullable=False)) - updated = cast( - datetime, - Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), - ) - - packet = cast(Packet, relationship("Packet", back_populates="fresh_signatures")) - freshman = cast( - Freshman, relationship("Freshman", back_populates="fresh_signatures") - ) - - -class MiscSignature(db.Model): - __tablename__ = "signature_misc" - packet_id = cast(int, Column(Integer, ForeignKey("packet.id"), primary_key=True)) - member = cast(str, Column(String(36), primary_key=True)) - updated = cast( - datetime, - Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False), - ) - - packet = cast(Packet, relationship("Packet", back_populates="misc_signatures")) - - -class NotificationSubscription(db.Model): - __tablename__ = "notification_subscriptions" - member = cast(str, Column(String(36), nullable=True)) - freshman_username = cast( - str, Column(ForeignKey("freshman.rit_username"), nullable=True) - ) - token = cast(str, Column(String(256), primary_key=True, nullable=False)) diff --git a/conditional/templates/active_packets.html b/conditional/templates/active_packets.html deleted file mode 100644 index 3fa65640..00000000 --- a/conditional/templates/active_packets.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends "extend/base.html" %} - -{% block body %} -
    -
    -
    -
    -

    Active Packets

    -
    - {% if info.is_upper %} -
    - -
    - {% endif %} -
    -
    -
    - {% if packets|length > 0 %} -
    -
    -
    -
    - - - - - {% if info.is_upper %} - - - - {% endif %} - - - - - {% for packet in packets %} - - - {% if info.is_upper %} - - - - {% endif %} - - - {% endfor %} - -
    NameSignaturesSignaturesSignaturesSign
    - {% if info.is_upper %} - - {% endif %} - {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} - {% if info.is_upper %} - - {% endif %} - - {% if packet.signatures_received_result.member_total == packet.signatures_required_result.member_total %} - 💯 {# 100% emoji #} - {% else %} - {{ packet.signatures_received_result.member_total }} / - {{ packet.signatures_required_result.member_total }} - {% endif %} - - {% if packet.signatures_received_result.fresh == packet.signatures_required_result.fresh %} - 💯 {# 100% emoji #} - {% else %} - {{ packet.signatures_received_result.fresh }} / - {{ packet.signatures_required_result.fresh }} - {% endif %} - - {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} - 💯 {# 100% emoji #} - {% else %} - {{ packet.signatures_received_result.total }} / - {{ packet.signatures_required_result.total }} - {% endif %} - - {% if not packet.did_sign_result and info.ritdn != packet.freshman_username %} - - {% elif info.ritdn != packet.freshman_username %} - - {% endif %} -
    -
    -
    -
    -
    - {% else %} - - {% endif %} -
    -
    -{% endblock %} - -{% block scripts %} - {{ super() }} - {% if info.realm == "csh" %} - - {% endif %} -{% endblock %} diff --git a/conditional/templates/admin_freshmen.html b/conditional/templates/admin_freshmen.html deleted file mode 100644 index e922375f..00000000 --- a/conditional/templates/admin_freshmen.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "extend/base.html" %} - -{% block body %} -
    -
    -
    -
    -

    All Freshmen

    -
    -
    - - {% include 'include/admin/sync_freshmen.html' %} -
    -
    -
    -
    - {% include 'include/admin/all_freshmen.html' %} -
    -
    -{% endblock %} - -{% block scripts %} - {{ super() }} - -{% endblock %} diff --git a/conditional/templates/admin_packets.html b/conditional/templates/admin_packets.html deleted file mode 100644 index 9db36fd2..00000000 --- a/conditional/templates/admin_packets.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "extend/base.html" %} - -{% block body %} -
    -
    -
    -
    -

    Active Packets

    -
    -
    - - - {% include 'include/admin/new_packets.html' %} -
    -
    -
    -
    - {% include 'include/admin/open_packets.html' %} -
    -
    -{% endblock %} - -{% block scripts %} - {{ super() }} - -{% endblock %} diff --git a/conditional/templates/error.html b/conditional/templates/error.html deleted file mode 100644 index de33536b..00000000 --- a/conditional/templates/error.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'extend/base.html' %} - -{% block body %} -
    -

    Oops!

    -
    -
    -
    - I guess this is what you get when you trust a bunch of college kids. -
    -

    -

    {{ e }}
    -

    -
    - Do us a favor, try again. If you end up here on the second try, shoot us an email. -
    -
    -
    -
    -{% endblock %} diff --git a/conditional/templates/extend/base.html b/conditional/templates/extend/base.html deleted file mode 100644 index 418e93d6..00000000 --- a/conditional/templates/extend/base.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -{% block head %} - {% include "include/head.html" %} -{% endblock %} - - - -{% block nav %} - {% include "include/nav.html" %} -{% endblock %} - -{% block body %} -{% endblock %} - -{% block footer %} - {% include "include/footer.html" %} -{% endblock %} - -{% block includes %} -{% endblock %} - -{% block scripts %} - {% include "include/scripts.html" %} -{% endblock %} - - - diff --git a/conditional/templates/extend/email.html b/conditional/templates/extend/email.html deleted file mode 100644 index 20fb7de7..00000000 --- a/conditional/templates/extend/email.html +++ /dev/null @@ -1,17 +0,0 @@ - - - -{% block head %} - - CSH Packet - - -{% endblock %} - - -{% block body %} -{% endblock %} - - diff --git a/conditional/templates/include/admin/all_freshmen.html b/conditional/templates/include/admin/all_freshmen.html deleted file mode 100644 index a3e79e3b..00000000 --- a/conditional/templates/include/admin/all_freshmen.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    -
    - - - - - - - - - {% for freshman in all_freshmen %} - {% set freshman_name = freshman.name + ' (' + freshman.rit_username + ')' %} - - - - - {% endfor %} - -
    NameOn-Floor
    - {{ freshman_name }} {{ freshman_name }} - - {{ freshman.onfloor }} -
    -
    -
    -
    -
    diff --git a/conditional/templates/include/admin/new_packets.html b/conditional/templates/include/admin/new_packets.html deleted file mode 100644 index c6dd3075..00000000 --- a/conditional/templates/include/admin/new_packets.html +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/conditional/templates/include/admin/open_packets.html b/conditional/templates/include/admin/open_packets.html deleted file mode 100644 index 2db5d4b8..00000000 --- a/conditional/templates/include/admin/open_packets.html +++ /dev/null @@ -1,39 +0,0 @@ -
    -
    -
    -
    - - - - - - - - - {% for packet in open_packets %} - - - - - {% endfor %} - -
    NameSignatures
    - - {{ get_rit_name(packet.freshman_username) }} {{ get_rit_name(packet.freshman_username) }} - - - {% if packet.signatures_received_result.total == packet.signatures_required_result.total %} - 💯 {# 100% emoji #} - {% else %} - {{ packet.signatures_received_result.total }} / - {{ packet.signatures_required_result.total }} - {% endif %} -
    -
    -
    -
    -
    diff --git a/conditional/templates/include/admin/sync_freshmen.html b/conditional/templates/include/admin/sync_freshmen.html deleted file mode 100644 index 6f9b4806..00000000 --- a/conditional/templates/include/admin/sync_freshmen.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/conditional/templates/include/footer.html b/conditional/templates/include/footer.html deleted file mode 100644 index b1e9bdcf..00000000 --- a/conditional/templates/include/footer.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/conditional/templates/include/head.html b/conditional/templates/include/head.html deleted file mode 100644 index 20d0f420..00000000 --- a/conditional/templates/include/head.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - CSH Packet - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/conditional/templates/include/nav.html b/conditional/templates/include/nav.html deleted file mode 100644 index 6c035623..00000000 --- a/conditional/templates/include/nav.html +++ /dev/null @@ -1,67 +0,0 @@ - diff --git a/conditional/templates/include/scripts.html b/conditional/templates/include/scripts.html deleted file mode 100644 index af0b7632..00000000 --- a/conditional/templates/include/scripts.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - -{% if info.realm == "intro" %} - -{% endif %} diff --git a/conditional/templates/mail/packet_start.html b/conditional/templates/mail/packet_start.html deleted file mode 100644 index 722e5768..00000000 --- a/conditional/templates/mail/packet_start.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "extend/email.html" %} - -{% block body %} -
    -

    Hello {{ packet.freshman.name }},

    -

    Welcome to Computer Science House!

    -

    Soon you'll starting the introductory process for CSH, and the first part of that is Packet.

    -

    Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }}

    -

    You can view your packet at {{ config["PACKET_INTRO"] }} with - the credentials you should have been sent.

    -

    If you don't know your credentials, reach out to an RTP

    -

    If you have any questions about Packet or the introductory process, email evals@csh.rit.edu

    -

    If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu

    -
    -{% endblock %} diff --git a/conditional/templates/mail/packet_start.txt b/conditional/templates/mail/packet_start.txt deleted file mode 100644 index 162ec147..00000000 --- a/conditional/templates/mail/packet_start.txt +++ /dev/null @@ -1,14 +0,0 @@ -Hello {{ packet.freshman.name }}, - -Welcome to Computer Science House! - -Soon you'll starting the introductory process for CSH, and the first part of that is Packet. - -Your packet will start on {{ packet.start.strftime('%A, %B %-d') }} at {{ packet.start.strftime('%-I:%M %p') }} - -You can view your packet at {{ config["PROTOCOL"] + config["PACKET_INTRO"] }} with the credentials you should have been sent. -If you don't know your credentials, reach out to an RTP - -If you have any questions about Packet or the introductory process, email evals@csh.rit.edu - -If you have any questions about login credentials or any technical issues, email rtp@csh.rit.edu diff --git a/conditional/templates/mail/report.html b/conditional/templates/mail/report.html deleted file mode 100644 index dbfedcbc..00000000 --- a/conditional/templates/mail/report.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "extend/email.html" %} - -{% block body %} -
    -

    Hello,

    -

    {{ reporter }} just made a report against {{ person }}

    -

    The report reads:

    -
    {{ report }}
    -
    -{% endblock %} diff --git a/conditional/templates/mail/report.txt b/conditional/templates/mail/report.txt deleted file mode 100644 index 7a5576c7..00000000 --- a/conditional/templates/mail/report.txt +++ /dev/null @@ -1,7 +0,0 @@ -Hello, - -{{ reporter }} just made a report against {{ person }} - -The report reads: - -{{ report }} diff --git a/conditional/templates/nav.html b/conditional/templates/nav.html index ffb3ec9d..6e4173f7 100644 --- a/conditional/templates/nav.html +++ b/conditional/templates/nav.html @@ -54,8 +54,6 @@ -
  • Packet
  • - {% if is_eboard or is_rtp%}
    From 2e140fecad52965599d3ad04fff40733463955d7 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Tue, 14 Oct 2025 23:37:26 -0400 Subject: [PATCH 70/76] account for semesters --- conditional/__init__.py | 107 +++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index 556c0543..753c75fc 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -58,6 +58,9 @@ def start_of_year(): # pylint: disable=C0413 from .models.models import ( CommitteeMeeting, + CurrentCoops, + FreshmanEvalData, + HouseMeeting, MemberCommitteeAttendance, MemberHouseMeetingAttendance, MemberSeminarAttendance, @@ -137,7 +140,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) -from .util.ldap import ldap_get_member +from .util.ldap import ldap_get_member, ldap_is_active, ldap_is_intromember @app.route('/') @@ -168,30 +171,88 @@ def health(): @app.route("/gatekeep/") def gatekeep_status(username): - token = request.headers.get("X-VOTE-TOKEN","") + token = request.headers.get("X-VOTE-TOKEN", "") if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 - # number of directorship meetings attended - d_meetings = len([m.meeting_id for m in - MemberCommitteeAttendance.query.filter( - MemberCommitteeAttendance.uid == username - ) if CommitteeMeeting.query.filter( - CommitteeMeeting.id == m.meeting_id).first().approved]) - # technical seminar total - t_seminars = len([s.seminar_id for s in - MemberSeminarAttendance.query.filter( - MemberSeminarAttendance.uid == username - ) if TechnicalSeminar.query.filter( - TechnicalSeminar.id == s.seminar_id).first().approved]) - # house meeting total - h_meetings = len([(m.meeting_id, m.attendance_status) for m in - MemberHouseMeetingAttendance.query.filter( - MemberHouseMeetingAttendance.uid == username)]) - result = d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6 - return {"result": result, - "h_meetings": h_meetings, - "c_meetings": d_meetings, - "t_seminars": t_seminars}, 200 + + if datetime.today() < datetime(start_of_year().year, 12, 31): + semester = "Fall" + semester_start = datetime(start_of_year().year,6,1) + else: + semester = "Spring" + semester_start = datetime(start_of_year().year + 1,1,1) + + # groups + ldap_member = ldap_get_member(username) + is_intro_member = ldap_is_intromember(ldap_member) + is_active_member = ldap_is_active(ldap_member) and not is_intro_member + + is_on_coop = ( + CurrentCoops.query.filter( + CurrentCoops.date_created > start_of_year(), + CurrentCoops.semester == semester, + CurrentCoops.uid == username, + ).first() + is not None + ) + + passed_fall = ( + FreshmanEvalData.query.filter( + FreshmanEvalData.freshman_eval_result == "Passed", + FreshmanEvalData.eval_date > start_of_year(), + FreshmanEvalData.uid == username, + ).first() + is not None + ) + eligibility_of_groups = (is_active_member and not is_on_coop) or passed_fall + + # number of directorship meetings attended in the current semester + d_meetings = ( + MemberCommitteeAttendance.query.join( + CommitteeMeeting, + MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id, + ) + .filter( + MemberCommitteeAttendance.uid == username, + CommitteeMeeting.approved is True, + CommitteeMeeting.date >= semester_start, + ) + .count() + ) + # number of technical seminars attended in the current semester + t_seminars = ( + MemberSeminarAttendance.query.join( + TechnicalSeminar, + MemberSeminarAttendance.meeting_id == TechnicalSeminar.id, + ) + .filter( + MemberSeminarAttendance.uid == username, + TechnicalSeminar.approved is True, + TechnicalSeminar.date >= semester_start, + ) + .count() + ) + # number of house meetings attended in the current semester + h_meetings = ( + MemberHouseMeetingAttendance.query.join( + HouseMeeting, + MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id, + ) + .filter( + MemberHouseMeetingAttendance.uid == username, + HouseMeeting.date >= semester_start + ) + .count() + ) + result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6) + + return { + "result": result, + "h_meetings": h_meetings, + "c_meetings": d_meetings, + "t_seminars": t_seminars, + }, 200 + From af307ee3b8395ad8846b5bfdfaba6c884e9d4b54 Mon Sep 17 00:00:00 2001 From: shaeespring Date: Tue, 14 Oct 2025 23:38:35 -0400 Subject: [PATCH 71/76] remove frosh from voting lists if they don't have requirements after 6 weeks --- conditional/util/member.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/conditional/util/member.py b/conditional/util/member.py index 5b5c9c4d..69a6ac97 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -32,8 +32,7 @@ def get_voting_members(): on_coop = set(member.uid for member in CurrentCoops.query.filter( CurrentCoops.date_created > start_of_year(), CurrentCoops.semester == semester).all()) - voting_list = list(active_members - intro_members - on_coop) - voting_list = list(username for username in voting_list if gatekeep_status(username)) + voting_set = active_members - intro_members - on_coop passed_fall = FreshmanEvalData.query.filter( FreshmanEvalData.freshman_eval_result == "Passed", @@ -41,9 +40,9 @@ def get_voting_members(): ).distinct() for intro_member in passed_fall: - if intro_member.uid not in voting_list: - voting_list.append(intro_member.uid) + voting_set.add(intro_member.uid) + voting_list = list(username for username in voting_set if gatekeep_status(username)) return voting_list From b8fd20c1661fa0381b56c548b53ab3fdeebc1e07 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Sat, 18 Oct 2025 14:04:00 -0400 Subject: [PATCH 72/76] Revert @tallen42's work on the major project form (#410) --- .../blueprints/major_project_submission.py | 152 ++-- conditional/models/models.py | 8 +- .../templates/major_project_submission.html | 214 ++--- config.env.py | 4 - frontend/images/photo_video.svg | 6 - frontend/javascript/modules/dropzone.js | 19 +- .../javascript/modules/majorProjectForm.js | 95 +-- .../stylesheets/pages/_major-project.scss | 52 -- .../versions/05126dcdf40e_add_mp_fields.py | 89 --- package-lock.json | 742 +++++++++++------- package.json | 8 +- requirements.in | 4 +- requirements.txt | 11 - 13 files changed, 615 insertions(+), 789 deletions(-) delete mode 100644 frontend/images/photo_video.svg delete mode 100644 migrations/versions/05126dcdf40e_add_mp_fields.py diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 6fb9649f..54829e7d 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -1,8 +1,5 @@ import json -import os - import requests -import boto3 from flask import Blueprint from flask import request @@ -12,7 +9,6 @@ from sqlalchemy import desc import structlog -from werkzeug.utils import secure_filename from conditional.util.context_processors import get_member_name @@ -26,162 +22,108 @@ logger = structlog.get_logger() -major_project_bp = Blueprint('major_project_bp', __name__) +major_project_bp = Blueprint("major_project_bp", __name__) -@major_project_bp.route('/major_project/') +@major_project_bp.route("/major_project/") @auth.oidc_auth("default") @get_user def display_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Display Major Project Page') + log.info("Display Major Project Page") major_projects = [ { - 'username': p.uid, - 'name': ldap_get_member(p.uid).cn, - 'proj_name': p.name, - 'status': p.status, - 'description': p.description, - 'id': p.id, - 'is_owner': bool(user_dict['username'] == p.uid) - } for p in - MajorProject.query.filter( - MajorProject.date > start_of_year()).order_by( - desc(MajorProject.id))] + "username": p.uid, + "name": ldap_get_member(p.uid).cn, + "proj_name": p.name, + "status": p.status, + "description": p.description, + "id": p.id, + "is_owner": bool(user_dict["username"] == p.uid), + } + for p in MajorProject.query.filter( + MajorProject.date > start_of_year() + ).order_by(desc(MajorProject.id)) + ] major_projects_len = len(major_projects) # return names in 'first last (username)' format - return render_template('major_project_submission.html', - major_projects=major_projects, - major_projects_len=major_projects_len, - username=user_dict['username']) - -@major_project_bp.route('/major_project/upload', methods=['POST']) -@auth.oidc_auth("default") -@get_user -def upload_major_project_files(user_dict=None): - log = logger.new(request=request, auth_dict=user_dict) - log.info('Uploading Major Project File(s)') - + return render_template( + "major_project_submission.html", + major_projects=major_projects, + major_projects_len=major_projects_len, + username=user_dict["username"], + ) - if len(list(request.files.keys())) < 1: - return "No file", 400 - - # Temporarily save files to a place, to be uploaded on submit - - for _, file in request.files.lists(): - file = file[0] # remove it from the list because this is not the best - safe_name = secure_filename(file.filename) - filename = f"/tmp/{user_dict['username']}/{safe_name}" - - os.makedirs(os.path.dirname(filename), exist_ok=True) - file.save(filename) - - return jsonify({"success": True}), 200 - - -@major_project_bp.route('/major_project/submit', methods=['POST']) +@major_project_bp.route("/major_project/submit", methods=["POST"]) @auth.oidc_auth("default") @get_user def submit_major_project(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info('Submit Major Project') + log.info("Submit Major Project") post_data = request.get_json() - print(post_data) - name = post_data['projectName'] - tldr = post_data['projectTldr'] - time_spent = post_data['projectTimeSpent'] - skills = post_data['projectSkills'] - description = post_data['projectDescription'] - - user_id = user_dict['username'] - - print(skills) + name = post_data["projectName"] + description = post_data["projectDescription"] - if name == "" or len(description.strip().split()) < 50: # check for 50 word minimum + if name == "" or description == "": return jsonify({"success": False}), 400 - project = MajorProject(user_id, name, tldr, time_spent, description) + project = MajorProject(user_dict["username"], name, description) + username = user_dict["username"] + send_slack_ping( + { + "text": f" *{get_member_name(username)}* ({username})" + f" submitted their major project, *{name}*!" + } + ) db.session.add(project) db.session.commit() - - # This allows us to get a project with a database ID - project = MajorProject.query.filter( - MajorProject.name == name and MajorProject.uid == user_id - ).first() - - if project is None: - return jsonify({"success": False}), 500 - - # Don't send slack ping until after we are sure the DB worked fine - send_slack_ping({"text":f" *{get_member_name(user_id)}* ({user_id})" - f" submitted their major project, *{name}*!"}) - - # Acquire S3 Bucket instance - s3 = boto3.resource("s3", endpoint_url="https://s3.csh.rit.edu") - bucket = s3.create_bucket(Bucket="major-project-media") - # Collect all the locally cached files and put them in the bucket - for file in os.listdir(f"/tmp/{user_id}"): - filepath = f"/tmp/{user_id}/{file}" - print(filepath) - bucket.upload_file(filepath, f"{project.id}-{file}") - os.remove(filepath) - os.rmdir(f"/tmp/{user_id}") - return jsonify({"success": True}), 200 -@major_project_bp.route('/major_project/review', methods=['POST']) +@major_project_bp.route("/major_project/review", methods=["POST"]) @auth.oidc_auth("default") @get_user def major_project_review(user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - if not ldap_is_eval_director(user_dict['account']): + if not ldap_is_eval_director(user_dict["account"]): return redirect("/dashboard", code=302) post_data = request.get_json() - pid = post_data['id'] - status = post_data['status'] + pid = post_data["id"] + status = post_data["status"] - log.info(f'{status} Major Project ID: {pid}') + log.info(f"{status} Major Project ID: {pid}") print(post_data) - MajorProject.query.filter( - MajorProject.id == pid). \ - update( - { - 'status': status - }) + MajorProject.query.filter(MajorProject.id == pid).update({"status": status}) db.session.flush() db.session.commit() return jsonify({"success": True}), 200 -@major_project_bp.route('/major_project/delete/', methods=['DELETE']) +@major_project_bp.route("/major_project/delete/", methods=["DELETE"]) @auth.oidc_auth("default") @get_user def major_project_delete(pid, user_dict=None): log = logger.new(request=request, auth_dict=user_dict) - log.info(f'Delete Major Project ID: {pid}') + log.info(f"Delete Major Project ID: {pid}") - major_project = MajorProject.query.filter( - MajorProject.id == pid - ).first() + major_project = MajorProject.query.filter(MajorProject.id == pid).first() creator = major_project.uid - if creator == user_dict['username'] or ldap_is_eval_director(user_dict['account']): - MajorProject.query.filter( - MajorProject.id == pid - ).delete() + if creator == user_dict["username"] or ldap_is_eval_director(user_dict["account"]): + MajorProject.query.filter(MajorProject.id == pid).delete() db.session.flush() db.session.commit() return jsonify({"success": True}), 200 return "Must be project owner to delete!", 401 + def send_slack_ping(payload): - requests.post(app.config['WEBHOOK_URL'], json.dumps(payload), timeout=120) + requests.post(app.config["WEBHOOK_URL"], json.dumps(payload), timeout=120) diff --git a/conditional/models/models.py b/conditional/models/models.py index 423795ea..97e8875f 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -134,20 +134,16 @@ class MajorProject(db.Model): date = Column(Date, nullable=False) uid = Column(String(32), nullable=False) name = Column(String(64), nullable=False) - tldr = Column(String(128), nullable=False) - time = Column(Text, nullable=False) - description = Column(Text, nullable=False) + description = Column(Text) active = Column(Boolean, nullable=False) status = Column(Enum('Pending', 'Passed', 'Failed', name="major_project_enum"), nullable=False) - def __init__(self, uid, name, tldr, time, desc): # pylint: disable=too-many-positional-arguments,redefined-outer-name + def __init__(self, uid, name, desc): self.uid = uid self.date = datetime.now() self.name = name - self.tldr = tldr - self.time = time self.description = desc self.status = 'Pending' self.active = True diff --git a/conditional/templates/major_project_submission.html b/conditional/templates/major_project_submission.html index 256688b7..8705d2ad 100644 --- a/conditional/templates/major_project_submission.html +++ b/conditional/templates/major_project_submission.html @@ -1,162 +1,94 @@ {% extends "nav.html" %} -{% block extraHeader %} - -{% endblock %} {% block title %} - Major Project Form +Major Project Form {% endblock %} {% block body %} -
    -

    Major Project Form

    +
    +

    Major Project Form

    +
    -

    Welcome to the Major Project submission form! We're excited to read about what you've - been working on. For us (E-Board) to best evaluate your project, please give us as much detail as - possible. Don't feel pressured to write full paragraphs though, good bullet points are plenty! -
    Generally, a major project is something that you make with the goal of challenging yourself, - learning new things, and doing something you would be proud of. Major projects are most likely to - pass when they meet at least 2 of the 3 - Major Project Pillars - - considerable time on your project, benefiting House, and meaningfully applying skills. And of course, - after you submit, please try to talk to E-Board members (in-person or over Slack) so we are familiar - with your project and can ask you questions!

    -
    -
    - -
    -
    - +
    + - - -
    -
    - -
    - - List what skills you meaningfully used while working on this project (at least 2!)
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    - -
    Upload Media
    -
    Drag files here or click to upload.
    -
    -
    -
    -
    + placeholder="A clever name for your project, sometimes people will come up with an acronym.">
    - - - -

    All Major Projects

    {% if major_projects_len - <=0 %} +
    -
    -

    No Pending Major Projects

    +
    +
    + + +
    - {% else %} + + +

    All Major Projects

    {% if major_projects_len + <=0 %}
    +
    +

    No Pending Major Projects

    +
    +
    +{% else %} - {% for p in major_projects %} -
    -
    -
    -

    {{ p['proj_name'] }}

    - - {{ p['name'] }} ({{ p['username'] }}) -
    -
    + {% for p in major_projects %} +
    +
    +
    +

    {{p['proj_name']}}

    + + {{p['name']}} ({{p['username']}}) +
    +
    - {% if is_eval_director %} + {% if is_eval_director %} - - {% else %} - {% if p['status'] == 'Passed' %} -
    - {% elif p['status'] == 'Failed' %} -
    - {% else %} -
    - {% endif %} - {% if p.is_owner and p['status'] == 'Pending' %} - - {% endif %} - {% endif %} -
    - -
    - {{ p['description'] }} -
    + + {% else %} + {% if p['status'] == 'Passed' %} +
    + {% elif p['status'] == 'Failed' %} +
    + {% else %} +
    + {% endif %} + {% if p.is_owner and p['status'] == 'Pending' %} + + {% endif %} + {% endif %} +
    + +
    + {{p['description']}}
    - {% endfor %} +
    +
    + {% endfor %} - {% endif %} +{% endif %} -
    +
    {% endblock %} diff --git a/config.env.py b/config.env.py index 29c0689b..44e0464e 100644 --- a/config.env.py +++ b/config.env.py @@ -25,10 +25,6 @@ LDAP_BIND_DN = env.get("CONDITIONAL_LDAP_BIND_DN", "cn=conditional,ou=Apps,dc=csh,dc=rit,dc=edu") LDAP_BIND_PW = env.get("CONDITIONAL_LDAP_BIND_PW", "") -# S3 information -AWS_ACCESS_KEY_ID = env.get("AWS_ACCESS_KEY_ID", "") -AWS_SECRET_ACCESS_KEY = env.get("AWS_SECRET_ACCESS_KEY", "") - # Sentry config # Not required for local development, but if you set it, make sure the # SENTRY_ENV is 'local-development' diff --git a/frontend/images/photo_video.svg b/frontend/images/photo_video.svg deleted file mode 100644 index 2324ec73..00000000 --- a/frontend/images/photo_video.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/javascript/modules/dropzone.js b/frontend/javascript/modules/dropzone.js index 959f2b19..2b04a87e 100644 --- a/frontend/javascript/modules/dropzone.js +++ b/frontend/javascript/modules/dropzone.js @@ -1,19 +1,18 @@ import Dropzone from "dropzone"; - Dropzone.autoDiscover = false; export default class DropzoneUpload { - constructor(element) { - this.element = element; - this.render(); - } + constructor(element) { + this.element = element; + this.render(); + } - render() { - const dz = new Dropzone(this.element); // eslint-disable-line new-cap - dz.on("complete", () => window.location.reload()); - } + render() { + const dz = new Dropzone(this.element); // eslint-disable-line new-cap + dz.on("complete", () => window.location.reload()); + } } Dropzone.options.uploadUser = { - acceptedFiles: ".csv" + acceptedFiles: ".csv" }; diff --git a/frontend/javascript/modules/majorProjectForm.js b/frontend/javascript/modules/majorProjectForm.js index 89a210dd..fc9969c1 100644 --- a/frontend/javascript/modules/majorProjectForm.js +++ b/frontend/javascript/modules/majorProjectForm.js @@ -1,73 +1,30 @@ import FetchUtil from "../utils/fetchUtil"; export default class MajorProjectForm { - - - constructor(form) { - this.form = form; - this.endpoint = '/major_project/submit'; - this.tags_written = false; - this.tag_keys = ["Enter", "Comma"]; - this.render(); - } - - render() { - this.form.querySelector('input[type=submit]') - .addEventListener('click', e => this._submitForm(e)); - this.form.querySelector('input[id=skill-input]') - .addEventListener('focusout', e => this.onWriteSkill(e)); - this.form.querySelector('input[id=skill-input]') - .addEventListener('keypress', e => this.onKeyPress(e)); - } - - onKeyPress(e) { - if (this.tag_keys.includes(e.code)) { - e.preventDefault(); - this.onWriteSkill(e); - } - return false; - } - - onWriteSkill(e) { - let input = document.getElementById("skill-input") - if (!this.tags_written) { - this.tags_written = true - document.getElementsByClassName("placeholder").item(0).remove() - } - let txt = input.value.replace(/[^a-zA-Z0-9\+\-\.\# ]/g, ''); // allowed characters list - if (txt) input.insertAdjacentHTML("beforebegin", '' + txt + ''); - let skills = document.getElementsByClassName("skill-tag") - skills.item(skills.length - 1).addEventListener('click', e => this.onRemoveTag(e)); - input.value = ""; - input.focus(); - - } - - onRemoveTag(e) { - e.target.remove(); - } - - _submitForm(e) { - e.preventDefault(); - - let skills = []; - - Array.from(document.getElementsByClassName('skill-tag')).forEach(tag => skills.push(tag.firstChild.data)) - - let payload = { - projectName: this.form.querySelector('input[name=name]').value, - projectTldr: this.form.querySelector('input[name=tldr]').value, - projectTimeSpent: this.form.querySelector('textarea[name=time-commitment]').value, - projectSkills: skills, - projectDescription: this.form.querySelector('textarea[name=description]').value - }; - - console.log(payload) - - FetchUtil.postWithWarning(this.endpoint, payload, { - warningText: "You will not be able to edit your " + - "project once it has been submitted.", - successText: "Your project has been submitted." - }); - } + constructor(form) { + this.form = form; + this.endpoint = '/major_project/submit'; + this.render(); + } + + render() { + this.form.querySelector('input[type=submit]') + .addEventListener('click', e => this._submitForm(e)); + } + + _submitForm(e) { + e.preventDefault(); + + let payload = { + projectName: this.form.querySelector('input[name=name]').value, + projectDescription: + this.form.querySelector('textarea[name=description]').value + }; + + FetchUtil.postWithWarning(this.endpoint, payload, { + warningText: "You will not be able to edit your " + + "project once it has been submitted.", + successText: "Your project has been submitted." + }); + } } diff --git a/frontend/stylesheets/pages/_major-project.scss b/frontend/stylesheets/pages/_major-project.scss index ca0667ce..01438510 100644 --- a/frontend/stylesheets/pages/_major-project.scss +++ b/frontend/stylesheets/pages/_major-project.scss @@ -1,55 +1,3 @@ .major-project-desc { white-space: pre-line; } - -.form-label { - margin-bottom: 0; - margin-top: 1.25rem; - font-size: 2rem; -} - -.form-label span { - font-size: 1rem; -} - -.form-textarea { - resize: vertical; -} - -.form-skilltags { - box-shadow: inset 0 -1px 0 #ddd; - border: none; - border-radius: 0; - padding: 0 0 10px; - height: fit-content; -} - -.placeholder { - color: #bbb; - font-size: 16px; -} - -.skill-tag { - display: block; - float: left; - background: #d979e3; - padding: 4px 30px 4px 8px; - margin: 2px 3px; - color: #444; - border-radius: 5px; - transition: .5s all; -} - -.skill-tag:after { - position: absolute; - content: "×"; - border: 1px solid; - border-radius: 10px; - padding: 0 4px; - margin: 3px 0 10px 7px; - font-size: 10px; -} - -.skill-tag:hover { - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} \ No newline at end of file diff --git a/migrations/versions/05126dcdf40e_add_mp_fields.py b/migrations/versions/05126dcdf40e_add_mp_fields.py deleted file mode 100644 index 9eafb64f..00000000 --- a/migrations/versions/05126dcdf40e_add_mp_fields.py +++ /dev/null @@ -1,89 +0,0 @@ -"""add major project fields - -Revision ID: 05126dcdf40e -Revises: 757e18146d16 -Create Date: 2024-09-15 00:20:50.617251 - -""" - -# revision identifiers, used by Alembic. -revision = '05126dcdf40e' -down_revision = '757e18146d16' - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('freshman_batch_pulls') - op.drop_table('batch_conditions') - op.drop_table('freshman_batch_users') - op.drop_table('batch') - op.drop_table('member_batch_users') - op.drop_table('member_batch_pulls') - op.alter_column('freshman_hm_attendance', 'attendance_status', - existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), - nullable=True) - op.add_column('major_projects', sa.Column('time', sa.Text(), nullable=False, server_default='N/A')) - op.add_column('major_projects', sa.Column('tldr', sa.String(length=128), nullable=False, server_default='N/A')) - op.alter_column('major_projects', 'description', - existing_type=sa.TEXT(), - nullable=False) - op.alter_column('member_hm_attendance', 'attendance_status', - existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), - nullable=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('member_hm_attendance', 'attendance_status', - existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), - nullable=False) - op.alter_column('major_projects', 'description', - existing_type=sa.TEXT(), - nullable=True) - op.drop_column('major_projects', 'tldr') - op.drop_column('major_projects', 'time') - op.alter_column('freshman_hm_attendance', 'attendance_status', - existing_type=postgresql.ENUM('Attended', 'Excused', 'Absent', name='attendance_enum'), - nullable=False) - op.create_table('member_batch_pulls', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column('reason', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False) - ) - op.create_table('member_batch_users', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=True), - sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) - ) - op.create_table('batch', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('uid', sa.VARCHAR(length=32), autoincrement=False, nullable=True), - sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False) - ) - op.create_table('freshman_batch_users', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) - ) - op.create_table('batch_conditions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('condition', postgresql.ENUM('packet', 'seminar', 'committee', 'house', name='batch_ctype_enum'), autoincrement=False, nullable=False), - sa.Column('comparison', postgresql.ENUM('less', 'equal', 'greater', name='batch_comparison'), autoincrement=False, nullable=False), - sa.Column('batch_id', sa.INTEGER(), autoincrement=False, nullable=False) - ) - op.create_table('freshman_batch_pulls', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('fid', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column('reason', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('puller', sa.VARCHAR(), autoincrement=False, nullable=False) - ) - # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 4688860d..e232319f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "conditional", - "version": "1.12.0", + "version": "1.10.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -67,12 +67,6 @@ "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=", "dev": true }, - "a-sync-waterfall": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -190,10 +184,10 @@ "ansi-wrap": "0.1.0" } }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "ansi-html": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.5.tgz", + "integrity": "sha1-DcqloIEgaGa8JAo7dzoYTqO4i2Q=", "dev": true }, "ansi-red": { @@ -369,7 +363,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", "dev": true }, "asn1": { @@ -430,9 +424,9 @@ "dev": true }, "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "requires": { "lodash": "^4.17.14" } @@ -1493,9 +1487,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true, "optional": true }, @@ -1539,14 +1533,40 @@ } }, "bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", "dev": true, - "optional": true, "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "readable-stream": "~2.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } } }, "blob": { @@ -1834,9 +1854,9 @@ } }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz", + "integrity": "sha1-1baAoWW2IBc5rLYRVCqrwtjOsHA=", "dev": true }, "cache-base": { @@ -2361,49 +2381,48 @@ } }, "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.6.2.tgz", + "integrity": "sha1-zOsSHsydCcUtetDDNQ6pPd1AK8M=", "dev": true, "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" + "accepts": "~1.3.3", + "bytes": "2.3.0", + "compressible": "~2.0.8", + "debug": "~2.2.0", + "on-headers": "~1.0.1", + "vary": "~1.1.0" }, "dependencies": { "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" } }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", "dev": true, "requires": { - "mime-db": "1.52.0" + "ms": "0.7.1" } }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true } } @@ -2755,11 +2774,11 @@ } }, "datatables.net": { - "version": "1.13.11", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.11.tgz", - "integrity": "sha512-AE6RkMXziRaqzPcu/pl3SJXeRa6fmXQG/fVjuRESujvkzqDCYEeKTTpPMuVJSGYJpPi32WGSphVNNY1G4nSN/g==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.2.tgz", + "integrity": "sha512-Dg+Yeyky+BhXoooJqTDTGssLxkAxe73CljcjYd4630XQZKqqqy0akYk2r4RSHSJLyYCOGRZEZiUF/N39+5nqvg==", "requires": { - "jquery": "1.8 - 4" + "jquery": ">=1.7" } }, "datatables.net-bs": { @@ -4042,23 +4061,14 @@ } }, "es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "dependencies": { - "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - } + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "es6-iterator": { @@ -4238,26 +4248,6 @@ "integrity": "sha1-+RZ2VDK6Z9L8enF3uLz+8/brBWQ=", "dev": true }, - "esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "requires": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "dependencies": { - "type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true - } - } - }, "espree": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", @@ -4472,18 +4462,18 @@ "dev": true }, "json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { "minimist": "^1.2.0" } }, "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -4492,9 +4482,9 @@ } }, "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -4595,6 +4585,12 @@ "is-extglob": "^1.0.0" } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -5694,9 +5690,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "semver": { @@ -6093,40 +6089,217 @@ } }, "gulp-nunjucks-render": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/gulp-nunjucks-render/-/gulp-nunjucks-render-2.2.3.tgz", - "integrity": "sha512-YHrmwiwPw2DN16WJVj8S2tgUanB1ssZE8e5rRF6rH1T41mhO7r59iAqw92Yz5WSZXYB9G+uYXDTCODgixYPIRw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-nunjucks-render/-/gulp-nunjucks-render-2.0.0.tgz", + "integrity": "sha1-z0hQasXAWtiQIyYKMPaVHyI7JZ8=", "dev": true, "requires": { - "lodash": "^4.17.11", - "nunjucks": "^3.1.2", - "plugin-error": "^1.0.1", - "replace-ext": "^1.0.0", - "through2": "^2.0.3" + "gulp-util": "~2.2.0", + "lodash": "^3.3.0", + "nunjucks": "^2.0.0", + "through2": "~0.4.0" }, "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", "dev": true }, - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", "dev": true, "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" } }, - "replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "^0.5.0", + "dateformat": "^1.0.7-1.2.3", + "lodash._reinterpolate": "^2.4.1", + "lodash.template": "^2.4.1", + "minimist": "^0.2.0", + "multipipe": "^0.1.0", + "through2": "^0.5.0", + "vinyl": "^0.2.1" + }, + "dependencies": { + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + } + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "~2.4.1", + "lodash._reunescapedhtml": "~2.4.1", + "lodash.keys": "~2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "~2.4.1", + "lodash._reinterpolate": "~2.4.1", + "lodash.defaults": "~2.4.1", + "lodash.escape": "~2.4.1", + "lodash.keys": "~2.4.1", + "lodash.templatesettings": "~2.4.1", + "lodash.values": "~2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~2.4.1", + "lodash.escape": "~2.4.1" + } + }, + "minimist": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.1.tgz", + "integrity": "sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + }, + "dependencies": { + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "~0.0.1" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", "dev": true } } @@ -6927,9 +7100,9 @@ "dev": true }, "html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", "dev": true }, "html-minifier": { @@ -7452,24 +7625,16 @@ "dev": true }, "is-my-json-valid": { - "version": "2.20.6", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", - "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", + "version": "2.20.5", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz", + "integrity": "sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A==", "dev": true, "requires": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^5.0.0", + "jsonpointer": "^4.0.0", "xtend": "^4.0.0" - }, - "dependencies": { - "jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true - } } }, "is-natural-number": { @@ -7755,19 +7920,19 @@ "dev": true }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", "dev": true, "requires": { "argparse": "^1.0.7", - "esprima": "^4.0.0" + "esprima": "^2.6.0" }, "dependencies": { "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", "dev": true } } @@ -7784,6 +7949,12 @@ "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7838,12 +8009,22 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, + "jsonpointer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", + "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==", + "dev": true + }, "jsprim": { "version": "1.4.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "dev": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" }, "dependencies": { "assert-plus": { @@ -8716,9 +8897,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -8792,9 +8973,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -8842,9 +9023,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -9063,6 +9244,12 @@ "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=", "dev": true }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -9323,9 +9510,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -9422,7 +9609,8 @@ "dev": true, "requires": { "glob": "~7.1.1", - "lodash": "~4.17.10" + "lodash": "~4.17.10", + "minimatch": "~3.0.2" } }, "har-validator": { @@ -9646,21 +9834,53 @@ "dev": true }, "nunjucks": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", - "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-2.5.2.tgz", + "integrity": "sha1-6n00bnhbikh0Zmw8yp4YxXf7oiw=", "dev": true, "requires": { - "a-sync-waterfall": "^1.0.0", "asap": "^2.0.3", - "commander": "^5.1.0" + "chokidar": "^1.6.0", + "yargs": "^3.32.0" }, "dependencies": { - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "dev": true, + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } } } }, @@ -9704,6 +9924,12 @@ } } }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, "object-path": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz", @@ -10800,9 +11026,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true, "optional": true } @@ -11350,46 +11576,11 @@ "tunnel-agent": "~0.4.1" }, "dependencies": { - "bl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", - "integrity": "sha512-phbvN+yOk05EGoFcV/0S8N8ShnJqf6VCWRAw5he2gvRwBubFt/OzmcTNGqBt5b7Y4RK3YCgf6jrgGSR0Cwtsgw==", - "dev": true, - "requires": { - "readable-stream": "~2.0.5" - } - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==", - "dev": true - }, "qs": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", "dev": true - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true } } }, @@ -11611,7 +11802,8 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "ansi-styles": { @@ -11837,17 +12029,6 @@ "glob": "~7.1.1", "lodash": "~4.17.10", "minimatch": "~3.0.2" - }, - "dependencies": { - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } } }, "gonzales-pe-sl": { @@ -11949,9 +12130,9 @@ "integrity": "sha1-myE4zYi8JQSdi7SnCcMzHxg+XJ4=" }, "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "semver-regex": { @@ -12932,9 +13113,9 @@ "optional": true }, "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true, "optional": true } @@ -13102,24 +13283,6 @@ "mkdirp": "~0.5.1", "sax": "~1.2.1", "whet.extend": "~0.9.9" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "dev": true - }, - "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha512-eIlkGty7HGmntbV6P/ZlAsoncFLGsNoM27lkTzS+oneY/EiNhj+geqD9ezg/ip+SW6Var0BJU2JtV0vEUZpWVQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - } - } } }, "svgpath": { @@ -13678,9 +13841,9 @@ "dev": true }, "underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", "dev": true }, "union-value": { @@ -13969,7 +14132,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, "vendors": { @@ -13978,6 +14141,31 @@ "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", "dev": true }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + } + } + }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", @@ -14307,31 +14495,15 @@ } }, "webpack-hot-middleware": { - "version": "2.26.1", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", - "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.12.1.tgz", + "integrity": "sha1-Qlf5ZeaKa4HaW93b1h+J+pB170w=", "dev": true, "requires": { - "ansi-html-community": "0.0.8", - "html-entities": "^2.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "ansi-html": "0.0.5", + "html-entities": "^1.2.0", + "querystring": "^0.2.0", + "strip-ansi": "^3.0.0" } }, "weinre": { @@ -14343,14 +14515,6 @@ "express": "2.5.x", "nopt": "3.0.x", "underscore": "1.7.x" - }, - "dependencies": { - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", - "dev": true - } } }, "whatwg-fetch": { @@ -14395,9 +14559,9 @@ "dev": true }, "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, "wordwrap": { diff --git a/package.json b/package.json index 970e23b1..efcb531e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "bootstrap-sass": "^3.4.1", "bootstrap-sweetalert": "^1.0.1", "csh-material-bootstrap": "1.0.0", - "datatables.net": "^1.13.11", + "datatables.net": "^1.10.12", "datatables.net-bs": "^1.10.12", "dropzone": "^4.3.0", "enumify": "^1.0.4", @@ -51,7 +51,7 @@ "babel-preset-stage-1": "6.5.0", "browser-sync": "2.13.0", "chai": "3.5.0", - "compression": "^1.7.4", + "compression": "1.6.2", "del": "2.2.1", "eslint-config-google": "^0.6.0", "exports-loader": "^0.6.3", @@ -69,7 +69,7 @@ "gulp-if": "2.0.1", "gulp-imagemin": "3.0.1", "gulp-notify": "2.2.0", - "gulp-nunjucks-render": "^2.2.3", + "gulp-nunjucks-render": "2.0.0", "gulp-rename": "1.2.2", "gulp-rev": "7.1.0", "gulp-rev-napkin": "0.1.0", @@ -95,6 +95,6 @@ "sinon-chai": "2.8.0", "webpack": "1.13.1", "webpack-dev-middleware": "1.6.1", - "webpack-hot-middleware": "^2.26.1" + "webpack-hot-middleware": "2.12.1" } } diff --git a/requirements.in b/requirements.in index 49cd221c..0d554b9a 100644 --- a/requirements.in +++ b/requirements.in @@ -1,9 +1,7 @@ alembic~=1.15.1 astroid~=3.3.9 blinker~=1.4 -boto3==1.35.13 -botocore==1.35.13 -click~=8.1.8 +click~=7.1 csh_ldap>=2.3.1 ddtrace~=3.2.1 Flask~=3.1.0 diff --git a/requirements.txt b/requirements.txt index 44225432..dd17e05d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,17 +19,6 @@ blinker==1.9.0 # -r requirements.in # flask # sentry-sdk -boto3==1.35.13 - # via -r requirements.in -botocore==1.35.13 - # via - # -r requirements.in - # boto3 - # s3transfer -build==1.2.2.post1 - # via pip-tools -bytecode==0.16.1 - # via ddtrace certifi==2022.5.18.1 # via # requests From 17b0773cf79bcf56549573e5242672f2fa87a2d8 Mon Sep 17 00:00:00 2001 From: tyler <137842227+goosenotduck@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:53:11 -0400 Subject: [PATCH 73/76] Fix errors missed in #408 (#411) * move gatekeep_status to members.py * Fix errors in gatekeep_status * Fix unused import, fix logic error * Remove duplicate imports * Remove non-existent options from lint --- .pylintrc | 7 +-- conditional/__init__.py | 96 ++------------------------------------ conditional/util/member.py | 85 ++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 99 deletions(-) diff --git a/.pylintrc b/.pylintrc index 22e31bcb..667edd88 100644 --- a/.pylintrc +++ b/.pylintrc @@ -17,14 +17,11 @@ disable = [REPORTS] output-format = text -files-output = no reports = no [FORMAT] max-line-length = 120 -max-statement-lines = 75 single-line-if-stmt = no -no-space-check = trailing-comma,dict-separator max-module-lines = 1000 indent-string = ' ' @@ -72,8 +69,6 @@ good-names=logger,id,ID # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata -# List of builtins function names that should not be used, separated by a comma -bad-functions=apply,input [DESIGN] @@ -89,4 +84,4 @@ min-public-methods = 2 max-public-methods = 20 [EXCEPTIONS] -overgeneral-exceptions = Exception +overgeneral-exceptions = builtins.Exception diff --git a/conditional/__init__.py b/conditional/__init__.py index 753c75fc..10ee9214 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -56,18 +56,7 @@ def start_of_year(): # pylint: disable=C0413 -from .models.models import ( - CommitteeMeeting, - CurrentCoops, - FreshmanEvalData, - HouseMeeting, - MemberCommitteeAttendance, - MemberHouseMeetingAttendance, - MemberSeminarAttendance, - TechnicalSeminar, - UserLog, -) - +from .models.models import UserLog # Configure Logging def request_processor(logger, log_method, event_dict): # pylint: disable=unused-argument, redefined-outer-name @@ -112,6 +101,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse # pylint: disable=wrong-import-order from conditional.util import context_processors from conditional.util.auth import get_user +from conditional.util.member import gatekeep_status from .blueprints.dashboard import dashboard_bp # pylint: disable=ungrouped-imports from .blueprints.attendance import attendance_bp from .blueprints.major_project_submission import major_project_bp @@ -140,7 +130,7 @@ def database_processor(logger, log_method, event_dict): # pylint: disable=unuse app.register_blueprint(co_op_bp) app.register_blueprint(log_bp) -from .util.ldap import ldap_get_member, ldap_is_active, ldap_is_intromember +from .util.ldap import ldap_get_member @app.route('/') @@ -170,88 +160,12 @@ def health(): @app.route("/gatekeep/") -def gatekeep_status(username): +def gatekeep(username): token = request.headers.get("X-VOTE-TOKEN", "") if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 - if datetime.today() < datetime(start_of_year().year, 12, 31): - semester = "Fall" - semester_start = datetime(start_of_year().year,6,1) - else: - semester = "Spring" - semester_start = datetime(start_of_year().year + 1,1,1) - - # groups - ldap_member = ldap_get_member(username) - is_intro_member = ldap_is_intromember(ldap_member) - is_active_member = ldap_is_active(ldap_member) and not is_intro_member - - is_on_coop = ( - CurrentCoops.query.filter( - CurrentCoops.date_created > start_of_year(), - CurrentCoops.semester == semester, - CurrentCoops.uid == username, - ).first() - is not None - ) - - passed_fall = ( - FreshmanEvalData.query.filter( - FreshmanEvalData.freshman_eval_result == "Passed", - FreshmanEvalData.eval_date > start_of_year(), - FreshmanEvalData.uid == username, - ).first() - is not None - ) - eligibility_of_groups = (is_active_member and not is_on_coop) or passed_fall - - # number of directorship meetings attended in the current semester - d_meetings = ( - MemberCommitteeAttendance.query.join( - CommitteeMeeting, - MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id, - ) - .filter( - MemberCommitteeAttendance.uid == username, - CommitteeMeeting.approved is True, - CommitteeMeeting.date >= semester_start, - ) - .count() - ) - # number of technical seminars attended in the current semester - t_seminars = ( - MemberSeminarAttendance.query.join( - TechnicalSeminar, - MemberSeminarAttendance.meeting_id == TechnicalSeminar.id, - ) - .filter( - MemberSeminarAttendance.uid == username, - TechnicalSeminar.approved is True, - TechnicalSeminar.date >= semester_start, - ) - .count() - ) - # number of house meetings attended in the current semester - h_meetings = ( - MemberHouseMeetingAttendance.query.join( - HouseMeeting, - MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id, - ) - .filter( - MemberHouseMeetingAttendance.uid == username, - HouseMeeting.date >= semester_start - ) - .count() - ) - result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6) - - return { - "result": result, - "h_meetings": h_meetings, - "c_meetings": d_meetings, - "t_seminars": t_seminars, - }, 200 + return gatekeep_status(username) diff --git a/conditional/util/member.py b/conditional/util/member.py index 69a6ac97..9040c6ae 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -1,6 +1,6 @@ from datetime import datetime -from conditional import gatekeep_status, start_of_year +from conditional import start_of_year from conditional.models.models import CommitteeMeeting from conditional.models.models import CurrentCoops from conditional.models.models import FreshmanEvalData @@ -17,6 +17,8 @@ from conditional.util.ldap import ldap_get_roomnumber from conditional.util.ldap import ldap_is_active from conditional.util.ldap import ldap_is_onfloor +from conditional.util.ldap import ldap_is_intromember +from conditional.util.ldap import ldap_get_member @service_cache(maxsize=1024) @@ -42,7 +44,7 @@ def get_voting_members(): for intro_member in passed_fall: voting_set.add(intro_member.uid) - voting_list = list(username for username in voting_set if gatekeep_status(username)) + voting_list = list(username for username in voting_set if gatekeep_status(username)[0]["result"]) return voting_list @@ -158,3 +160,82 @@ def req_cm(member): if co_op: return 15 return 30 + +def gatekeep_status(username): + if datetime.today() < datetime(start_of_year().year, 12, 31): + semester = "Fall" + semester_start = datetime(start_of_year().year,6,1) + else: + semester = "Spring" + semester_start = datetime(start_of_year().year + 1,1,1) + + # groups + ldap_member = ldap_get_member(username) + is_intro_member = ldap_is_intromember(ldap_member) + is_active_member = ldap_is_active(ldap_member) and not is_intro_member + + is_on_coop = ( + CurrentCoops.query.filter( + CurrentCoops.date_created > start_of_year(), + CurrentCoops.semester == semester, + CurrentCoops.uid == username, + ).first() + is not None + ) + + passed_fall = ( + FreshmanEvalData.query.filter( + FreshmanEvalData.freshman_eval_result == "Passed", + FreshmanEvalData.eval_date > start_of_year(), + FreshmanEvalData.uid == username, + ).first() + is not None + ) + eligibility_of_groups = (is_active_member and not is_on_coop) or passed_fall + + # number of directorship meetings attended in the current semester + d_meetings = ( + MemberCommitteeAttendance.query.join( + CommitteeMeeting, + MemberCommitteeAttendance.meeting_id == CommitteeMeeting.id, + ) + .filter( + MemberCommitteeAttendance.uid == username, + CommitteeMeeting.approved is True, + CommitteeMeeting.timestamp >= semester_start, + ) + .count() + ) + # number of technical seminars attended in the current semester + t_seminars = ( + MemberSeminarAttendance.query.join( + TechnicalSeminar, + MemberSeminarAttendance.seminar_id == TechnicalSeminar.id, + ) + .filter( + MemberSeminarAttendance.uid == username, + TechnicalSeminar.approved is True, + TechnicalSeminar.timestamp >= semester_start, + ) + .count() + ) + # number of house meetings attended in the current semester + h_meetings = ( + MemberHouseMeetingAttendance.query.join( + HouseMeeting, + MemberHouseMeetingAttendance.meeting_id == HouseMeeting.id, + ) + .filter( + MemberHouseMeetingAttendance.uid == username, + HouseMeeting.date >= semester_start + ) + .count() + ) + result = eligibility_of_groups and (d_meetings >= 6 and t_seminars >= 2 and h_meetings >= 6) + + return { + "result": result, + "h_meetings": h_meetings, + "c_meetings": d_meetings, + "t_seminars": t_seminars, + }, 200 From e6364827d33a22e663fa3153ab45bdef92845ae2 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 21 Oct 2025 12:11:47 -0400 Subject: [PATCH 74/76] move status for gatekeep response to route --- conditional/__init__.py | 4 +--- conditional/util/member.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index 10ee9214..d091e4b4 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -165,9 +165,7 @@ def gatekeep(username): if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 - return gatekeep_status(username) - - + return gatekeep_status(username), 200 @app.errorhandler(404) diff --git a/conditional/util/member.py b/conditional/util/member.py index 9040c6ae..edc3e99d 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -238,4 +238,4 @@ def gatekeep_status(username): "h_meetings": h_meetings, "c_meetings": d_meetings, "t_seminars": t_seminars, - }, 200 + } From 888b082cdb61a9da0fd44519b34b1caa36117fde Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 21 Oct 2025 12:18:55 -0400 Subject: [PATCH 75/76] fix me being dumb :) --- conditional/util/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conditional/util/member.py b/conditional/util/member.py index edc3e99d..15bd1f61 100644 --- a/conditional/util/member.py +++ b/conditional/util/member.py @@ -44,7 +44,7 @@ def get_voting_members(): for intro_member in passed_fall: voting_set.add(intro_member.uid) - voting_list = list(username for username in voting_set if gatekeep_status(username)[0]["result"]) + voting_list = list(username for username in voting_set if gatekeep_status(username)["result"]) return voting_list From b48db4f5eac65253cef7ae7d9193f5ef4268ed51 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 21 Oct 2025 13:15:31 -0400 Subject: [PATCH 76/76] fix 500 error when requesting gatekeep for a user that doesn't exist --- conditional/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/conditional/__init__.py b/conditional/__init__.py index d091e4b4..e8f54a8b 100644 --- a/conditional/__init__.py +++ b/conditional/__init__.py @@ -164,8 +164,12 @@ def gatekeep(username): token = request.headers.get("X-VOTE-TOKEN", "") if token != app.config["VOTE_TOKEN"]: return "Users cannot access this page", 403 + try: + gatekeep_data = gatekeep_status(username) + except KeyError: + return "", 404 - return gatekeep_status(username), 200 + return gatekeep_data, 200 @app.errorhandler(404)