diff --git a/package-lock.json b/package-lock.json index 8a85eaf1f..c581ee3bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -715,29 +715,6 @@ "chalk": "^4.0.0" } }, - "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", - "requires": { - "@nodelib/fs.stat": "2.0.4", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" - }, - "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "requires": { - "@nodelib/fs.scandir": "2.1.4", - "fastq": "^1.6.0" - } - }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -914,22 +891,6 @@ "es6-promisify": "^5.0.0" } }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - } - } - }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -992,11 +953,6 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" - }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -1083,7 +1039,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "binary-extensions": { "version": "2.2.0", @@ -1094,6 +1051,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1224,11 +1182,6 @@ "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", "integrity": "sha1-k1vC39lFiodrJ5YXUUY4vKqWSi4=" }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -1319,7 +1272,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "1.6.2", @@ -1447,31 +1401,6 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, - "del": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", - "requires": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1490,21 +1419,6 @@ "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", "dev": true }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "requires": { - "path-type": "^4.0.0" - }, - "dependencies": { - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - } - } - }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -1533,11 +1447,6 @@ "integrity": "sha512-JTPxdUibkefeomWNaYs8lI/x/Zb4cOhZWX+d7kpzsNKzUd07pNuo/AcHeNJ/qgEchxM1IAxda9aaGUhKN/poOg==", "dev": true }, - "elem-dataset": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/elem-dataset/-/elem-dataset-2.0.0.tgz", - "integrity": "sha512-e7gieGopWw5dMdEgythH3lUS7nMizutPDTtkzfQW/q2gCvFnACyNnK3ytCncAXKxdBXQWcXeKaYTTODiMnp8mw==" - }, "element-closest": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", @@ -1707,19 +1616,6 @@ "yauzl": "^2.10.0" } }, - "fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - } - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1732,14 +1628,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fastq": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", - "integrity": "sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA==", - "requires": { - "reusify": "^1.0.4" - } - }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -1916,7 +1804,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.3.2", @@ -1958,6 +1847,7 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2019,23 +1909,11 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "globby": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", - "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, "graceful-fs": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz", - "integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==" + "integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==", + "dev": true }, "growly": { "version": "1.3.0", @@ -2161,11 +2039,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" - }, "import-local": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", @@ -2186,6 +2059,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2194,7 +2068,8 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "ini": { "version": "1.3.8", @@ -2378,16 +2253,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3348,11 +3213,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3402,20 +3262,6 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "mime": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", @@ -3447,6 +3293,7 @@ "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" } @@ -3589,6 +3436,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -3660,14 +3508,6 @@ "p-limit": "^2.2.0" } }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "requires": { - "aggregate-error": "^3.0.0" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -3695,7 +3535,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "3.1.1", @@ -3942,11 +3783,6 @@ "signal-exit": "^3.0.2" } }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -3962,11 +3798,6 @@ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true }, - "run-parallel": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", - "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==" - }, "rxjs": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", @@ -4076,7 +3907,8 @@ "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true }, "source-map": { "version": "0.6.1", @@ -4369,15 +4201,12 @@ "dev": true }, "uswds": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/uswds/-/uswds-2.10.0.tgz", - "integrity": "sha512-A6HjL42ryf9pdfbm6JrGxZywKzCZraHO4v3Ve21uFDqoA3ijoNjiSYME+3SG86CIgC6zRasrbQVuI3bvd3Xv2w==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/uswds/-/uswds-2.12.2.tgz", + "integrity": "sha512-Y9YeILoe4xiH26JpyznSbIsC8osHY88QjTFM3K5r7GcSuryauu7CpPnQdBUCw5R7myc24N5X8S/nxnXvOEtudg==", "requires": { "classlist-polyfill": "^1.0.3", - "del": "^6.0.0", "domready": "^1.0.8", - "elem-dataset": "^2.0.0", - "lodash.debounce": "^4.0.7", "object-assign": "^4.1.1", "receptor": "^1.0.0", "resolve-id-refs": "^0.1.0" @@ -4559,7 +4388,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "3.0.3", diff --git a/package.json b/package.json index 255a1f86f..d43b5bb3d 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "chosen-js": "^1.8.7", "jquery": "^3.6.0", "sass": "^1.35.1", - "uswds": "^2.10.0" + "uswds": "^2.12.2" }, "devDependencies": { "jest": "^27.0.6", diff --git a/tock/api/tests.py b/tock/api/tests.py index 31d4f27bc..b4582c327 100644 --- a/tock/api/tests.py +++ b/tock/api/tests.py @@ -180,6 +180,90 @@ def test_get_unsubmitted_timecards(self): ) self.assertEqual(len(queryset), 2) +""" + Adding data to the timecards.json fixture results in failing tests since many tests + assert on the length of a list returned. You can add tests here by creating mock data + inside of setUp() and not worry about breaking existing tests that rely on the timecard + fixture +""" +class FixturelessTimecardsAPITests(WebTest): + def setUp(self): + super(FixturelessTimecardsAPITests, self).setUp() + self.user = UserFactory() + self.userdata = UserData.objects.create(user=self.user) + self.billable_code = AccountingCodeFactory(billable=True) + self.weekly_billed_project = ProjectFactory(accounting_code=self.billable_code,is_weekly_bill=True) + self.period1 = ReportingPeriodFactory(start_date=datetime.datetime(2021, 9, 1)) + self.period2 = ReportingPeriodFactory(start_date=datetime.datetime(2021, 9, 8)) + self.period3 = ReportingPeriodFactory(start_date=datetime.datetime(2021, 9, 14)) + self.period4 = ReportingPeriodFactory(start_date=datetime.datetime(2021, 9, 21)) + self.period5 = ReportingPeriodFactory(start_date=datetime.datetime(2021, 9, 29)) + self.full_allocation_timecard = TimecardFactory(user=self.user, reporting_period=self.period1) + self.three_quarter_allocation_timecard = TimecardFactory(user=self.user, reporting_period=self.period2) + self.half_allocation_timecard = TimecardFactory(user=self.user, reporting_period=self.period3) + self.one_quarter_allocation_timecard = TimecardFactory(user=self.user, reporting_period=self.period4) + self.one_eighth_allocation_timecard = TimecardFactory(user=self.user, reporting_period=self.period5) + self.full_allocation_timecard_objects = [ + TimecardObjectFactory( + timecard=self.full_allocation_timecard, + project=self.weekly_billed_project, + hours_spent=0, + project_allocation=1.000 + ) + ] + self.three_quarter_allocation_timecard_objects = [ + TimecardObjectFactory( + timecard=self.three_quarter_allocation_timecard, + project=self.weekly_billed_project, + hours_spent=0, + project_allocation=0.750 + ) + ] + self.half_allocation_timecard_objects = [ + TimecardObjectFactory( + timecard=self.half_allocation_timecard, + project=self.weekly_billed_project, + hours_spent=0, + project_allocation=0.500 + ) + ] + self.one_quarter_allocation_timecard_objects = [ + TimecardObjectFactory( + timecard=self.one_quarter_allocation_timecard, + project=self.weekly_billed_project, + hours_spent=0, + project_allocation=0.250 + ) + ] + self.one_eighth_allocation_timecard_objects = [ + TimecardObjectFactory( + timecard=self.one_eighth_allocation_timecard, + project=self.weekly_billed_project, + hours_spent=0, + project_allocation=0.125 + ) + ] + + def test_project_allocation_scale_precision(self): + """ + project_allocation allows a scale of 6 digits and a precision of 3 digits + Test to make sure that the API, which relies on TimecardSerializer, follows this convention + """ + all_timecards = client().get( + reverse('TimecardList'), + kwargs={'date': '2021-09-01'}).data + + full_allocation_timecard = all_timecards[0] + three_quarter_allocation_timecard = all_timecards[1] + half_allocation_timecard = all_timecards[2] + one_quarter_allocation_timecard = all_timecards[3] + one_eighth_allocation_timecard = all_timecards[4] + + self.assertEqual(full_allocation_timecard['project_allocation'], "1.000") + self.assertEqual(three_quarter_allocation_timecard['project_allocation'], "0.750") + self.assertEqual(half_allocation_timecard['project_allocation'], "0.500") + self.assertEqual(one_quarter_allocation_timecard['project_allocation'], "0.250") + self.assertEqual(one_eighth_allocation_timecard['project_allocation'], "0.125") class TestAggregates(WebTest): diff --git a/tock/api/views.py b/tock/api/views.py index 92d0bf980..df13d5be3 100644 --- a/tock/api/views.py +++ b/tock/api/views.py @@ -87,7 +87,7 @@ class TimecardSerializer(serializers.Serializer): allow_null=True ) hours_spent = serializers.DecimalField(max_digits=5, decimal_places=2) - project_allocation = serializers.DecimalField(max_digits=5, decimal_places=2) + project_allocation = serializers.DecimalField(max_digits=6, decimal_places=3) start_date = serializers.DateField( source='timecard.reporting_period.start_date' ) diff --git a/tock/employees/migrations/0037_small_allocation.py b/tock/employees/migrations/0037_small_allocation.py new file mode 100644 index 000000000..f4453f756 --- /dev/null +++ b/tock/employees/migrations/0037_small_allocation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-09-29 19:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('employees', '0036_alter_userdata_expected_project_allocation'), + ] + + operations = [ + migrations.AlterField( + model_name='userdata', + name='expected_project_allocation', + field=models.DecimalField(blank=True, decimal_places=3, default=1.0, help_text='Enter in Decimal Format (ex. 1.00 = 100%, 0.50 = 50%)', max_digits=6, null=True, verbose_name='Expected Project Allocation'), + ), + ] diff --git a/tock/employees/models.py b/tock/employees/models.py index 088758112..1bc053fac 100644 --- a/tock/employees/models.py +++ b/tock/employees/models.py @@ -85,8 +85,8 @@ class UserData(models.Model): default=settings.DEFAULT_EXPECTED_BILLABLE_HOURS, help_text="Number of hours expected to be billable in a 40 hour work week") expected_project_allocation = models.DecimalField( - decimal_places=2, - max_digits=5, + decimal_places=3, + max_digits=6, blank=True, null=True, default=settings.DEFAULT_EXPECTED_PROJECT_ALLOCATION, diff --git a/tock/hours/admin.py b/tock/hours/admin.py index e22d5492e..caa15a7b6 100644 --- a/tock/hours/admin.py +++ b/tock/hours/admin.py @@ -1,9 +1,11 @@ from decimal import Decimal +from math import isclose from django.conf import settings from django.contrib import admin from django.core.exceptions import ValidationError from django.forms import DecimalField, ModelForm +from django.forms.widgets import Select from django.forms.models import BaseInlineFormSet from employees.models import UserData @@ -24,8 +26,79 @@ def queryset(self, request, queryset): return queryset +def safe_float(value): + """Convert a string to a float with no exceptions. + + Return NaN if the conversion fails. + """ + try: + return float(value) + except ValueError: + return float("NaN") + + +class DecimalChoiceWidget(Select): + + """A choice widget for decimal typed data. + + The Select widget when used with a form's TypedChoiceField that has an + underlying `DecimalField` in the model is very sensitive to formatting because + it uses string comparison (as is natural for a Select widget on a web page that + deals in strings). + + This modifies the method in a `Select` widget where equality is checked so that + it uses numerical equality. This should make it much better behaved with the + numeric data type. + """ + + def optgroups(self, name, value, attrs=None): + """Change the baseclass's method to use numerical comparison.""" + groups = [] + has_selected = False + + for index, (option_value, option_label) in enumerate(self.choices): + if option_value is None: + option_value = '' + + subgroup = [] + if isinstance(option_label, (list, tuple)): + group_name = option_value + subindex = 0 + choices = option_label + else: + group_name = None + subindex = None + choices = [(option_value, option_label)] + groups.append((group_name, subgroup, index)) + + for subvalue, sublabel in choices: + selected = ( + # instead of string comparison, use numerical comparison here + any(isclose(safe_float(subvalue), safe_float(v)) for v in value) + and (not has_selected or self.allow_multiple_selected) + ) + has_selected |= selected + subgroup.append(self.create_option( + name, subvalue, sublabel, selected, index, + subindex=subindex, attrs=attrs, + )) + if subindex is not None: + subindex += 1 + return groups + + +class TimecardObjectForm(ModelForm): + + class Meta: + model = TimecardObject + fields = "__all__" + widgets = {"project_allocation": DecimalChoiceWidget} + class TimecardObjectFormset(BaseInlineFormSet): + + form = TimecardObjectForm + def clean(self): """ Check to ensure the proper number of hours are entered. @@ -37,6 +110,7 @@ def clean(self): return hours = Decimal(0.0) + weekly_billing_found = False aws_eligible = UserData.objects.get( user__id=self.instance.user_id).is_aws_eligible min_working_hours = self.instance.reporting_period.min_working_hours @@ -44,7 +118,12 @@ def clean(self): for unit in self.cleaned_data: try: - hours = hours + unit['hours_spent'] + if unit['hours_spent']: + hours = hours + unit['hours_spent'] + else: + if (unit['project_allocation'] and unit['project_allocation'] > 0): + weekly_billing_found = True + break except KeyError: pass @@ -53,7 +132,7 @@ def clean(self): 'You have entered more than %s hours' % max_working_hours ) - if hours < min_working_hours and not aws_eligible: + if hours < min_working_hours and not aws_eligible and not weekly_billing_found: raise ValidationError( 'You have entered fewer than %s hours' % min_working_hours ) @@ -67,6 +146,7 @@ class ReportingPeriodAdmin(admin.ModelAdmin): class TimecardObjectInline(admin.TabularInline): formset = TimecardObjectFormset + form = TimecardObjectForm model = TimecardObject readonly_fields = [ 'grade', diff --git a/tock/hours/forms.py b/tock/hours/forms.py index 9905cb127..5f24f60c7 100644 --- a/tock/hours/forms.py +++ b/tock/hours/forms.py @@ -11,6 +11,7 @@ from projects.models import AccountingCode, Project from .models import ReportingPeriod, Timecard, TimecardObject +from .admin import DecimalChoiceWidget class ReportingPeriodForm(forms.ModelForm): @@ -143,7 +144,7 @@ class TimecardObjectForm(forms.ModelForm): project_allocation = forms.ChoiceField( choices=settings.PROJECT_ALLOCATION_CHOICES, required=False, - widget=forms.Select(attrs={'onchange' : "populateHourTotals();"}) + widget=DecimalChoiceWidget(attrs={'onchange' : "populateHourTotals();"}) ) hours_spent = forms.DecimalField( min_value=0, diff --git a/tock/hours/migrations/0063_small_allocation.py b/tock/hours/migrations/0063_small_allocation.py new file mode 100644 index 000000000..e6ad82cf8 --- /dev/null +++ b/tock/hours/migrations/0063_small_allocation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-09-29 19:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hours', '0062_timecardobject_project_allocation'), + ] + + operations = [ + migrations.AlterField( + model_name='timecardobject', + name='project_allocation', + field=models.DecimalField(choices=[(0, '---'), (1.0, '100%'), (0.5, '50%'), (0.25, '25%'), (0.125, '12.5%')], decimal_places=2, default=0, max_digits=5), + ), + ] diff --git a/tock/hours/migrations/0064_small_allocation2.py b/tock/hours/migrations/0064_small_allocation2.py new file mode 100644 index 000000000..7be18d0ad --- /dev/null +++ b/tock/hours/migrations/0064_small_allocation2.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-09-30 17:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hours', '0063_small_allocation'), + ] + + operations = [ + migrations.AlterField( + model_name='timecardobject', + name='project_allocation', + field=models.DecimalField(choices=[(0, '---'), (1.0, '100%'), (0.5, '50%'), (0.25, '25%'), (0.125, '12.5%')], decimal_places=3, default=0, max_digits=6), + ), + ] diff --git a/tock/hours/models.py b/tock/hours/models.py index 06b3400a1..412d55ad6 100644 --- a/tock/hours/models.py +++ b/tock/hours/models.py @@ -476,8 +476,8 @@ class TimecardObject(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) grade = models.ForeignKey(EmployeeGrade, blank=True, null=True, on_delete=models.CASCADE) - project_allocation = models.DecimalField(choices=settings.PROJECT_ALLOCATION_CHOICES, default=0, decimal_places=2, - max_digits=5) + project_allocation = models.DecimalField(choices=settings.PROJECT_ALLOCATION_CHOICES, default=0, decimal_places=3, + max_digits=6) # The notes field is where the user records notes about time spent on # certain projects (for example, time spent on general projects). It may # only be display and required when certain projects are selected. diff --git a/tock/hours/tests/test_forms.py b/tock/hours/tests/test_forms.py index 444370c99..5a3093d01 100644 --- a/tock/hours/tests/test_forms.py +++ b/tock/hours/tests/test_forms.py @@ -365,3 +365,10 @@ def test_one_project_with_notes_and_one_without_notes_is_valid(self): form_data['timecardobjects-0-notes'] = 'Did some work.' formset = TimecardFormSet(form_data, instance=self.timecard) self.assertTrue(formset.is_valid()) + + def test_smallest_project_allocation(self): + """Should be able to make a timecard with 12.5% project allocation""" + form_data = self.form_data() + form_data['timecardobjects-0-project_allocation'] = '0.125' + formset = TimecardFormSet(form_data, instance=self.timecard) + self.assertTrue(formset.is_valid()) diff --git a/tock/hours/tests/test_integration.py b/tock/hours/tests/test_integration.py index 47b128cc7..d29e04237 100644 --- a/tock/hours/tests/test_integration.py +++ b/tock/hours/tests/test_integration.py @@ -3,6 +3,7 @@ from django.urls import reverse from django_webtest import WebTest +from webtest.forms import Hidden from test_common import ProtectedViewTestCase @@ -25,7 +26,7 @@ def setUp(self): ) self.projects = [ projects.models.Project.objects.get(name='openFEC'), - projects.models.Project.objects.get(name='Peace Corps'), + projects.models.Project.objects.get(name='Peace Corps') ] self.reporting_period = hours.models.ReportingPeriod.objects.create( start_date=datetime.date(2015, 1, 1), @@ -45,6 +46,11 @@ def setUp(self): user=self.user, reporting_period=self.reporting_period2, ) + self.reporting_period3 = hours.models.ReportingPeriod.objects.create( + start_date=datetime.date(2015, 1, 15), + end_date=datetime.date(2015, 1, 21), + exact_working_hours=40, + ) def _assert_project_options(self, positive=None, negative=None): """Browse to timecard update page, then assert that positive options are @@ -155,3 +161,76 @@ def test_timecard_submit_twice(self): # successful POST will give a 302 redirect self.assertEqual(res.status_code, 302) + + def test_timecard_save_weekly_bill(self): + # Make a new timecard so we can save it + hours.models.Timecard.objects.create( + user=self.user, + reporting_period=self.reporting_period3, + ) + date = self.reporting_period3.start_date.strftime('%Y-%m-%d') + url = reverse('reportingperiod:UpdateTimesheet', + kwargs={'reporting_period': date},) + res = self.app.get(url, user=self.user) + form = res.form # only one form on the page + + weekly_billed_project = projects.models.Project.objects.get(name='Weekly Billing') + form["timecardobjects-0-project"] = weekly_billed_project.id + form["timecardobjects-0-hours_spent"] = 0 + form["timecardobjects-0-project_allocation"] = "0.5" + + # normally added by JS in a browser, we add the save_only field manually here + # technique from https://stackoverflow.com/a/23877996 + field = Hidden(form, "input", None, 999, "1") + form.fields["save_only"] = [field] + form.field_order.append(("save_only", field)) + + page = form.submit().follow() # follow the 302 redirect on success + + # The project allocation is properly displayed + self.assertEqual(page.form["timecardobjects-0-project_allocation"].value, "0.5") + + + +class TestAdmin(ProtectedViewTestCase, WebTest): + + fixtures = [ + 'projects/fixtures/projects.json' + ] + + setUp = TestOptions.setUp + + def test_admin_weekly_bill_timecard_submit(self): + """Test a weekly billed project via the admin interface""" + weekly_billed_project = projects.models.Project.objects.get(name='Weekly Billing') + url = reverse('admin:hours_timecard_add') + res = self.app.get(url, user=self.user) + form = res.form + form["user"] = self.user.id + form["reporting_period"] = self.reporting_period3.id + form["timecardobjects-0-project"] = weekly_billed_project.id + form["timecardobjects-0-project_allocation"] = "1.0" + res = form.submit("submit-timecard").follow() + self.assertEqual(res.status_code, 200) + self.assertContains(res, "was added successfully") + + def test_admin_weekly_bill_project_allocation(self): + """Test project allocation in the admin interface""" + weekly_billed_project = projects.models.Project.objects.get(name='Weekly Billing') + timecard = hours.models.Timecard.objects.first() + change_url = reverse( + 'admin:hours_timecard_change', + args=[timecard.id], + ) + + # save a timecard with project allocation set. + form = self.app.get(change_url, user=self.user).form + form["timecardobjects-0-project"] = weekly_billed_project.id + form["timecardobjects-0-hours_spent"] = '' + form["timecardobjects-0-project_allocation"] = "1.0" + form.submit("submit-timecard") + + # now visit the change page and make sure that 100% is selected + change_page = self.app.get(change_url, user=self.user) + form = change_page.form + self.assertEqual(form["timecardobjects-0-project_allocation"].value, "1.0") diff --git a/tock/projects/fixtures/projects.json b/tock/projects/fixtures/projects.json index 05d558bb5..2aa47b053 100644 --- a/tock/projects/fixtures/projects.json +++ b/tock/projects/fixtures/projects.json @@ -684,5 +684,15 @@ "accounting_code":15 }, "pk":50 + }, + { + "model":"projects.project", + "fields":{ + "name":"Weekly Billing", + "description":"", + "accounting_code":15, + "is_weekly_bill": true + }, + "pk":51 } ] diff --git a/tock/tock/templates/hours/timecard_form.html b/tock/tock/templates/hours/timecard_form.html index 598a69d9b..45fcf584b 100644 --- a/tock/tock/templates/hours/timecard_form.html +++ b/tock/tock/templates/hours/timecard_form.html @@ -62,15 +62,6 @@

{{ timecard_note.title }}

{% endfor %} -
-
-

- For additional help and guidance with using Tock and entering hours, please consult the TTS Handbook page on Tock. -

-
-
-
{% csrf_token %} @@ -181,4 +172,4 @@

{{ timecard_note.title }}

let objectId = {{ object.id }}; -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tock/utilization/tests/test_views.py b/tock/utilization/tests/test_views.py index 204fabfc6..54cf6affb 100644 --- a/tock/utilization/tests/test_views.py +++ b/tock/utilization/tests/test_views.py @@ -43,7 +43,7 @@ def setUp(self): non_billable_project.accounting_code = non_billable_acct non_billable_project.save() - billable_project = Project.objects.last() + billable_project = Project.objects.get(pk=50) billable_project.accounting_code = billable_acct billable_project.save() @@ -60,8 +60,21 @@ def setUp(self): self.user_data.unit = test_unit self.user_data.save() + # These utilization tests get weird around fiscal years, this is an attempt + # to handle things better inside of the first week to 10 days of October + today = datetime.date.today() + current_fy = ReportingPeriod.get_fiscal_year_from_date(today) + fy_start_date = ReportingPeriod.get_fiscal_year_start_date(current_fy) + safe_date = fy_start_date + datetime.timedelta(days=7) + adjust_rp_start_date_for_fy_boundary = today < safe_date + + if adjust_rp_start_date_for_fy_boundary: + rp_start_date = fy_start_date + else: + rp_start_date = datetime.date.today() - datetime.timedelta(days=7) + self.reporting_period = ReportingPeriod.objects.create( - start_date=datetime.date.today() - datetime.timedelta(days=7), + start_date=rp_start_date, end_date=datetime.date.today() ) diff --git a/tock/utilization/utils.py b/tock/utilization/utils.py index cb4624933..dce59d187 100644 --- a/tock/utilization/utils.py +++ b/tock/utilization/utils.py @@ -77,8 +77,8 @@ def limit_to_fy(): """ Filter component to Limit timecard aggregation to the current fiscal year """ - current_fy = ReportingPeriod().get_fiscal_year_from_date(datetime.date.today()) - fy_start_date = ReportingPeriod().get_fiscal_year_start_date(current_fy) + current_fy = ReportingPeriod.get_fiscal_year_from_date(datetime.date.today()) + fy_start_date = ReportingPeriod.get_fiscal_year_start_date(current_fy) return Q(timecards__submitted=True, timecards__reporting_period__start_date__gte=fy_start_date) def _get_reporting_periods(count):