diff options
36 files changed, 749 insertions, 931 deletions
diff --git a/.hadolint.yaml b/.hadolint.yaml index 5a0a0197..c3c3449b 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -1,4 +1,4 @@ ignored: - # Ignore suggestion for pinned versions in `apt-get´ and `apk` - - DL3008 - - DL3018 + - DL3008 # Ignore suggestion for pinned versions in `apt-get`... + - DL3013 # ... and `pip`. + - DL3018 # ... and `apk`. @@ -23,7 +23,6 @@ django-filter = "~=2.1.0" django-hosts = "~=3.0" djangorestframework = "~=3.9.2" djangorestframework-bulk = "~=0.2.1" -uwsgi = "~=2.0.18" psycopg2-binary = "~=2.8" django-simple-bulma = ">=1.1.6,<2.0" django-crispy-bulma = ">=0.1.2,<2.0" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 0adf7124..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,431 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "bc1a4e553aca43f2682f1954ae00b54413613646a20585c3007c121960a86b36" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", - "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" - ], - "version": "==2019.3.9" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "django": { - "hashes": [ - "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", - "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" - ], - "index": "pypi", - "version": "==2.2" - }, - "django-crispy-bulma": { - "hashes": [ - "sha256:0d982e217a95706e0bbecd9f43990c191b071a20287478c7847ff096567e6e64", - "sha256:2067cce1f481f9f6fcbcde86eb314eb4d5786e5a955907e1fd8359f319191b91" - ], - "index": "pypi", - "version": "==0.1.2" - }, - "django-crispy-forms": { - "hashes": [ - "sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f", - "sha256:705ededc554ad8736157c666681165fe22ead2dec0d5446d65fc9dd976a5a876" - ], - "index": "pypi", - "version": "==1.7.2" - }, - "django-environ": { - "hashes": [ - "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde", - "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4" - ], - "index": "pypi", - "version": "==0.4.5" - }, - "django-filter": { - "hashes": [ - "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", - "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "django-hosts": { - "hashes": [ - "sha256:3599645f37b4c51df6140d659bef356e05ae7ff7748f8fef14c2c84083dd8089", - "sha256:8e83232dbd7ff0d9de5c814f16bdf4cd1971bd00c54fa1f3e507aed4f93215a8" - ], - "index": "pypi", - "version": "==3.0" - }, - "django-simple-bulma": { - "hashes": [ - "sha256:420042e26dd4bc70b148fc721bd77a48130b62b3d91b977e56f8232a4cfac555", - "sha256:beb323aa1c51e5efee398b6e5a2b481b31a0be7505e3118d140c65349f637100" - ], - "index": "pypi", - "version": "==1.1.6" - }, - "djangorestframework": { - "hashes": [ - "sha256:8a435df9007c8b7d8e69a21ef06650e3c0cbe0d4b09e55dd1bd74c89a75a9fcd", - "sha256:f7a266260d656e1cf4ca54d7a7349609dc8af4fe2590edd0ecd7d7643ea94a17" - ], - "index": "pypi", - "version": "==3.9.2" - }, - "djangorestframework-bulk": { - "hashes": [ - "sha256:39230d8379acebd86d313df6c9150cafecb636eae1d097c30a26389ab9fee5b1" - ], - "index": "pypi", - "version": "==0.2.1" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "libsass": { - "hashes": [ - "sha256:2ae3b061a7d250fb47e5fdad1a8191600ca15dc604e76b109b6d3bf8e08fd2ed", - "sha256:2ee186aa682a035a53c557b7e61ce562a1114f1a1a992d0ba962cbc3e82c490c", - "sha256:366f4fd5a5eab4a519beb583e9fa78718cf2c0f40e92ed835d7ed23b82e5d954", - "sha256:5511b3c62e8d97daf929c63bd516b794f0a06acd09dd261445d864e48290551b", - "sha256:7462da168c8fb997b31cb4dc3ee5adb9af2d106f7b92c2d57a1c68a56ae5a3a0", - "sha256:84a16ec5cf7842ff5bc2caed2c032ed624d587699797bc2a4d4a8e41f579b6e7", - "sha256:8fc0360ee99224f7a3cb09987e641171d34180759f467ba3d15934102ade396f", - "sha256:a6c5535a21a07d769151453270bc6a8373b821d1d2fd9810d84fccfe315ab188", - "sha256:b375bfbf3c86ec0f4a27f266b44b2753a4b8cab7e73649eed7afcad84bc56257", - "sha256:b548af46c1a606aed93da2566901146005d6065f73fefc63d256ba62ba1f803d", - "sha256:bb30fc7125350c64925a98cb90da7979f76bb0ea1a0157e8aeb268f8da38e296", - "sha256:c2f386677514f9fc758631328bd318dd3e9d839ad7b6e248ec4535a191bfd271", - "sha256:d1f301637ad5768aecc81d17dcf40a68f2e11b7ca8b427dbb9f8972c150d303e", - "sha256:e0cf54dddf2cc6e373005bed6e46ccdce1f3a77bd169ab505c3a8ad9023eee5f", - "sha256:e8941881063691d50f9cc8b8d6d8fd7bec86a8c461b2a4fc87188a5fc44d6ba4", - "sha256:f4b29b0c70d753c754a58aaad7c31ad3309ca4a26f9aa64e695157251f6832ad" - ], - "version": "==0.18.0" - }, - "psycopg2-binary": { - "hashes": [ - "sha256:163d3ee445a0b4c0109877da9e46271aacf4e5e3d60ae7368669555c30f13e7c", - "sha256:1af0bfe7b0c13a0e613a27311fd4f9c5d024e8fc0f4b3d284e7df02a58a11fc0", - "sha256:2169c3a1bf52d5b30cc98625b5919a964c571a32e8646be20be6c7e3e82079de", - "sha256:218f079fa48e2ef812dc3d3ce6ec2f67ac56427ba4b038d5d6331f2cceb489c2", - "sha256:26a958930687e94c4c6c73c171e4d4783b82ae4e16aa3424e6bcd4529bceedf0", - "sha256:2c7c195aef3acdbc853942bc674844031a732890d2fee88a324298ed376b6c2b", - "sha256:2ecdbfed7004669472bfa27c8d51012c717c241c7154ae17e4c8f93024043525", - "sha256:345fc31b71a90ada1b51826537917b19a1af685a91c0f066787069c184d7d00f", - "sha256:378a06649503f548be5f1e9eec2e94cc1d6138250b82a08dcc6151bca8cec107", - "sha256:3f300bf2930e501dde09605de85cb2b84c2638e2c954be02a3c86f28176d3525", - "sha256:6c2f66c653ce8bbd7e789d0f7f92c3f9fea881b55226f0ae5ee550cce9e3cf0e", - "sha256:6fccbac2633831b877a8fbf865f7082d34895e82a015795a9f80f99a2efe2576", - "sha256:7a166f8ccb6888358d3e67795b057540ea7caa71ab9e089b0cb0097f01088965", - "sha256:8f6b84f887ec6fef6c1796779f8ec2603dc7e9ef52bc9269de719d4bcbdaebbb", - "sha256:92cf3ceb7bb90cf35b8bd993c640b15d4832ba0e142a3b9da5006ef217da595d", - "sha256:a20dfdf73f56da674926a3811929cff9fd23b9af90be9a6c36ac246a3486eef3", - "sha256:a84415df4689251556c961e4fe3b25d30e32f00faa8064ce0909458dbe0d67b2", - "sha256:ab1aa1cd50df3860f624c9713ee9e690eefd4e049d3a4d86577bab6e741e9616", - "sha256:abc9dcf85e75a8687f2a6d560c0c1a2593e8e34ba6f9ad6721f8212c5de179a2", - "sha256:c10454710a81a2f4b1ff4d1c83ac2cec63e0e55845a56324991514af5b1299d0", - "sha256:c38f80719e4dfae7a6311a4f091f07f4fb2fb5d602352015d5639f63f8fabb68", - "sha256:d75cf00605630b2cfefa5c62373c605dcda1cc0d607902847dbb8e8e9b67c1ce", - "sha256:dce15cb6ef604c9e38fdaa848f58f83153ade9f4aa5e4cf5812aa27163561750", - "sha256:e7e0db4311bb76bf3f6e0380f71912cfa6d0be7cc635e3772476050b0dabdabd", - "sha256:eac59cae78dfe3fbf7ece25c170d7a152f88df7643381aa5e7344c2028a8d8d4", - "sha256:ead7b3e1567bd14cacd44279c5e42cd19f54b9feed39180220253f4fbe3abd56", - "sha256:ed772a5e8e7e5dd6bede960a86940c17cf653c7f158dafa5d52e919b676f10ba", - "sha256:f2d73131acb94afa45de8b6b8a4bfb21bbe3736633d6478e53247f19dd8c299c" - ], - "index": "pypi", - "version": "==2.8.1" - }, - "pytz": { - "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" - ], - "version": "==2019.1" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "sqlparse": { - "hashes": [ - "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", - "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" - ], - "version": "==0.3.0" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - }, - "uwsgi": { - "hashes": [ - "sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583" - ], - "index": "pypi", - "version": "==2.0.18" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" - ], - "version": "==19.1.0" - }, - "bandit": { - "hashes": [ - "sha256:6102b5d6afd9d966df5054e0bdfc2e73a24d0fea400ec25f2e54c134412158d7", - "sha256:9413facfe9de1e1bd291d525c784e1beb1a55c9916b51dae12979af63a69ba4c" - ], - "version": "==1.5.1" - }, - "coverage": { - "hashes": [ - "sha256:0c5fe441b9cfdab64719f24e9684502a59432df7570521563d7b1aff27ac755f", - "sha256:2b412abc4c7d6e019ce7c27cbc229783035eef6d5401695dccba80f481be4eb3", - "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", - "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", - "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", - "sha256:42692db854d13c6c5e9541b6ffe0fe921fe16c9c446358d642ccae1462582d3b", - "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", - "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", - "sha256:4ec30ade438d1711562f3786bea33a9da6107414aed60a5daa974d50a8c2c351", - "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", - "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", - "sha256:6899797ac384b239ce1926f3cb86ffc19996f6fa3a1efbb23cb49e0c12d8c18c", - "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", - "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", - "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", - "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", - "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", - "sha256:8e679d1bde5e2de4a909efb071f14b472a678b788904440779d2c449c0355b27", - "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", - "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", - "sha256:93f965415cc51604f571e491f280cff0f5be35895b4eb5e55b47ae90c02a497b", - "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", - "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", - "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", - "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", - "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", - "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", - "sha256:a9abc8c480e103dc05d9b332c6cc9fb1586330356fc14f1aa9c0ca5745097d19", - "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", - "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", - "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", - "sha256:c22ab9f96cbaff05c6a84e20ec856383d27eae09e511d3e6ac4479489195861d", - "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", - "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", - "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", - "sha256:ca58eba39c68010d7e87a823f22a081b5290e3e3c64714aac3c91481d8b34d22", - "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", - "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", - "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", - "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", - "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" - ], - "index": "pypi", - "version": "==4.5.3" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, - "flake8": { - "hashes": [ - "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", - "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" - ], - "index": "pypi", - "version": "==3.7.7" - }, - "flake8-bandit": { - "hashes": [ - "sha256:5eac24fa9fef532e4e4ce599c5b3c5248c5cc435d2927537b529b0a7bcb72467", - "sha256:be5840923ccf06cac6a8893a2f0abc17f03b6b9fdb5284d796f722b69c8f840b" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "flake8-bugbear": { - "hashes": [ - "sha256:5070774b668be92c4312e5ca82748ddf4ecaa7a24ff062662681bb745c7896eb", - "sha256:fef9c9826d14ec23187ae1edeb3c6513c4e46bf0e70d86bac38f7d9aabae113d" - ], - "index": "pypi", - "version": "==19.3.0" - }, - "flake8-import-order": { - "hashes": [ - "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", - "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92" - ], - "index": "pypi", - "version": "==0.18.1" - }, - "flake8-polyfill": { - "hashes": [ - "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", - "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" - ], - "version": "==1.0.2" - }, - "flake8-string-format": { - "hashes": [ - "sha256:68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", - "sha256:774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1" - ], - "index": "pypi", - "version": "==0.2.3" - }, - "flake8-tidy-imports": { - "hashes": [ - "sha256:1c476aabc6e8db26dc75278464a3a392dba0ea80562777c5f13fd5cdf2646154", - "sha256:b3f5b96affd0f57cacb6621ed28286ce67edaca807757b51227043ebf7b136a1" - ], - "index": "pypi", - "version": "==2.0.0" - }, - "gitdb2": { - "hashes": [ - "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2", - "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a" - ], - "version": "==2.0.5" - }, - "gitpython": { - "hashes": [ - "sha256:563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82", - "sha256:8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8" - ], - "version": "==2.1.11" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "index": "pypi", - "version": "==0.6.1" - }, - "pbr": { - "hashes": [ - "sha256:8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843", - "sha256:8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824" - ], - "version": "==5.1.3" - }, - "pep8-naming": { - "hashes": [ - "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", - "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb" - ], - "index": "pypi", - "version": "==0.8.2" - }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, - "pyyaml": { - "hashes": [ - "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", - "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", - "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", - "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", - "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", - "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", - "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", - "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", - "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", - "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", - "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" - ], - "version": "==5.1" - }, - "six": { - "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ], - "version": "==1.12.0" - }, - "smmap2": { - "hashes": [ - "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", - "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" - ], - "version": "==2.0.5" - }, - "stevedore": { - "hashes": [ - "sha256:7be098ff53d87f23d798a7ce7ae5c31f094f3deb92ba18059b1aeb1ca9fec0a0", - "sha256:7d1ce610a87d26f53c087da61f06f9b7f7e552efad2a7f6d2322632b5f932ea2" - ], - "version": "==1.30.1" - }, - "unittest-xml-reporting": { - "hashes": [ - "sha256:140982e4b58e4052d9ecb775525b246a96bfc1fc26097806e05ea06e9166dd6c", - "sha256:d1fbc7a1b6c6680ccfe75b5e9701e5431c646970de049e687b4bb35ba4325d72" - ], - "index": "pypi", - "version": "==2.5.1" - } - } -} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 78bbffae..35fb9f82 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,8 +8,10 @@ jobs: steps: - script: | + set -eux + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install docker-ce displayName: install docker diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 93d0c378..5b965740 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -28,7 +28,8 @@ RUN rm -r /opt/bitnami/python/lib/python3.*/site-packages/setuptools* && \ pip install --no-cache-dir -U setuptools RUN python3 -m pip install pipenv \ - && python3 -m pipenv install --dev --system --deploy + && python3 -m pipenv install --system --deploy \ + && pip install uwsgi==2.0.18 COPY . . diff --git a/docs/setup.md b/docs/setup.md index d6e5a7bf..18f5ee97 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -26,6 +26,8 @@ in your environment variables: export DATABASE_URL=postgres://pysite@localhost/pysite ``` +After this step, inside the `.env` file, set the `SECRET_KEY` variable which can be anything you like. + A simpler approach to automatically configuring this might come in the near future - if you have any suggestions, please let us know! @@ -66,6 +68,26 @@ pip install -e .[lint,test] to install base dependencies along with lint and test dependencies. -You can either use `python manage.py` directly, or you can use the console -entrypoint for it, `psmgr`. For example, to run tests, you could use either -`python manage.py test` or `psmgr test`. Happy hacking! +To run tests, use `python manage.py test`. + +## Hosts file + +Make sure you add the following to your hosts file: + +```sh +127.0.0.1 pythondiscord.local +127.0.0.1 api.pythondiscord.local +127.0.0.1 staff.pythondiscord.local +127.0.0.1 admin.pythondiscord.local +127.0.0.1 wiki.pythondiscord.local +127.0.0.1 ws.pythondiscord.local +``` +When trying to access the site, you'll be using the domains above instead of the usual `localhost:8000`. + +Finally, you will need to set the environment variable `DEBUG=1`. When using `pipenv`, you can +set put this into an `.env` file to have it exported automatically. It's also recommended to +export `LOG_LEVEL=INFO` when using `DEBUG=1` if you don't want super verbose logs. + +To run the server, run `python manage.py runserver`. If it gives you an error saying +`django.core.exceptions.ImproperlyConfigured: Set the DATABASE_URL environment variable` please make sure the server that your postgres database is located at is running +and run the command `$(export cat .env)`. Happy hacking! diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py index ea8f03d2..d53ddb90 100644 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py @@ -1,6 +1,6 @@ # Generated by Django 2.1.1 on 2018-09-23 10:07 -import pydis_site.apps.api.validators +import pydis_site.apps.api.models.bot.tag import django.contrib.postgres.fields.jsonb from django.db import migrations @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='tag', name='embed', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.validators.validate_tag_embed]), + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), ), ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index f451ecf4..4b028f0c 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -1,7 +1,6 @@ # Generated by Django 2.1.1 on 2018-11-18 20:26 import pydis_site.apps.api.models -import pydis_site.apps.api.validators from django.db import migrations, models import django.db.models.deletion @@ -19,7 +18,7 @@ class Migration(migrations.Migration): ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), - ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.validators.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), + ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')), ], diff --git a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py index bd370d8e..d2a98e5d 100644 --- a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py +++ b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py @@ -1,6 +1,6 @@ # Generated by Django 2.1.5 on 2019-02-18 19:41 -import pydis_site.apps.api.validators +import pydis_site.apps.api.models.bot.bot_setting from django.db import migrations, models @@ -15,6 +15,6 @@ class Migration(migrations.Migration): model_name='botsetting', name='name', field=models.CharField(max_length=50, primary_key=True, serialize=False, validators=[ - pydis_site.apps.api.validators.validate_bot_setting_name]), + pydis_site.apps.api.models.bot.bot_setting.validate_bot_setting_name]), ), ] diff --git a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py new file mode 100644 index 00000000..a8256a0e --- /dev/null +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.5 on 2019-04-08 18:27 + +from django.db import migrations, models +import django.utils.timezone +import pydis_site.apps.api.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0034_add_botsetting_name_validator'), + ] + + operations = [ + migrations.CreateModel( + name='LogEntry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('application', models.CharField(choices=[('bot', 'Bot'), ('seasonalbot', 'Seasonalbot'), ('site', 'Website')], help_text='The application that generated this log entry.', max_length=20)), + ('logger_name', models.CharField(help_text='The name of the logger that generated this log entry.', max_length=100)), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time when this entry was created.')), + ('level', models.CharField(choices=[('debug', 'Debug'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], help_text='The logger level at which this entry was emitted. The levels correspond to the Python `logging` levels.', max_length=8)), + ('module', models.CharField(help_text='The fully qualified path of the module generating this log line.', max_length=100)), + ('line', models.PositiveSmallIntegerField(help_text='The line at which the log line was emitted.')), + ('message', models.TextField(help_text='The textual content of the log line.')), + ], + bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + ), + ] diff --git a/pydis_site/apps/api/models.py b/pydis_site/apps/api/models.py deleted file mode 100644 index 86c99f86..00000000 --- a/pydis_site/apps/api/models.py +++ /dev/null @@ -1,452 +0,0 @@ -from operator import itemgetter - -from django.contrib.postgres import fields as pgfields -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator -from django.db import models -from django.utils import timezone - -from .validators import validate_bot_setting_name, validate_tag_embed - - -class ModelReprMixin: - """ - Adds a `__repr__` method to the model subclassing this - mixin which will display the model's class name along - with all parameters used to construct the object. - """ - - def __repr__(self): - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class BotSetting(ModelReprMixin, models.Model): - """A configuration entry for the bot.""" - - name = models.CharField( - primary_key=True, - max_length=50, - validators=(validate_bot_setting_name,) - ) - data = pgfields.JSONField( - help_text="The actual settings of this setting." - ) - - -class DocumentationLink(ModelReprMixin, models.Model): - """A documentation link used by the `!docs` command of the bot.""" - - package = models.CharField( - primary_key=True, - max_length=50, - help_text="The Python package name that this documentation link belongs to." - ) - base_url = models.URLField( - help_text=( - "The base URL from which documentation will be available for this project. " - "Used to generate links to various symbols within this package." - ) - ) - inventory_url = models.URLField( - help_text="The URL at which the Sphinx inventory is available for this package." - ) - - def __str__(self): - return f"{self.package} - {self.base_url}" - - -class OffTopicChannelName(ModelReprMixin, models.Model): - name = models.CharField( - primary_key=True, - max_length=96, - validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), - help_text="The actual channel name that will be used on our Discord server." - ) - - def __str__(self): - return self.name - - -class Role(ModelReprMixin, models.Model): - """A role on our Discord server.""" - - id = models.BigIntegerField( # noqa - primary_key=True, - validators=( - MinValueValidator( - limit_value=0, - message="Role IDs cannot be negative." - ), - ), - help_text="The role ID, taken from Discord." - ) - name = models.CharField( - max_length=100, - help_text="The role name, taken from Discord." - ) - colour = models.IntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Colour hex cannot be negative." - ), - ), - help_text="The integer value of the colour of this role from Discord." - ) - permissions = models.IntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Role permissions cannot be negative." - ), - MaxValueValidator( - limit_value=2 << 32, - message="Role permission bitset exceeds value of having all permissions" - ) - ), - help_text="The integer value of the permission bitset of this role from Discord." - ) - - def __str__(self): - return self.name - - -class SnakeFact(ModelReprMixin, models.Model): - """A snake fact used by the bot's snake cog.""" - - fact = models.CharField( - primary_key=True, - max_length=200, - help_text="A fact about snakes." - ) - - def __str__(self): - return self.fact - - -class SnakeIdiom(ModelReprMixin, models.Model): - """A snake idiom used by the snake cog.""" - - idiom = models.CharField( - primary_key=True, - max_length=140, - help_text="A saying about a snake." - ) - - def __str__(self): - return self.idiom - - -class SnakeName(ModelReprMixin, models.Model): - """A snake name used by the bot's snake cog.""" - - name = models.CharField( - primary_key=True, - max_length=100, - help_text="The regular name for this snake, e.g. 'Python'.", - validators=[RegexValidator(regex=r'^([^0-9])+$')] - ) - scientific = models.CharField( - max_length=150, - help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", - validators=[RegexValidator(regex=r'^([^0-9])+$')] - ) - - def __str__(self): - return f"{self.name} ({self.scientific})" - - -class SpecialSnake(ModelReprMixin, models.Model): - """A special snake's name, info and image from our database used by the bot's snake cog.""" - - name = models.CharField( - max_length=140, - primary_key=True, - help_text='A special snake name.', - validators=[RegexValidator(regex=r'^([^0-9])+$')] - ) - info = models.TextField( - help_text='Info about a special snake.' - ) - images = pgfields.ArrayField( - models.URLField(), - help_text='Images displaying this special snake.' - ) - - def __str__(self): - return self.name - - -class Tag(ModelReprMixin, models.Model): - """A tag providing (hopefully) useful information.""" - - title = models.CharField( - max_length=100, - help_text=( - "The title of this tag, shown in searches and providing " - "a quick overview over what this embed contains." - ), - primary_key=True - ) - embed = pgfields.JSONField( - help_text="The actual embed shown by this tag.", - validators=(validate_tag_embed,) - ) - - def __str__(self): - return self.title - - -class User(ModelReprMixin, models.Model): - """A Discord user.""" - - id = models.BigIntegerField( # noqa - primary_key=True, - validators=( - MinValueValidator( - limit_value=0, - message="User IDs cannot be negative." - ), - ), - help_text="The ID of this user, taken from Discord." - ) - name = models.CharField( - max_length=32, - help_text="The username, taken from Discord." - ) - discriminator = models.PositiveSmallIntegerField( - validators=( - MaxValueValidator( - limit_value=9999, - message="Discriminators may not exceed `9999`." - ), - ), - help_text="The discriminator of this user, taken from Discord." - ) - avatar_hash = models.CharField( - max_length=100, - help_text=( - "The user's avatar hash, taken from Discord. " - "Null if the user does not have any custom avatar." - ), - null=True - ) - roles = models.ManyToManyField( - Role, - help_text="Any roles this user has on our server." - ) - in_guild = models.BooleanField( - default=True, - help_text="Whether this user is in our server." - ) - - def __str__(self): - return f"{self.name}#{self.discriminator}" - - -class Message(ModelReprMixin, models.Model): - id = models.BigIntegerField( - primary_key=True, - help_text="The message ID as taken from Discord.", - validators=( - MinValueValidator( - limit_value=0, - message="Message IDs cannot be negative." - ), - ) - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The author of this message." - ) - channel_id = models.BigIntegerField( - help_text=( - "The channel ID that this message was " - "sent in, taken from Discord." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Channel IDs cannot be negative." - ), - ) - ) - content = models.CharField( - max_length=2_000, - help_text="The content of this message, taken from Discord.", - blank=True - ) - embeds = pgfields.ArrayField( - pgfields.JSONField( - validators=(validate_tag_embed,) - ), - help_text="Embeds attached to this message." - ) - - class Meta: - abstract = True - - -class MessageDeletionContext(ModelReprMixin, models.Model): - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text=( - "The original actor causing this deletion. Could be the author " - "of a manual clean command invocation, the bot when executing " - "automatic actions, or nothing to indicate that the bulk " - "deletion was not issued by us." - ), - null=True - ) - creation = models.DateTimeField( - # Consider whether we want to add a validator here that ensures - # the deletion context does not take place in the future. - help_text="When this deletion took place." - ) - - -class DeletedMessage(Message): - deletion_context = models.ForeignKey( - MessageDeletionContext, - help_text="The deletion context this message is part of.", - on_delete=models.CASCADE - ) - - -class Infraction(ModelReprMixin, models.Model): - """An infraction for a Discord user.""" - - TYPE_CHOICES = ( - ("note", "Note"), - ("warning", "Warning"), - ("watch", "Watch"), - ("mute", "Mute"), - ("kick", "Kick"), - ("ban", "Ban"), - ("superstar", "Superstar") - ) - inserted_at = models.DateTimeField( - default=timezone.now, - help_text="The date and time of the creation of this infraction." - ) - expires_at = models.DateTimeField( - null=True, - help_text=( - "The date and time of the expiration of this infraction. " - "Null if the infraction is permanent or it can't expire." - ) - ) - active = models.BooleanField( - default=True, - help_text="Whether the infraction is still active." - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='infractions_received', - help_text="The user to which the infraction was applied." - ) - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='infractions_given', - help_text="The user which applied the infraction." - ) - type = models.CharField( - max_length=9, - choices=TYPE_CHOICES, - help_text="The type of the infraction." - ) - reason = models.TextField( - null=True, - help_text="The reason for the infraction." - ) - hidden = models.BooleanField( - default=False, - help_text="Whether the infraction is a shadow infraction." - ) - - def __str__(self): - s = f"#{self.id}: {self.type} on {self.user_id}" - if self.expires_at: - s += f" until {self.expires_at}" - if self.hidden: - s += " (hidden)" - return s - - -class Reminder(ModelReprMixin, models.Model): - """A reminder created by a user.""" - - active = models.BooleanField( - default=True, - help_text=( - "Whether this reminder is still active. " - "If not, it has been sent out to the user." - ) - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The creator of this reminder." - ) - channel_id = models.BigIntegerField( - help_text=( - "The channel ID that this message was " - "sent in, taken from Discord." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Channel IDs cannot be negative." - ), - ) - ) - content = models.CharField( - max_length=1500, - help_text="The content that the user wants to be reminded of." - ) - expiration = models.DateTimeField( - help_text="When this reminder should be sent." - ) - - def __str__(self): - return f"{self.content} on {self.expiration} by {self.author}" - - -class Nomination(ModelReprMixin, models.Model): - """A helper nomination created by staff.""" - - active = models.BooleanField( - default=True, - help_text="Whether this nomination is still relevant." - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The staff member that nominated this user.", - related_name='nomination_set' - ) - reason = models.TextField( - help_text="Why this user was nominated." - ) - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - help_text="The nominated user.", - primary_key=True, - related_name='nomination' - ) - inserted_at = models.DateTimeField( - auto_now_add=True, - help_text="The creation date of this nomination." - ) diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py new file mode 100644 index 00000000..4645bda2 --- /dev/null +++ b/pydis_site/apps/api/models/__init__.py @@ -0,0 +1,20 @@ +from .bot import ( # noqa + BotSetting, + DocumentationLink, + DeletedMessage, + Infraction, + Message, + MessageDeletionContext, + Nomination, + OffTopicChannelName, + Reminder, + Role, + SnakeFact, + SnakeIdiom, + SnakeName, + SpecialSnake, + Tag, + User +) +from .log_entry import LogEntry # noqa +from .utils import ModelReprMixin # noqa diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py new file mode 100644 index 00000000..fb313193 --- /dev/null +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -0,0 +1,16 @@ +from .bot_setting import BotSetting # noqa +from .deleted_message import DeletedMessage # noqa +from .documentation_link import DocumentationLink # noqa +from .infraction import Infraction # noqa +from .message import Message # noqa +from .message_deletion_context import MessageDeletionContext # noqa +from .nomination import Nomination # noqa +from .off_topic_channel_name import OffTopicChannelName # noqa +from .reminder import Reminder # noqa +from .role import Role # noqa +from .snake_fact import SnakeFact # noqa +from .snake_idiom import SnakeIdiom # noqa +from .snake_name import SnakeName # noqa +from .special_snake import SpecialSnake # noqa +from .tag import Tag # noqa +from .user import User # noqa diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py new file mode 100644 index 00000000..a6eeaa1f --- /dev/null +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -0,0 +1,27 @@ +from django.contrib.postgres import fields as pgfields +from django.core.exceptions import ValidationError +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def validate_bot_setting_name(name): + known_settings = ( + 'defcon', + ) + + if name not in known_settings: + raise ValidationError(f"`{name}` is not a known setting name.") + + +class BotSetting(ModelReprMixin, models.Model): + """A configuration entry for the bot.""" + + name = models.CharField( + primary_key=True, + max_length=50, + validators=(validate_bot_setting_name,) + ) + data = pgfields.JSONField( + help_text="The actual settings of this setting." + ) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py new file mode 100644 index 00000000..0f46cd12 --- /dev/null +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -0,0 +1,12 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.message import Message +from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext + + +class DeletedMessage(Message): + deletion_context = models.ForeignKey( + MessageDeletionContext, + help_text="The deletion context this message is part of.", + on_delete=models.CASCADE + ) diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py new file mode 100644 index 00000000..d7df22ad --- /dev/null +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -0,0 +1,25 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class DocumentationLink(ModelReprMixin, models.Model): + """A documentation link used by the `!docs` command of the bot.""" + + package = models.CharField( + primary_key=True, + max_length=50, + help_text="The Python package name that this documentation link belongs to." + ) + base_url = models.URLField( + help_text=( + "The base URL from which documentation will be available for this project. " + "Used to generate links to various symbols within this package." + ) + ) + inventory_url = models.URLField( + help_text="The URL at which the Sphinx inventory is available for this package." + ) + + def __str__(self): + return f"{self.package} - {self.base_url}" diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py new file mode 100644 index 00000000..911ca589 --- /dev/null +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -0,0 +1,67 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Infraction(ModelReprMixin, models.Model): + """An infraction for a Discord user.""" + + TYPE_CHOICES = ( + ("note", "Note"), + ("warning", "Warning"), + ("watch", "Watch"), + ("mute", "Mute"), + ("kick", "Kick"), + ("ban", "Ban"), + ("superstar", "Superstar") + ) + inserted_at = models.DateTimeField( + default=timezone.now, + help_text="The date and time of the creation of this infraction." + ) + expires_at = models.DateTimeField( + null=True, + help_text=( + "The date and time of the expiration of this infraction. " + "Null if the infraction is permanent or it can't expire." + ) + ) + active = models.BooleanField( + default=True, + help_text="Whether the infraction is still active." + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_received', + help_text="The user to which the infraction was applied." + ) + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='infractions_given', + help_text="The user which applied the infraction." + ) + type = models.CharField( + max_length=9, + choices=TYPE_CHOICES, + help_text="The type of the infraction." + ) + reason = models.TextField( + null=True, + help_text="The reason for the infraction." + ) + hidden = models.BooleanField( + default=False, + help_text="Whether the infraction is a shadow infraction." + ) + + def __str__(self): + s = f"#{self.id}: {self.type} on {self.user_id}" + if self.expires_at: + s += f" until {self.expires_at}" + if self.hidden: + s += " (hidden)" + return s diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py new file mode 100644 index 00000000..22500be0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message.py @@ -0,0 +1,51 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.bot.tag import validate_tag_embed +from pydis_site.apps.api.models.bot.user import User + + +class Message(ModelReprMixin, models.Model): + id = models.BigIntegerField( + primary_key=True, + help_text="The message ID as taken from Discord.", + validators=( + MinValueValidator( + limit_value=0, + message="Message IDs cannot be negative." + ), + ) + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The author of this message." + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that this message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + content = models.CharField( + max_length=2_000, + help_text="The content of this message, taken from Discord.", + blank=True + ) + embeds = pgfields.ArrayField( + pgfields.JSONField( + validators=(validate_tag_embed,) + ), + help_text="Embeds attached to this message." + ) + + class Meta: + abstract = True diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py new file mode 100644 index 00000000..9904ef71 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -0,0 +1,23 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class MessageDeletionContext(ModelReprMixin, models.Model): + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text=( + "The original actor causing this deletion. Could be the author " + "of a manual clean command invocation, the bot when executing " + "automatic actions, or nothing to indicate that the bulk " + "deletion was not issued by us." + ), + null=True + ) + creation = models.DateTimeField( + # Consider whether we want to add a validator here that ensures + # the deletion context does not take place in the future. + help_text="When this deletion took place." + ) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py new file mode 100644 index 00000000..5ebb9759 --- /dev/null +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -0,0 +1,33 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Nomination(ModelReprMixin, models.Model): + """A helper nomination created by staff.""" + + active = models.BooleanField( + default=True, + help_text="Whether this nomination is still relevant." + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The staff member that nominated this user.", + related_name='nomination_set' + ) + reason = models.TextField( + help_text="Why this user was nominated." + ) + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + help_text="The nominated user.", + primary_key=True, + related_name='nomination' + ) + inserted_at = models.DateTimeField( + auto_now_add=True, + help_text="The creation date of this nomination." + ) diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py new file mode 100644 index 00000000..dff7eaf8 --- /dev/null +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -0,0 +1,16 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class OffTopicChannelName(ModelReprMixin, models.Model): + name = models.CharField( + primary_key=True, + max_length=96, + validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), + help_text="The actual channel name that will be used on our Discord server." + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py new file mode 100644 index 00000000..abccdf82 --- /dev/null +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -0,0 +1,44 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Reminder(ModelReprMixin, models.Model): + """A reminder created by a user.""" + + active = models.BooleanField( + default=True, + help_text=( + "Whether this reminder is still active. " + "If not, it has been sent out to the user." + ) + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The creator of this reminder." + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that this message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + content = models.CharField( + max_length=1500, + help_text="The content that the user wants to be reminded of." + ) + expiration = models.DateTimeField( + help_text="When this reminder should be sent." + ) + + def __str__(self): + return f"{self.content} on {self.expiration} by {self.author}" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py new file mode 100644 index 00000000..8106394f --- /dev/null +++ b/pydis_site/apps/api/models/bot/role.py @@ -0,0 +1,48 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Role(ModelReprMixin, models.Model): + """A role on our Discord server.""" + + id = models.BigIntegerField( # noqa + primary_key=True, + validators=( + MinValueValidator( + limit_value=0, + message="Role IDs cannot be negative." + ), + ), + help_text="The role ID, taken from Discord." + ) + name = models.CharField( + max_length=100, + help_text="The role name, taken from Discord." + ) + colour = models.IntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Colour hex cannot be negative." + ), + ), + help_text="The integer value of the colour of this role from Discord." + ) + permissions = models.IntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Role permissions cannot be negative." + ), + MaxValueValidator( + limit_value=2 << 32, + message="Role permission bitset exceeds value of having all permissions" + ) + ), + help_text="The integer value of the permission bitset of this role from Discord." + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/models/bot/snake_fact.py b/pydis_site/apps/api/models/bot/snake_fact.py new file mode 100644 index 00000000..4398620a --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_fact.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeFact(ModelReprMixin, models.Model): + """A snake fact used by the bot's snake cog.""" + + fact = models.CharField( + primary_key=True, + max_length=200, + help_text="A fact about snakes." + ) + + def __str__(self): + return self.fact diff --git a/pydis_site/apps/api/models/bot/snake_idiom.py b/pydis_site/apps/api/models/bot/snake_idiom.py new file mode 100644 index 00000000..e4db00e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_idiom.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeIdiom(ModelReprMixin, models.Model): + """A snake idiom used by the snake cog.""" + + idiom = models.CharField( + primary_key=True, + max_length=140, + help_text="A saying about a snake." + ) + + def __str__(self): + return self.idiom diff --git a/pydis_site/apps/api/models/bot/snake_name.py b/pydis_site/apps/api/models/bot/snake_name.py new file mode 100644 index 00000000..045a7faa --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_name.py @@ -0,0 +1,23 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeName(ModelReprMixin, models.Model): + """A snake name used by the bot's snake cog.""" + + name = models.CharField( + primary_key=True, + max_length=100, + help_text="The regular name for this snake, e.g. 'Python'.", + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + scientific = models.CharField( + max_length=150, + help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + + def __str__(self): + return f"{self.name} ({self.scientific})" diff --git a/pydis_site/apps/api/models/bot/special_snake.py b/pydis_site/apps/api/models/bot/special_snake.py new file mode 100644 index 00000000..1406b9e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/special_snake.py @@ -0,0 +1,26 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SpecialSnake(ModelReprMixin, models.Model): + """A special snake's name, info and image from our database used by the bot's snake cog.""" + + name = models.CharField( + max_length=140, + primary_key=True, + help_text='A special snake name.', + validators=[RegexValidator(regex=r'^([^0-9])+$')] + ) + info = models.TextField( + help_text='Info about a special snake.' + ) + images = pgfields.ArrayField( + models.URLField(), + help_text='Images displaying this special snake.' + ) + + def __str__(self): + return self.name diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/models/bot/tag.py index ea2112a9..62881ca2 100644 --- a/pydis_site/apps/api/validators.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -1,7 +1,11 @@ from collections.abc import Mapping +from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin def validate_tag_embed_fields(fields): @@ -155,10 +159,21 @@ def validate_tag_embed(embed): validator(value) -def validate_bot_setting_name(name): - known_settings = ( - 'defcon', +class Tag(ModelReprMixin, models.Model): + """A tag providing (hopefully) useful information.""" + + title = models.CharField( + max_length=100, + help_text=( + "The title of this tag, shown in searches and providing " + "a quick overview over what this embed contains." + ), + primary_key=True + ) + embed = pgfields.JSONField( + help_text="The actual embed shown by this tag.", + validators=(validate_tag_embed,) ) - if name not in known_settings: - raise ValidationError(f"`{name}` is not a known setting name.") + def __str__(self): + return self.title diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py new file mode 100644 index 00000000..f5365ed1 --- /dev/null +++ b/pydis_site/apps/api/models/bot/user.py @@ -0,0 +1,52 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.role import Role +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class User(ModelReprMixin, models.Model): + """A Discord user.""" + + id = models.BigIntegerField( # noqa + primary_key=True, + validators=( + MinValueValidator( + limit_value=0, + message="User IDs cannot be negative." + ), + ), + help_text="The ID of this user, taken from Discord." + ) + name = models.CharField( + max_length=32, + help_text="The username, taken from Discord." + ) + discriminator = models.PositiveSmallIntegerField( + validators=( + MaxValueValidator( + limit_value=9999, + message="Discriminators may not exceed `9999`." + ), + ), + help_text="The discriminator of this user, taken from Discord." + ) + avatar_hash = models.CharField( + max_length=100, + help_text=( + "The user's avatar hash, taken from Discord. " + "Null if the user does not have any custom avatar." + ), + null=True + ) + roles = models.ManyToManyField( + Role, + help_text="Any roles this user has on our server." + ) + in_guild = models.BooleanField( + default=True, + help_text="Whether this user is in our server." + ) + + def __str__(self): + return f"{self.name}#{self.discriminator}" diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py new file mode 100644 index 00000000..acd7953a --- /dev/null +++ b/pydis_site/apps/api/models/log_entry.py @@ -0,0 +1,50 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class LogEntry(ModelReprMixin, models.Model): + """A log entry generated by one of the PyDis applications.""" + + application = models.CharField( + max_length=20, + help_text="The application that generated this log entry.", + choices=( + ('bot', 'Bot'), + ('seasonalbot', 'Seasonalbot'), + ('site', 'Website') + ) + ) + logger_name = models.CharField( + max_length=100, + help_text="The name of the logger that generated this log entry." + ) + timestamp = models.DateTimeField( + default=timezone.now, + help_text="The date and time when this entry was created." + ) + level = models.CharField( + max_length=8, # 'critical' + choices=( + ('debug', 'Debug'), + ('info', 'Info'), + ('warning', 'Warning'), + ('error', 'Error'), + ('critical', 'Critical') + ), + help_text=( + "The logger level at which this entry was emitted. The levels " + "correspond to the Python `logging` levels." + ) + ) + module = models.CharField( + max_length=100, + help_text="The fully qualified path of the module generating this log line." + ) + line = models.PositiveSmallIntegerField( + help_text="The line at which the log line was emitted." + ) + message = models.TextField( + help_text="The textual content of the log line." + ) diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..731486e7 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,20 @@ +from operator import itemgetter + + +class ModelReprMixin: + """ + Adds a `__repr__` method to the model subclassing this + mixin which will display the model's class name along + with all parameters used to construct the object. + """ + + def __repr__(self): + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 5b3cb28c..8f045044 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,12 +4,12 @@ from rest_framework_bulk import BulkSerializerMixin from .models import ( BotSetting, DeletedMessage, DocumentationLink, Infraction, - MessageDeletionContext, Nomination, - OffTopicChannelName, Reminder, - Role, SnakeFact, - SnakeIdiom, SnakeName, - SpecialSnake, Tag, - User + LogEntry, MessageDeletionContext, + Nomination, OffTopicChannelName, + Reminder, Role, + SnakeFact, SnakeIdiom, + SnakeName, SpecialSnake, + Tag, User ) @@ -101,6 +101,15 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret +class LogEntrySerializer(ModelSerializer): + class Meta: + model = LogEntry + fields = ( + 'application', 'logger_name', 'timestamp', + 'level', 'module', 'line', 'message' + ) + + class OffTopicChannelNameSerializer(ModelSerializer): class Meta: model = OffTopicChannelName diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index d0b78c23..ffa2f61e 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -1,10 +1,8 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from ..validators import ( - validate_bot_setting_name, - validate_tag_embed -) +from ..models.bot.bot_setting import validate_bot_setting_name +from ..models.bot.tag import validate_tag_embed REQUIRED_KEYS = ( diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 6c89a52e..724d7e2b 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -5,11 +5,12 @@ from .views import HealthcheckView, RulesView from .viewsets import ( BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, InfractionViewSet, - NominationViewSet, OffTopicChannelNameViewSet, - ReminderViewSet, RoleViewSet, - SnakeFactViewSet, SnakeIdiomViewSet, - SnakeNameViewSet, SpecialSnakeViewSet, - TagViewSet, UserViewSet + LogEntryViewSet, NominationViewSet, + OffTopicChannelNameViewSet, ReminderViewSet, + RoleViewSet, SnakeFactViewSet, + SnakeIdiomViewSet, SnakeNameViewSet, + SpecialSnakeViewSet, TagViewSet, + UserViewSet ) @@ -81,6 +82,7 @@ urlpatterns = ( # from django_hosts.resolvers import reverse # snake_name_endpoint = reverse('bot:snakename-list', host='api') # `bot/` endpoints path('bot/', include((bot_router.urls, 'api'), namespace='bot')), + path('logs', LogEntryViewSet.as_view({'post': 'create'}), name='logs'), path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), path('rules', RulesView.as_view(), name='rules') ) diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py index 949ffaaa..47915256 100644 --- a/pydis_site/apps/api/viewsets.py +++ b/pydis_site/apps/api/viewsets.py @@ -15,22 +15,23 @@ from rest_framework_bulk import BulkCreateModelMixin from .models import ( BotSetting, DocumentationLink, - Infraction, MessageDeletionContext, - Nomination, OffTopicChannelName, - Reminder, Role, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag, User + Infraction, LogEntry, + MessageDeletionContext, Nomination, + OffTopicChannelName, Reminder, + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag, + User ) from .serializers import ( BotSettingSerializer, DocumentationLinkSerializer, ExpandedInfractionSerializer, InfractionSerializer, - MessageDeletionContextSerializer, NominationSerializer, - OffTopicChannelNameSerializer, ReminderSerializer, - RoleSerializer, SnakeFactSerializer, - SnakeIdiomSerializer, SnakeNameSerializer, - SpecialSnakeSerializer, TagSerializer, - UserSerializer + LogEntrySerializer, MessageDeletionContextSerializer, + NominationSerializer, OffTopicChannelNameSerializer, + ReminderSerializer, RoleSerializer, + SnakeFactSerializer, SnakeIdiomSerializer, + SnakeNameSerializer, SpecialSnakeSerializer, + TagSerializer, UserSerializer ) @@ -280,6 +281,38 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge return self.partial_update(*args, **kwargs) +class LogEntryViewSet(CreateModelMixin, GenericViewSet): + """ + View providing support for creating log entries in the site database + for viewing via the log browser. + + ## Routes + ### POST /logs + Create a new log entry. + + #### Request body + >>> { + ... 'application': str, # 'bot' | 'seasonalbot' | 'site' + ... 'logger_name': str, # such as 'bot.cogs.moderation' + ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()` + ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical' + ... 'module': str, # such as 'pydis_site.apps.api.serializers' + ... 'line': int, # > 0 + ... 'message': str, # textual formatted content of the logline + ... } + + #### Status codes + - 201: returned on success + - 400: if the request body has invalid fields, see the response for details + + ## Authentication + Requires a API token. + """ + + queryset = LogEntry.objects.all() + serializer_class = LogEntrySerializer + + class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): """ View of off-topic channel names used by the bot diff --git a/pydis_site/apps/main/tests.py b/pydis_site/apps/main/tests.py index 54fac6e8..733ddaa3 100644 --- a/pydis_site/apps/main/tests.py +++ b/pydis_site/apps/main/tests.py @@ -1,9 +1,16 @@ from django.test import TestCase from django_hosts.resolvers import reverse +from pydis_site.apps.home.templatetags import extra_filters + class TestIndexReturns200(TestCase): def test_index_returns_200(self): - url = reverse('index') + url = reverse('home.index') resp = self.client.get(url) self.assertEqual(resp.status_code, 200) + + +class TestExtraFilterTemplateTags(TestCase): + def test_starts_with(self): + self.assertTrue(extra_filters.starts_with('foo', 'f')) |