diff options
| author | 2020-10-08 12:08:04 -0700 | |
|---|---|---|
| committer | 2020-10-08 12:08:04 -0700 | |
| commit | fcfb422016d2d0d9689a19e0b1010979f1f7ae42 (patch) | |
| tree | 6a956f0eec1c8252d3b5e05139563249295da321 | |
| parent | Merge pull request #408 from imsofi/master (diff) | |
| parent | Move Validation checks to serializer from viewset (diff) | |
Merge pull request #378 from RohanJnr/user_endpoint
Add pagination and bulk update support for User(discord user) serializer.
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | Pipfile.lock | 235 | ||||
| -rw-r--r-- | pydis_site/apps/api/serializers.py | 98 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_users.py | 221 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/user.py | 120 | 
5 files changed, 543 insertions, 132 deletions
| @@ -9,7 +9,6 @@ django-environ = "~=0.4.5"  django-filter = "~=2.1.0"  django-hosts = "~=4.0"  djangorestframework = "~=3.11.0" -djangorestframework-bulk = "~=0.2.1"  psycopg2-binary = "~=2.8"  django-simple-bulma = "~=1.2"  whitenoise = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index b8c85d33..65b9c154 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "ad586b840e82b4ae87eed1af70adde0b2c8b7f862a832c4cfa87748b97add3bd" +            "sha256": "4ecc64deaa82df654479986c9c9569721a66296725e51272608a8294ac562af2"          },          "pipfile-spec": 6,          "requires": { @@ -56,11 +56,11 @@          },          "django": {              "hashes": [ -                "sha256:96fbe04e8ba0df289171e7f6970e0ff8b472bf4f909ed9e0e5beccbac7e1dbbe", -                "sha256:c22b4cd8e388f8219dc121f091e53a8701f9f5bca9aa132b5254263cab516215" +                "sha256:2d14be521c3ae24960e5e83d4575e156a8c479a75c935224b671b1c6e66eddaf", +                "sha256:313d0b8f96685e99327785cc600a5178ca855f8e6f4ed162e671e8c3cf749739"              ],              "index": "pypi", -            "version": "==3.0.9" +            "version": "==3.0.10"          },          "django-allauth": {              "hashes": [ @@ -71,9 +71,10 @@          },          "django-classy-tags": {              "hashes": [ -                "sha256:ad6a25fc2b58a098f00d86bd5e5dad47922f5ca4e744bc3cccb7b4be5bc35eb1" +                "sha256:25eb4f95afee396148683bfb4811b83b3f5729218d73ad0a3399271a6f9fcc49", +                "sha256:d59d98bdf96a764dcf7a2929a86439d023b283a9152492811c7e44fc47555bc9"              ], -            "version": "==1.0.0" +            "version": "==2.0.0"          },          "django-environ": {              "hashes": [ @@ -123,9 +124,10 @@          },          "django-sekizai": {              "hashes": [ -                "sha256:e2f6e666d4dd9d3ecc27284acb85ef709e198014f5d5af8c6d54ed04c2d684d9" +                "sha256:5c5e16845d37ce822fc655ce79ec02715191b3d03330b550997bcb842cf24fdf", +                "sha256:e829f09b0d6bf01ee5cde05de1fb3faf2fbc5df66dc4dc280fbaac224ca4336f"              ], -            "version": "==1.1.0" +            "version": "==2.0.0"          },          "django-simple-bulma": {              "hashes": [ @@ -143,13 +145,6 @@              "index": "pypi",              "version": "==3.11.1"          }, -        "djangorestframework-bulk": { -            "hashes": [ -                "sha256:39230d8379acebd86d313df6c9150cafecb636eae1d097c30a26389ab9fee5b1" -            ], -            "index": "pypi", -            "version": "==0.2.1" -        },          "gitdb": {              "hashes": [                  "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", @@ -160,11 +155,11 @@          },          "gitpython": {              "hashes": [ -                "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", -                "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" +                "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912", +                "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"              ],              "index": "pypi", -            "version": "==3.1.7" +            "version": "==3.1.8"          },          "idna": {              "hashes": [ @@ -184,21 +179,21 @@          },          "libsass": {              "hashes": [ -                "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726", -                "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7", -                "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b", -                "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd", -                "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d", -                "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687", -                "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a", -                "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57", -                "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60", -                "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb", -                "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc", -                "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481", -                "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1" -            ], -            "version": "==0.20.0" +                "sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", +                "sha256:1b2d415bbf6fa7da33ef46e549db1418498267b459978eff8357e5e823962d35", +                "sha256:25ebc2085f5eee574761ccc8d9cd29a9b436fc970546d5ef08c6fa41eb57dff1", +                "sha256:2ae806427b28bc1bb7cb0258666d854fcf92ba52a04656b0b17ba5e190fb48a9", +                "sha256:4a246e4b88fd279abef8b669206228c92534d96ddcd0770d7012088c408dff23", +                "sha256:553e5096414a8d4fb48d0a48f5a038d3411abe254d79deac5e008516c019e63a", +                "sha256:697f0f9fa8a1367ca9ec6869437cb235b1c537fc8519983d1d890178614a8903", +                "sha256:a8fd4af9f853e8bf42b1425c5e48dd90b504fa2e70d7dac5ac80b8c0a5a5fe85", +                "sha256:c9411fec76f480ffbacc97d8188322e02a5abca6fc78e70b86a2a2b421eae8a2", +                "sha256:daa98a51086d92aa7e9c8871cf1a8258124b90e2abf4697852a3dca619838618", +                "sha256:e0e60836eccbf2d9e24ec978a805cd6642fa92515fbd95e3493fee276af76f8a", +                "sha256:e64ae2587f1a683e831409aad03ba547c245ef997e1329fffadf7a866d2510b8", +                "sha256:f6852828e9e104d2ce0358b73c550d26dd86cc3a69439438c3b618811b9584f5" +            ], +            "version": "==0.20.1"          },          "markdown": {              "hashes": [ @@ -226,73 +221,75 @@          },          "pillow": {              "hashes": [ -                "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", -                "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", +                "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", +                "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", +                "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", +                "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f",                  "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", +                "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", +                "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5",                  "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", +                "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", +                "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233",                  "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", -                "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", +                "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", +                "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", +                "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38",                  "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", -                "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", -                "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", +                "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", +                "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", +                "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6",                  "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", -                "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce", -                "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38",                  "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", -                "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", -                "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", -                "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", -                "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", -                "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", -                "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", -                "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", -                "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", -                "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", -                "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", -                "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", -                "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", +                "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f",                  "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41",                  "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", -                "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117" +                "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", +                "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", +                "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", +                "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", +                "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"              ],              "markers": "python_version >= '3.5'",              "version": "==7.2.0"          },          "psycopg2-binary": {              "hashes": [ -                "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac", -                "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a", -                "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5", -                "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04", -                "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1", -                "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5", -                "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce", -                "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434", -                "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9", -                "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057", -                "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98", -                "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522", -                "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505", -                "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa", -                "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3", -                "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f", -                "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4", -                "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4", -                "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266", -                "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66", -                "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38", -                "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3", -                "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389", -                "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab", -                "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb", -                "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6", -                "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d", -                "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162", -                "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e", -                "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd" -            ], -            "index": "pypi", -            "version": "==2.8.5" +                "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", +                "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", +                "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", +                "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", +                "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", +                "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", +                "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", +                "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", +                "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", +                "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", +                "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", +                "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", +                "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", +                "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", +                "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", +                "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", +                "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", +                "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", +                "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", +                "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", +                "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", +                "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", +                "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", +                "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", +                "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", +                "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", +                "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", +                "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", +                "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", +                "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", +                "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", +                "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" +            ], +            "index": "pypi", +            "version": "==2.8.6"          },          "pygments": {              "hashes": [ @@ -307,7 +304,7 @@                  "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",                  "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"              ], -            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",              "version": "==2.4.7"          },          "python3-openid": { @@ -378,18 +375,18 @@          },          "sentry-sdk": {              "hashes": [ -                "sha256:d359609e23ec9360b61e5ffdfa417e2f6bca281bfb869608c98c169c7e64acd5", -                "sha256:e12eb1c2c01cd9e9cfe70608dbda4ef451f37ef0b7cbb92e5d43f87c341d6334" +                "sha256:0af429c221670e602f960fca85ca3f607c85510a91f11e8be8f742a978127f78", +                "sha256:a088a1054673c6a19ea590045c871c38da029ef743b61a07bfee95e9f3c060f7"              ],              "index": "pypi", -            "version": "==0.16.5" +            "version": "==0.17.3"          },          "six": {              "hashes": [                  "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",                  "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"              ], -            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",              "version": "==1.15.0"          },          "smmap": { @@ -466,11 +463,11 @@          },          "attrs": {              "hashes": [ -                "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", -                "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" +                "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", +                "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.1.0" +            "version": "==20.2.0"          },          "bandit": {              "hashes": [ @@ -551,11 +548,11 @@          },          "flake8-annotations": {              "hashes": [ -                "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26", -                "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414" +                "sha256:09fe1aa3f40cb8fef632a0ab3614050a7584bb884b6134e70cf1fc9eeee642fa", +                "sha256:5bda552f074fd6e34276c7761756fa07d824ffac91ce9c0a8555eb2bc5b92d7a"              ],              "index": "pypi", -            "version": "==2.3.0" +            "version": "==2.4.0"          },          "flake8-bandit": {              "hashes": [ @@ -628,19 +625,19 @@          },          "gitpython": {              "hashes": [ -                "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", -                "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" +                "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912", +                "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"              ],              "index": "pypi", -            "version": "==3.1.7" +            "version": "==3.1.8"          },          "identify": {              "hashes": [ -                "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", -                "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7" +                "sha256:009f92ba753c467a99f6fd3eb395412cbc34077dd5a64313b62ba04297f2ab8e", +                "sha256:0868312cb7402b48cf44fe3f568259f804ef4e983c143d11bf7a51ca311ebc34"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==1.4.28" +            "version": "==1.5.0"          },          "importlib-metadata": {              "hashes": [ @@ -660,16 +657,18 @@          },          "nodeenv": {              "hashes": [ -                "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" +                "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", +                "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"              ], -            "version": "==1.4.0" +            "version": "==1.5.0"          },          "pbr": {              "hashes": [ -                "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", -                "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8" +                "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", +                "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"              ], -            "version": "==5.4.5" +            "markers": "python_version >= '2.6'", +            "version": "==5.5.0"          },          "pep8-naming": {              "hashes": [ @@ -681,11 +680,11 @@          },          "pre-commit": {              "hashes": [ -                "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", -                "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" +                "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", +                "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"              ],              "index": "pypi", -            "version": "==2.6.0" +            "version": "==2.7.1"          },          "pycodestyle": {              "hashes": [ @@ -697,11 +696,11 @@          },          "pydocstyle": {              "hashes": [ -                "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", -                "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" +                "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", +                "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"              ],              "markers": "python_version >= '3.5'", -            "version": "==5.0.2" +            "version": "==5.1.1"          },          "pyflakes": {              "hashes": [ @@ -733,7 +732,7 @@                  "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",                  "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"              ], -            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", +            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",              "version": "==1.15.0"          },          "smmap": { @@ -753,11 +752,11 @@          },          "stevedore": {              "hashes": [ -                "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5", -                "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633" +                "sha256:a34086819e2c7a7f86d5635363632829dab8014e5fd7be2454c7cba84ac7514e", +                "sha256:ddc09a744dc224c84ec8e8efcb70595042d21c97c76df60daee64c9ad53bc7ee"              ],              "markers": "python_version >= '3.6'", -            "version": "==3.2.0" +            "version": "==3.2.1"          },          "toml": {              "hashes": [ diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f9a5517e..25c5c82e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,7 +1,16 @@  """Converters from Django models to data interchange formats and back.""" -from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError +from django.db.models.query import QuerySet +from django.db.utils import IntegrityError +from rest_framework.exceptions import NotFound +from rest_framework.serializers import ( +    IntegerField, +    ListSerializer, +    ModelSerializer, +    PrimaryKeyRelatedField, +    ValidationError +) +from rest_framework.settings import api_settings  from rest_framework.validators import UniqueTogetherValidator -from rest_framework_bulk import BulkSerializerMixin  from .models import (      BotSetting, @@ -235,15 +244,98 @@ class RoleSerializer(ModelSerializer):          fields = ('id', 'name', 'colour', 'permissions', 'position') -class UserSerializer(BulkSerializerMixin, ModelSerializer): +class UserListSerializer(ListSerializer): +    """List serializer for User model to handle bulk updates.""" + +    def create(self, validated_data: list) -> list: +        """Override create method to optimize django queries.""" +        new_users = [] +        seen = set() + +        for user_dict in validated_data: +            if user_dict["id"] in seen: +                raise ValidationError( +                    {"id": [f"User with ID {user_dict['id']} given multiple times."]} +                ) +            seen.add(user_dict["id"]) +            new_users.append(User(**user_dict)) + +        User.objects.bulk_create(new_users, ignore_conflicts=True) +        return [] + +    def update(self, queryset: QuerySet, validated_data: list) -> list: +        """ +        Override update method to support bulk updates. + +        ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update +        """ +        object_ids = set() + +        for data in validated_data: +            try: +                if data["id"] in object_ids: +                    # If request data contains users with same ID. +                    raise ValidationError( +                        {"id": [f"User with ID {data['id']} given multiple times."]} +                    ) +            except KeyError: +                # If user ID not provided in request body. +                raise ValidationError( +                    {"id": ["This field is required."]} +                ) +            object_ids.add(data["id"]) + +        # filter queryset +        filtered_instances = queryset.filter(id__in=object_ids) + +        instance_mapping = {user.id: user for user in filtered_instances} + +        updated = [] +        fields_to_update = set() +        for user_data in validated_data: +            for key in user_data: +                fields_to_update.add(key) + +                try: +                    user = instance_mapping[user_data["id"]] +                except KeyError: +                    raise NotFound({"detail": f"User with id {user_data['id']} not found."}) + +                user.__dict__.update(user_data) +            updated.append(user) + +        fields_to_update.remove("id") + +        if not fields_to_update: +            # Raise ValidationError when only id field is given. +            raise ValidationError( +                {api_settings.NON_FIELD_ERRORS_KEY: ["Insufficient data provided."]} +            ) + +        User.objects.bulk_update(updated, fields_to_update) +        return updated + + +class UserSerializer(ModelSerializer):      """A class providing (de-)serialization of `User` instances.""" +    # ID field must be explicitly set as the default id field is read-only. +    id = IntegerField(min_value=0) +      class Meta:          """Metadata defined for the Django REST Framework."""          model = User          fields = ('id', 'name', 'discriminator', 'roles', 'in_guild')          depth = 1 +        list_serializer_class = UserListSerializer + +    def create(self, validated_data: dict) -> User: +        """Override create method to catch IntegrityError.""" +        try: +            return super().create(validated_data) +        except IntegrityError: +            raise ValidationError({"id": ["User with ID already present."]})  class NominationSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index a02fce8a..825e4edb 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -45,6 +45,13 @@ class CreationTests(APISubdomainTestCase):              position=1          ) +        cls.user = User.objects.create( +            id=11, +            name="Name doesn't matter.", +            discriminator=1122, +            in_guild=True +        ) +      def test_accepts_valid_data(self):          url = reverse('bot:user-list', host='api')          data = { @@ -89,7 +96,7 @@ class CreationTests(APISubdomainTestCase):          response = self.client.post(url, data=data)          self.assertEqual(response.status_code, 201) -        self.assertEqual(response.json(), data) +        self.assertEqual(response.json(), [])      def test_returns_400_for_unknown_role_id(self):          url = reverse('bot:user-list', host='api') @@ -115,6 +122,176 @@ class CreationTests(APISubdomainTestCase):          response = self.client.post(url, data=data)          self.assertEqual(response.status_code, 400) +    def test_returns_400_for_user_recreation(self): +        """Return 201 if User is already present in database as it skips User creation.""" +        url = reverse('bot:user-list', host='api') +        data = [{ +            'id': 11, +            'name': 'You saw nothing.', +            'discriminator': 112, +            'in_guild': True +        }] +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 201) + +    def test_returns_400_for_duplicate_request_users(self): +        """Return 400 if 2 Users with same ID is passed in the request data.""" +        url = reverse('bot:user-list', host='api') +        data = [ +            { +                'id': 11, +                'name': 'You saw nothing.', +                'discriminator': 112, +                'in_guild': True +            }, +            { +                'id': 11, +                'name': 'You saw nothing part 2.', +                'discriminator': 1122, +                'in_guild': False +            } +        ] +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 400) + +    def test_returns_400_for_existing_user(self): +        """Returns 400 if user is already present in DB.""" +        url = reverse('bot:user-list', host='api') +        data = { +            'id': 11, +            'name': 'You saw nothing part 3.', +            'discriminator': 1122, +            'in_guild': True +        } +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 400) + + +class MultiPatchTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.role_developer = Role.objects.create( +            id=159, +            name="Developer", +            colour=2, +            permissions=0b01010010101, +            position=10, +        ) +        cls.user_1 = User.objects.create( +            id=1, +            name="Patch test user 1.", +            discriminator=1111, +            in_guild=True +        ) +        cls.user_2 = User.objects.create( +            id=2, +            name="Patch test user 2.", +            discriminator=2222, +            in_guild=True +        ) + +    def test_multiple_users_patch(self): +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                "id": 1, +                "name": "User 1 patched!", +                "discriminator": 1010, +                "roles": [self.role_developer.id], +                "in_guild": False +            }, +            { +                "id": 2, +                "name": "User 2 patched!" +            } +        ] + +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json()[0], data[0]) + +        user_2 = User.objects.get(id=2) +        self.assertEqual(user_2.name, data[1]["name"]) + +    def test_returns_400_for_missing_user_id(self): +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                "name": "I am ghost user!", +                "discriminator": 1010, +                "roles": [self.role_developer.id], +                "in_guild": False +            }, +            { +                "name": "patch me? whats my id?" +            } +        ] +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 400) + +    def test_returns_404_for_not_found_user(self): +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                "id": 1, +                "name": "User 1 patched again!!!", +                "discriminator": 1010, +                "roles": [self.role_developer.id], +                "in_guild": False +            }, +            { +                "id": 22503405, +                "name": "User unknown not patched!" +            } +        ] +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 404) + +    def test_returns_400_for_bad_data(self): +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                "id": 1, +                "in_guild": "Catch me!" +            }, +            { +                "id": 2, +                "discriminator": "find me!" +            } +        ] + +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 400) + +    def test_returns_400_for_insufficient_data(self): +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                "id": 1, +            }, +            { +                "id": 2, +            } +        ] +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 400) + +    def test_returns_400_for_duplicate_request_users(self): +        """Return 400 if 2 Users with same ID is passed in the request data.""" +        url = reverse("bot:user-bulk-patch", host="api") +        data = [ +            { +                'id': 1, +                'name': 'You saw nothing.', +            }, +            { +                'id': 1, +                'name': 'You saw nothing part 2.', +            } +        ] +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 400) +  class UserModelTests(APISubdomainTestCase):      @classmethod @@ -170,3 +347,45 @@ class UserModelTests(APISubdomainTestCase):      def test_correct_username_formatting(self):          """Tests the username property with both name and discriminator formatted together."""          self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserPaginatorTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        users = [] +        for i in range(1, 10_001): +            users.append(User( +                id=i, +                name=f"user{i}", +                discriminator=1111, +                in_guild=True +            )) +        cls.users = User.objects.bulk_create(users) + +    def test_returns_single_page_response(self): +        url = reverse("bot:user-list", host="api") +        response = self.client.get(url).json() +        self.assertIsNone(response["next_page_no"]) +        self.assertIsNone(response["previous_page_no"]) + +    def test_returns_next_page_number(self): +        User.objects.create( +            id=10_001, +            name="user10001", +            discriminator=1111, +            in_guild=True +        ) +        url = reverse("bot:user-list", host="api") +        response = self.client.get(url).json() +        self.assertEqual(2, response["next_page_no"]) + +    def test_returns_previous_page_number(self): +        User.objects.create( +            id=10_001, +            name="user10001", +            discriminator=1111, +            in_guild=True +        ) +        url = reverse("bot:user-list", host="api") +        response = self.client.get(url, {"page": 2}).json() +        self.assertEqual(1, response["previous_page_no"]) diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..3e4b627e 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,21 +1,64 @@ +import typing +from collections import OrderedDict + +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer  from rest_framework.viewsets import ModelViewSet -from rest_framework_bulk import BulkCreateModelMixin  from pydis_site.apps.api.models.bot.user import User  from pydis_site.apps.api.serializers import UserSerializer -class UserViewSet(BulkCreateModelMixin, ModelViewSet): +class UserListPagination(PageNumberPagination): +    """Custom pagination class for the User Model.""" + +    page_size = 10000 +    page_size_query_param = "page_size" + +    def get_next_page_number(self) -> typing.Optional[int]: +        """Get the next page number.""" +        if not self.page.has_next(): +            return None +        page_number = self.page.next_page_number() +        return page_number + +    def get_previous_page_number(self) -> typing.Optional[int]: +        """Get the previous page number.""" +        if not self.page.has_previous(): +            return None + +        page_number = self.page.previous_page_number() +        return page_number + +    def get_paginated_response(self, data: list) -> Response: +        """Override method to send modified response.""" +        return Response(OrderedDict([ +            ('count', self.page.paginator.count), +            ('next_page_no', self.get_next_page_number()), +            ('previous_page_no', self.get_previous_page_number()), +            ('results', data) +        ])) + + +class UserViewSet(ModelViewSet):      """      View providing CRUD operations on Discord users through the bot.      ## Routes      ### GET /bot/users -    Returns all users currently known. +    Returns all users currently known with pagination.      #### Response format -    >>> [ -    ...     { +    >>> { +    ...     'count': 95000, +    ...     'next_page_no': "2", +    ...     'previous_page_no': None, +    ...     'results': [ +    ...      {      ...         'id': 409107086526644234,      ...         'name': "Python",      ...         'discriminator': 4329, @@ -26,8 +69,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      ...             458226699344019457      ...         ],      ...         'in_guild': True -    ...     } -    ... ] +    ...     }, +    ...     ] +    ... } + +    #### Optional Query Parameters +    - page_size: number of Users in one page, defaults to 10,000 +    - page: page number      #### Status codes      - 200: returned on success @@ -56,6 +104,7 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      ### POST /bot/users      Adds a single or multiple new users.      The roles attached to the user(s) must be roles known by the site. +    Users that already exist in the database will be skipped.      #### Request body      >>> { @@ -67,11 +116,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      ... }      Alternatively, request users can be POSTed as a list of above objects, -    in which case multiple users will be created at once. +    in which case multiple users will be created at once. In this case, +    the response is an empty list.      #### Status codes      - 201: returned on success      - 400: if one of the given roles does not exist, or one of the given fields is invalid +    - 400: if multiple user objects with the same id are given      ### PUT /bot/users/<snowflake:int>      Update the user with the given `snowflake`. @@ -109,6 +160,34 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      - 400: if the request body was invalid, see response body for details      - 404: if the user with the given `snowflake` could not be found +    ### BULK PATCH /bot/users/bulk_patch +    Update users with the given `ids` and `details`. +    `id` field and at least one other field is mandatory. + +    #### Request body +    >>> [ +    ...     { +    ...         'id': int, +    ...         'name': str, +    ...         'discriminator': int, +    ...         'roles': List[int], +    ...         'in_guild': bool +    ...     }, +    ...     { +    ...         'id': int, +    ...         'name': str, +    ...         'discriminator': int, +    ...         'roles': List[int], +    ...         'in_guild': bool +    ...     }, +    ... ] + +    #### Status codes +    - 200: returned on success +    - 400: if the request body was invalid, see response body for details +    - 400: if multiple user objects with the same id are given +    - 404: if the user with the given id does not exist +      ### DELETE /bot/users/<snowflake:int>      Deletes the user with the given `snowflake`. @@ -118,4 +197,27 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      """      serializer_class = UserSerializer -    queryset = User.objects +    queryset = User.objects.all().order_by("id") +    pagination_class = UserListPagination + +    def get_serializer(self, *args, **kwargs) -> ModelSerializer: +        """Set Serializer many attribute to True if request body contains a list.""" +        if isinstance(kwargs.get('data', {}), list): +            kwargs['many'] = True + +        return super().get_serializer(*args, **kwargs) + +    @action(detail=False, methods=["PATCH"], name='user-bulk-patch') +    def bulk_patch(self, request: Request) -> Response: +        """Update multiple User objects in a single request.""" +        serializer = self.get_serializer( +            instance=self.get_queryset(), +            data=request.data, +            many=True, +            partial=True +        ) + +        serializer.is_valid(raise_exception=True) +        serializer.save() + +        return Response(serializer.data, status=status.HTTP_200_OK) | 
