aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.hadolint.yaml6
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock431
-rw-r--r--azure-pipelines.yml4
-rw-r--r--docker/app/Dockerfile3
-rw-r--r--docs/setup.md28
-rw-r--r--pydis_site/apps/api/migrations/0008_tag_embed_validator.py4
-rw-r--r--pydis_site/apps/api/migrations/0019_deletedmessage.py3
-rw-r--r--pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py4
-rw-r--r--pydis_site/apps/api/migrations/0035_create_table_log_entry.py29
-rw-r--r--pydis_site/apps/api/models.py452
-rw-r--r--pydis_site/apps/api/models/__init__.py20
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py16
-rw-r--r--pydis_site/apps/api/models/bot/bot_setting.py27
-rw-r--r--pydis_site/apps/api/models/bot/deleted_message.py12
-rw-r--r--pydis_site/apps/api/models/bot/documentation_link.py25
-rw-r--r--pydis_site/apps/api/models/bot/infraction.py67
-rw-r--r--pydis_site/apps/api/models/bot/message.py51
-rw-r--r--pydis_site/apps/api/models/bot/message_deletion_context.py23
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py33
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py16
-rw-r--r--pydis_site/apps/api/models/bot/reminder.py44
-rw-r--r--pydis_site/apps/api/models/bot/role.py48
-rw-r--r--pydis_site/apps/api/models/bot/snake_fact.py16
-rw-r--r--pydis_site/apps/api/models/bot/snake_idiom.py16
-rw-r--r--pydis_site/apps/api/models/bot/snake_name.py23
-rw-r--r--pydis_site/apps/api/models/bot/special_snake.py26
-rw-r--r--pydis_site/apps/api/models/bot/tag.py (renamed from pydis_site/apps/api/validators.py)25
-rw-r--r--pydis_site/apps/api/models/bot/user.py52
-rw-r--r--pydis_site/apps/api/models/log_entry.py50
-rw-r--r--pydis_site/apps/api/models/utils.py20
-rw-r--r--pydis_site/apps/api/serializers.py21
-rw-r--r--pydis_site/apps/api/tests/test_validators.py6
-rw-r--r--pydis_site/apps/api/urls.py12
-rw-r--r--pydis_site/apps/api/viewsets.py57
-rw-r--r--pydis_site/apps/main/tests.py9
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`.
diff --git a/Pipfile b/Pipfile
index bb2c88ee..1e106f07 100644
--- a/Pipfile
+++ b/Pipfile
@@ -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'))