From 5b36c75e574c10a7b915b91664e564b3ed410a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 18 Sep 2020 11:02:26 +0200 Subject: [PATCH 1/6] reformat code with black --- Pipfile | 6 +- Pipfile.lock | 297 +++++++++++++++--- cancan/ability.py | 64 ++-- cancan/apps.py | 6 +- cancan/middleware.py | 25 +- cancan/testapp/migrations/0001_initial.py | 26 +- cancan/testapp/models.py | 3 +- cancan/testapp/tests/test_core.py | 73 +++-- cancan/testapp/tests/test_middleware.py | 10 +- cancan/testapp/tests/urls.py | 6 +- cancan/testapp/testsettings.py | 68 ++-- cancan/testapp/views.py | 4 +- example_project/abilities.py | 6 +- example_project/asgi.py | 2 +- example_project/manage.py | 4 +- example_project/sample/apps.py | 2 +- .../sample/migrations/0001_initial.py | 22 +- example_project/sample/models.py | 3 +- example_project/sample/views.py | 2 +- example_project/settings.py | 74 +++-- example_project/urls.py | 8 +- example_project/wsgi.py | 3 +- 22 files changed, 476 insertions(+), 238 deletions(-) diff --git a/Pipfile b/Pipfile index e86265f..c77cefa 100644 --- a/Pipfile +++ b/Pipfile @@ -12,10 +12,14 @@ django-environ = "*" setuptools = "*" wheel = "*" twine = "*" +black = "*" [requires] python_version = "3.7" [scripts] build = "python setup.py sdist bdist_wheel" -publish = "twine upload --skip-existing dist/*" \ No newline at end of file +publish = "twine upload --skip-existing dist/*" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index ddde820..24f13fd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "92347f6370ed224993619838c70f3ae1bf6aaa6d91c9f62c2668f6e4718552e8" + "sha256": "d4fe4bcf158db1e6ebcddcaef87c168ca113133baec07b5b960e471808d6d37e" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, "asgiref": { "hashes": [ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", @@ -23,13 +30,27 @@ ], "version": "==3.2.10" }, + "black": { + "hashes": [ + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + ], + "index": "pypi", + "version": "==20.8b1" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "version": "==7.1.2" + }, "django": { "hashes": [ - "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", - "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" + "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", + "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f" ], "index": "pypi", - "version": "==3.1" + "version": "==3.1.1" }, "django-environ": { "hashes": [ @@ -39,6 +60,20 @@ "index": "pypi", "version": "==0.4.5" }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, "pytz": { "hashes": [ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", @@ -46,15 +81,89 @@ ], "version": "==2020.1" }, + "regex": { + "hashes": [ + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" + }, "sqlparse": { "hashes": [ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], "version": "==0.3.1" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "version": "==3.7.4.3" } }, "develop": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, "autopep8": { "hashes": [ "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" @@ -62,12 +171,19 @@ "index": "pypi", "version": "==1.5.4" }, + "black": { + "hashes": [ + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + ], + "index": "pypi", + "version": "==20.8b1" + }, "bleach": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:769483204d247465c0b001ead257fb86bba6944bce6fe1b6759c812cceb54e3d", + "sha256:f9e0205cc57b558c21bdfc11034f9d96b14c4052c25be60885d94f4277c792e0" ], - "version": "==3.1.5" + "version": "==3.2.0" }, "certifi": { "hashes": [ @@ -78,36 +194,44 @@ }, "cffi": { "hashes": [ - "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", - "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", - "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", - "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", - "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", - "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", - "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", - "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", - "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", - "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", - "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", - "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", - "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", - "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", - "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", - "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", - "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", - "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", - "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", - "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", - "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", - "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", - "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", - "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", - "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", - "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", - "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", - "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" - ], - "version": "==1.14.2" + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" }, "chardet": { "hashes": [ @@ -116,6 +240,13 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "version": "==7.1.2" + }, "colorama": { "hashes": [ "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", @@ -187,6 +318,13 @@ ], "version": "==21.4.0" }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "packaging": { "hashes": [ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", @@ -194,6 +332,13 @@ ], "version": "==20.4" }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, "pkginfo": { "hashes": [ "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", @@ -217,17 +362,17 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], - "version": "==2.6.1" + "version": "==2.7.1" }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", + "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" ], - "version": "==2.4.7" + "version": "==3.0.0a2" }, "readme-renderer": { "hashes": [ @@ -236,6 +381,32 @@ ], "version": "==26.0" }, + "regex": { + "hashes": [ + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" + }, "requests": { "hashes": [ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", @@ -281,10 +452,10 @@ }, "tqdm": { "hashes": [ - "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", - "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" + "sha256:8f3c5815e3b5e20bc40463fa6b42a352178859692a68ffaa469706e6d38342a5", + "sha256:faf9c671bd3fad5ebaeee366949d969dca2b2be32c872a7092a1e1a9048d105b" ], - "version": "==4.48.2" + "version": "==4.49.0" }, "twine": { "hashes": [ @@ -294,6 +465,40 @@ "index": "pypi", "version": "==3.2.0" }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "version": "==3.7.4.3" + }, "urllib3": { "hashes": [ "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", diff --git a/cancan/ability.py b/cancan/ability.py index 916a6c7..cd618ca 100644 --- a/cancan/ability.py +++ b/cancan/ability.py @@ -11,23 +11,27 @@ def __init__(self, user): def can(self, action, model, **kwargs): if type(model) is str: model = apps.get_model(model) - self.abilities.append({ - 'type': 'can', - 'action': action, - 'model': model, - 'conditions': kwargs, - }) + self.abilities.append( + { + "type": "can", + "action": action, + "model": model, + "conditions": kwargs, + } + ) def cannot(self, action, model, **kwargs): if type(model) is str: model = apps.get_model(model) - self.abilities.append({ - 'type': 'cannot', - 'action': action, - 'model': model, - 'conditions': kwargs, - }) + self.abilities.append( + { + "type": "cannot", + "action": action, + "model": model, + "conditions": kwargs, + } + ) def set_alias(self, alias, action): self.aliases[alias] = action @@ -44,11 +48,13 @@ def validate_model(self, action, model): can_count = 0 cannot_count = 0 model_abilities = filter( - lambda c: c['model'] == model and c['action'] == action, self.ability.abilities) + lambda c: c["model"] == model and c["action"] == action, + self.ability.abilities, + ) for c in model_abilities: - if c['type'] == 'can': + if c["type"] == "can": can_count += 1 - if c['type'] == 'cannot': + if c["type"] == "cannot": cannot_count += 1 if cannot_count > 0: @@ -60,16 +66,19 @@ def validate_model(self, action, model): def validate_instance(self, action, instance): model = instance._meta.model model_abilities = filter( - lambda c: c['model'] == model and c['action'] == action, self.ability.abilities) + lambda c: c["model"] == model and c["action"] == action, + self.ability.abilities, + ) query_sets = [] for c in model_abilities: - if c['type'] == 'can': - qs = model.objects.all().filter(pk=instance.id, **c.get('conditions', {})) + if c["type"] == "can": + qs = model.objects.all().filter( + pk=instance.id, **c.get("conditions", {}) + ) - if c['type'] == 'cannot': - raise NotImplementedError( - 'cannot-type rules are not yet implemented') + if c["type"] == "cannot": + raise NotImplementedError("cannot-type rules are not yet implemented") query_sets.append(qs) @@ -93,16 +102,17 @@ def queryset_for(self, action, model): action = self.ability.alias_to_action(action) model_abilities = filter( - lambda c: c['model'] == model and c['action'] == action, self.ability.abilities) + lambda c: c["model"] == model and c["action"] == action, + self.ability.abilities, + ) query_sets = [] for c in model_abilities: - if c['type'] == 'can' and 'conditions' in c: - qs = model.objects.all().filter(**c.get('conditions', {})) + if c["type"] == "can" and "conditions" in c: + qs = model.objects.all().filter(**c.get("conditions", {})) - if c['type'] == 'cannot': - raise NotImplementedError( - 'cannot-type rules are not yet implemented') + if c["type"] == "cannot": + raise NotImplementedError("cannot-type rules are not yet implemented") query_sets.append(qs) diff --git a/cancan/apps.py b/cancan/apps.py index 6704f95..b5003da 100644 --- a/cancan/apps.py +++ b/cancan/apps.py @@ -2,8 +2,4 @@ class CanCanConfig(AppConfig): - name = 'cancan' - - def ready(self): - aaa() - pass + name = "cancan" diff --git a/cancan/middleware.py b/cancan/middleware.py index 077a94f..58d7f50 100644 --- a/cancan/middleware.py +++ b/cancan/middleware.py @@ -15,26 +15,25 @@ def get_validator(request, declare_abilities): class CanCanMiddleware(MiddlewareMixin): def process_request(self, request): - assert hasattr(request, 'user'), ( + assert hasattr(request, "user"), ( "Cancan authentication middleware requires authenticationMiddleware middleware " "to be installed. Edit your MIDDLEWARE setting to insert " "'django.contrib.auth.middleware.AuthenticationMiddleware' before." "'cancan.middleware.CanCanMiddleware'" ) - assert hasattr(settings, 'CANCAN'), ( - "CANCAN section not found in settings" - ) - assert 'ABILITIES' in settings.CANCAN, ( - "CANCAN['ABILITIES'] is missing. It must point to a function" - ) - fn_name = settings.CANCAN['ABILITIES'] + assert hasattr(settings, "CANCAN"), "CANCAN section not found in settings" + assert ( + "ABILITIES" in settings.CANCAN + ), "CANCAN['ABILITIES'] is missing. It must point to a function" + fn_name = settings.CANCAN["ABILITIES"] - declare_abilities = import_string(settings.CANCAN['ABILITIES']) + declare_abilities = import_string(settings.CANCAN["ABILITIES"]) - assert callable(declare_abilities), ( - f"{fn_name} must be callabe function fn(user: User, ability: Ability)" - ) + assert callable( + declare_abilities + ), f"{fn_name} must be callabe function fn(user: User, ability: Ability)" request.ability = SimpleLazyObject( - lambda: get_validator(request, declare_abilities)) + lambda: get_validator(request, declare_abilities) + ) diff --git a/cancan/testapp/migrations/0001_initial.py b/cancan/testapp/migrations/0001_initial.py index 9778ebf..26b8426 100644 --- a/cancan/testapp/migrations/0001_initial.py +++ b/cancan/testapp/migrations/0001_initial.py @@ -15,12 +15,28 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Article', + name="Article", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('is_published', models.BooleanField(default=False)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("is_published", models.BooleanField(default=False)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="articles", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/cancan/testapp/models.py b/cancan/testapp/models.py index c59c687..644acab 100644 --- a/cancan/testapp/models.py +++ b/cancan/testapp/models.py @@ -6,7 +6,8 @@ class Article(models.Model): name = models.CharField(max_length=255) is_published = models.BooleanField(default=False) created_by = models.ForeignKey( - User, on_delete=models.CASCADE, related_name='articles', null=True) + User, on_delete=models.CASCADE, related_name="articles", null=True + ) def get_absolute_url(self): return reverse("article_detail", kwargs={"pk": self.pk}) diff --git a/cancan/testapp/tests/test_core.py b/cancan/testapp/tests/test_core.py index dc52d5f..9af78e9 100644 --- a/cancan/testapp/tests/test_core.py +++ b/cancan/testapp/tests/test_core.py @@ -10,18 +10,18 @@ def setUp(self): def test_no_abilities_can_model(self): ability = Ability(user=self.user) validator = AbilityValidator(ability) - self.assertFalse(validator.can('view', Article)) + self.assertFalse(validator.can("view", Article)) def test_no_abilities_can_object(self): ability = Ability(user=self.user) validator = AbilityValidator(ability) article = Article.objects.create(name="foobar") - self.assertFalse(validator.can('view', article)) + self.assertFalse(validator.can("view", article)) def test_no_abilities_queryset_for(self): ability = Ability(user=self.user) validator = AbilityValidator(ability) - self.assertEqual(validator.queryset_for('view', Article).count(), 0) + self.assertEqual(validator.queryset_for("view", Article).count(), 0) class ModelAbilitiesTestCase(TestCase): @@ -31,82 +31,79 @@ def setUp(self): def test_no_abilities_when_initialized(self): ability = Ability(user=self.user) validator = AbilityValidator(ability) - self.assertFalse(validator.can('view', Article)) + self.assertFalse(validator.can("view", Article)) def test_happy_path(self): ability = Ability(user=self.user) - ability.can('view', Article) + ability.can("view", Article) validator = AbilityValidator(ability) - self.assertTrue(validator.can('view', Article)) + self.assertTrue(validator.can("view", Article)) def test_unknown_name(self): ability = Ability(user=self.user) - ability.can('view', Article) + ability.can("view", Article) validator = AbilityValidator(ability) - self.assertFalse(validator.can('read', Article)) + self.assertFalse(validator.can("read", Article)) class ObjectAbilitiesTestCase(TestCase): def setUp(self): - self.user = User.objects.create(username='user1') + self.user = User.objects.create(username="user1") ability = Ability(user=self.user) - ability.can('view', Article, is_published=True) + ability.can("view", Article, is_published=True) self.validator = AbilityValidator(ability) def test_can_view_published(self): - article = Article.objects.create(name='test', is_published=True) - self.assertTrue(self.validator.can( - 'view', article)) + article = Article.objects.create(name="test", is_published=True) + self.assertTrue(self.validator.can("view", article)) def test_cannot_view_unpublished(self): - article = Article.objects.create(name='test', is_published=False) - self.assertFalse(self.validator.can( - 'view', article)) + article = Article.objects.create(name="test", is_published=False) + self.assertFalse(self.validator.can("view", article)) def test_get_queryset1(self): - article1 = Article.objects.create(name='test', is_published=True) - qs = self.validator.queryset_for('view', Article) + article1 = Article.objects.create(name="test", is_published=True) + qs = self.validator.queryset_for("view", Article) self.assertEqual(qs.count(), 1) def test_get_queryset2(self): - article1 = Article.objects.create(name='test', is_published=False) - qs = self.validator.queryset_for('view', Article) + article1 = Article.objects.create(name="test", is_published=False) + qs = self.validator.queryset_for("view", Article) self.assertEqual(qs.count(), 0) class ObjectAbilitiesMultipleCanTestCase(TestCase): def setUp(self): - self.user = User.objects.create(username='user1') + self.user = User.objects.create(username="user1") ability = Ability(user=self.user) - ability.can('view', Article, is_published=True) - ability.can('view', Article, created_by=self.user) + ability.can("view", Article, is_published=True) + ability.can("view", Article, created_by=self.user) self.validator = AbilityValidator(ability) def test_can_view_published(self): - article1 = Article.objects.create(name='test', is_published=True) - article2 = Article.objects.create(name='test', created_by=self.user) - self.assertTrue(self.validator.can('view', article1)) - self.assertTrue(self.validator.can('view', article2)) + article1 = Article.objects.create(name="test", is_published=True) + article2 = Article.objects.create(name="test", created_by=self.user) + self.assertTrue(self.validator.can("view", article1)) + self.assertTrue(self.validator.can("view", article2)) def test_queryset_contails_all_allowed(self): - article1 = Article.objects.create(name='test', is_published=True) - article2 = Article.objects.create(name='test', created_by=self.user) - article3 = Article.objects.create(name='test', is_published=False) - qs = self.validator.queryset_for('view', Article) + article1 = Article.objects.create(name="test", is_published=True) + article2 = Article.objects.create(name="test", created_by=self.user) + article3 = Article.objects.create(name="test", is_published=False) + qs = self.validator.queryset_for("view", Article) self.assertEqual(qs.count(), 2) self.assertFalse(article3 in qs.all()) class AliasesTestCase(TestCase): def setUp(self): - self.user = User.objects.create(username='user1') - article1 = Article.objects.create(name='test') + self.user = User.objects.create(username="user1") + article1 = Article.objects.create(name="test") ability = Ability(user=self.user) - ability.can('view', Article) - ability.set_alias('list', 'view') + ability.can("view", Article) + ability.set_alias("list", "view") self.validator = AbilityValidator(ability) def test_can_view_published(self): - self.assertTrue(self.validator.can('list', Article)) - self.assertEqual(self.validator.queryset_for( - 'list', Article).count(), 1) + self.assertTrue(self.validator.can("list", Article)) + self.assertEqual(self.validator.queryset_for("list", Article).count(), 1) diff --git a/cancan/testapp/tests/test_middleware.py b/cancan/testapp/tests/test_middleware.py index 85c8294..96228a5 100644 --- a/cancan/testapp/tests/test_middleware.py +++ b/cancan/testapp/tests/test_middleware.py @@ -4,10 +4,12 @@ def get_abilities(user, ability): - ability.can('view', Article, is_published=True) + ability.can("view", Article, is_published=True) -@override_settings(CANCAN={'ABILITIES': 'cancan.testapp.tests.test_middleware.get_abilities'}) +@override_settings( + CANCAN={"ABILITIES": "cancan.testapp.tests.test_middleware.get_abilities"} +) class MiddlewareTestCase(TestCase): def setUp(self): self.article1 = Article.objects.create(is_published=True) @@ -15,8 +17,8 @@ def setUp(self): def test_list_view(self): c = Client() - response = c.get('/articles/') - ids = response.content.decode().split(' ') + response = c.get("/articles/") + ids = response.content.decode().split(" ") self.assertEqual(response.status_code, 200) self.assertIn(str(self.article1.id), ids) self.assertNotIn(str(self.article2.id), ids) diff --git a/cancan/testapp/tests/urls.py b/cancan/testapp/tests/urls.py index c68886e..8eb1ef7 100644 --- a/cancan/testapp/tests/urls.py +++ b/cancan/testapp/tests/urls.py @@ -13,7 +13,7 @@ urlpatterns = [ - path('admin/', admin.site.urls), - path('accounts/login/', LoginView.as_view(template_name='blank.html')), - path('articles/', ArticleListView.as_view()) + path("admin/", admin.site.urls), + path("accounts/login/", LoginView.as_view(template_name="blank.html")), + path("articles/", ArticleListView.as_view()), ] diff --git a/cancan/testapp/testsettings.py b/cancan/testapp/testsettings.py index 1acc805..4000b05 100644 --- a/cancan/testapp/testsettings.py +++ b/cancan/testapp/testsettings.py @@ -10,63 +10,59 @@ ANONYMOUS_USER_NAME = "AnonymousUser" INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.admin', - 'django.contrib.messages', - 'cancan', - 'cancan.testapp', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.admin", + "django.contrib.messages", + "cancan", + "cancan.testapp", ) -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', -) +AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'cancan.middleware.CanCanMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "cancan.middleware.CanCanMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ) -CANCAN = {'ABILITIES': 'cancan.testapp.abilities.get_abilities'} +CANCAN = {"ABILITIES": "cancan.testapp.abilities.get_abilities"} # TEST_RUNNER = 'django.test.runner.DiscoverRunner' -ROOT_URLCONF = 'cancan.testapp.tests.urls' +ROOT_URLCONF = "cancan.testapp.tests.urls" SITE_ID = 1 -SECRET_KEY = ''.join([random.choice(string.ascii_letters) for x in range(40)]) +SECRET_KEY = "".join([random.choice(string.ascii_letters) for x in range(40)]) # Database specific # DATABASES = {'default': env.db(default="sqlite:///")} DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': './test.db', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "./test.db", } } TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ( - os.path.join(os.path.dirname(__file__), 'tests', 'templates'), - ), - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": (os.path.join(os.path.dirname(__file__), "tests", "templates"),), + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], }, }, diff --git a/cancan/testapp/views.py b/cancan/testapp/views.py index db75533..90a01bf 100644 --- a/cancan/testapp/views.py +++ b/cancan/testapp/views.py @@ -8,7 +8,7 @@ class ArticleListView(ListView): def get_queryset(self): # this is how you can retrieve all objects a user can access - qs = self.request.ability.queryset_for('view', Article) + qs = self.request.ability.queryset_for("view", Article) return qs @@ -18,4 +18,4 @@ class ArticleDetailView(PermissionRequiredMixin, DetailView): def has_permission(self): article = self.get_object() # this is how you can check if user can access an object - return self.request.ability.can('view', article) + return self.request.ability.can("view", article) diff --git a/example_project/abilities.py b/example_project/abilities.py index 041309d..c5ff6ff 100644 --- a/example_project/abilities.py +++ b/example_project/abilities.py @@ -10,8 +10,8 @@ def declare_abilities(user, ability): # after can, you can put cannot which will be overriden by following can # Logged in user can view his own todos - ability.can('view', TodoItem, created_by=user.id) + ability.can("view", TodoItem, created_by=user.id) - if user.has_perm('sample.view_todo'): + if user.has_perm("sample.view_todo"): # OR condition - allow to view all todo items - ability.can('view', TodoItem) + ability.can("view", TodoItem) diff --git a/example_project/asgi.py b/example_project/asgi.py index 132b4bb..714e8d0 100644 --- a/example_project/asgi.py +++ b/example_project/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") application = get_asgi_application() diff --git a/example_project/manage.py b/example_project/manage.py index 1b243ab..9e94ce3 100755 --- a/example_project/manage.py +++ b/example_project/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/example_project/sample/apps.py b/example_project/sample/apps.py index f85f94a..79a288c 100644 --- a/example_project/sample/apps.py +++ b/example_project/sample/apps.py @@ -2,4 +2,4 @@ class SampleConfig(AppConfig): - name = 'sample' + name = "sample" diff --git a/example_project/sample/migrations/0001_initial.py b/example_project/sample/migrations/0001_initial.py index e31bdc0..aabebd4 100644 --- a/example_project/sample/migrations/0001_initial.py +++ b/example_project/sample/migrations/0001_initial.py @@ -15,11 +15,25 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='TodoItem', + name="TodoItem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/example_project/sample/models.py b/example_project/sample/models.py index 0f07bb9..82240d5 100644 --- a/example_project/sample/models.py +++ b/example_project/sample/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import User + # Create your models here. @@ -8,4 +9,4 @@ class TodoItem(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): - return f'#{self.id} {self.name} (by {self.created_by.username})' + return f"#{self.id} {self.name} (by {self.created_by.username})" diff --git a/example_project/sample/views.py b/example_project/sample/views.py index b637ab3..b7ceee9 100644 --- a/example_project/sample/views.py +++ b/example_project/sample/views.py @@ -20,4 +20,4 @@ class TodoDetailView(PermissionRequiredMixin, DetailView): def has_permission(self): obj = self.get_object() - return self.request.user.can('view', obj) + return self.request.user.can("view", obj) diff --git a/example_project/settings.py b/example_project/settings.py index 55a89d4..c236899 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -20,7 +20,7 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve(strict=True).parent -CANCAN_MODULE_PATH = abspath(BASE_DIR, '..') +CANCAN_MODULE_PATH = abspath(BASE_DIR, "..") sys.path.insert(0, CANCAN_MODULE_PATH) @@ -28,7 +28,7 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '*c+8wbdcba%@-kszf7s+-q^@=g#f+gj4xw^=a0ab%o*lnd%vkd' +SECRET_KEY = "*c+8wbdcba%@-kszf7s+-q^@=g#f+gj4xw^=a0ab%o*lnd%vkd" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -38,55 +38,55 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'cancan', - 'sample' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "cancan", + "sample", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'cancan.middleware.CanCanMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "cancan.middleware.CanCanMiddleware", ] -ROOT_URLCONF = 'urls' +ROOT_URLCONF = "urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'wsgi.application' +WSGI_APPLICATION = "wsgi.application" # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -113,9 +113,9 @@ # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -124,12 +124,10 @@ USE_TZ = True -CANCAN = { - 'ABILITIES': 'abilities.declare_abilities' -} +CANCAN = {"ABILITIES": "abilities.declare_abilities"} # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/example_project/urls.py b/example_project/urls.py index 1daa81e..5b375f6 100644 --- a/example_project/urls.py +++ b/example_project/urls.py @@ -18,8 +18,8 @@ from sample.views import TodoIndexView, TodoDetailView urlpatterns = [ - path('admin/', admin.site.urls), - path('accounts/', include('django.contrib.auth.urls')), - path('', TodoIndexView.as_view()), - path('todo//', TodoDetailView.as_view()), + path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), + path("", TodoIndexView.as_view()), + path("todo//", TodoDetailView.as_view()), ] diff --git a/example_project/wsgi.py b/example_project/wsgi.py index 5bb0228..00d7110 100644 --- a/example_project/wsgi.py +++ b/example_project/wsgi.py @@ -11,7 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', - 'settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") application = get_wsgi_application() From 458a7d2757fd2b082878dfaaeec4f707f037f17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Wed, 23 Sep 2020 12:56:05 +0200 Subject: [PATCH 2/6] changes in class names --- cancan/ability.py | 53 +++------------- cancan/access_rules.py | 23 +++++++ cancan/middleware.py | 10 +-- cancan/testapp/tests/test_core.py | 83 +++++++++++++------------ cancan/testapp/tests/test_middleware.py | 6 +- 5 files changed, 81 insertions(+), 94 deletions(-) create mode 100644 cancan/access_rules.py diff --git a/cancan/ability.py b/cancan/ability.py index cd618ca..270b74f 100644 --- a/cancan/ability.py +++ b/cancan/ability.py @@ -1,55 +1,18 @@ import inspect from django.apps import apps +from .access_rules import AccessRules class Ability: - def __init__(self, user): - self.user = user - self.abilities = [] - self.aliases = {} - - def can(self, action, model, **kwargs): - if type(model) is str: - model = apps.get_model(model) - self.abilities.append( - { - "type": "can", - "action": action, - "model": model, - "conditions": kwargs, - } - ) - - def cannot(self, action, model, **kwargs): - if type(model) is str: - model = apps.get_model(model) - - self.abilities.append( - { - "type": "cannot", - "action": action, - "model": model, - "conditions": kwargs, - } - ) - - def set_alias(self, alias, action): - self.aliases[alias] = action - - def alias_to_action(self, alias): - return self.aliases.get(alias, alias) - - -class AbilityValidator: - def __init__(self, ability: Ability): - self.ability = ability + def __init__(self, access_rules: AccessRules): + self.access_rules = access_rules def validate_model(self, action, model): can_count = 0 cannot_count = 0 model_abilities = filter( lambda c: c["model"] == model and c["action"] == action, - self.ability.abilities, + self.access_rules.rules, ) for c in model_abilities: if c["type"] == "can": @@ -67,7 +30,7 @@ def validate_instance(self, action, instance): model = instance._meta.model model_abilities = filter( lambda c: c["model"] == model and c["action"] == action, - self.ability.abilities, + self.access_rules.rules, ) query_sets = [] @@ -92,18 +55,18 @@ def validate_instance(self, action, instance): return can_query_set.count() > 0 def can(self, action, model_or_instance) -> bool: - action = self.ability.alias_to_action(action) + action = self.access_rules.alias_to_action(action) if inspect.isclass(model_or_instance): return self.validate_model(action, model_or_instance) else: return self.validate_instance(action, model_or_instance) def queryset_for(self, action, model): - action = self.ability.alias_to_action(action) + action = self.access_rules.alias_to_action(action) model_abilities = filter( lambda c: c["model"] == model and c["action"] == action, - self.ability.abilities, + self.access_rules.rules, ) query_sets = [] diff --git a/cancan/access_rules.py b/cancan/access_rules.py new file mode 100644 index 0000000..0639c42 --- /dev/null +++ b/cancan/access_rules.py @@ -0,0 +1,23 @@ +class AccessRules: + def __init__(self, user): + self.user = user + self.rules = [] + self.action_aliases = {} + + def allow(self, action, subject, **kwargs): + if type(subject) is str: + model = apps.get_model(subject) + rule = { + "type": "can", + "action": action, + "model": subject, + "conditions": kwargs, + } + self.rules.append(rule) + return rule + + def alias_action(self, action, alias): + self.action_aliases[alias] = action + + def alias_to_action(self, alias): + return self.action_aliases.get(alias, alias) \ No newline at end of file diff --git a/cancan/middleware.py b/cancan/middleware.py index 58d7f50..94c67a0 100644 --- a/cancan/middleware.py +++ b/cancan/middleware.py @@ -3,14 +3,14 @@ from django.utils.deprecation import MiddlewareMixin from django.utils.functional import SimpleLazyObject from django.utils.module_loading import import_string - -from .ability import Ability, AbilityValidator +from .access_rules import AccessRules +from .ability import Ability def get_validator(request, declare_abilities): - ability = Ability(request.user) - declare_abilities(request.user, ability) - return AbilityValidator(ability) + access_rules = AccessRules(request.user) + declare_abilities(request.user, access_rules) + return Ability(access_rules) class CanCanMiddleware(MiddlewareMixin): diff --git a/cancan/testapp/tests/test_core.py b/cancan/testapp/tests/test_core.py index 9af78e9..14754dd 100644 --- a/cancan/testapp/tests/test_core.py +++ b/cancan/testapp/tests/test_core.py @@ -1,6 +1,7 @@ from django.test import TestCase from cancan.testapp.models import Article, User -from cancan.ability import Ability, AbilityValidator +from cancan.ability import Ability +from cancan.access_rules import AccessRules class NoAbilitiesTestCase(TestCase): @@ -8,20 +9,20 @@ def setUp(self): self.user = User.objects.create(username="user1") def test_no_abilities_can_model(self): - ability = Ability(user=self.user) - validator = AbilityValidator(ability) - self.assertFalse(validator.can("view", Article)) + access_rules = AccessRules(user=self.user) + ability = Ability(access_rules) + self.assertFalse(ability.can("view", Article)) def test_no_abilities_can_object(self): - ability = Ability(user=self.user) - validator = AbilityValidator(ability) + access_rules = AccessRules(user=self.user) + ability = Ability(access_rules) article = Article.objects.create(name="foobar") - self.assertFalse(validator.can("view", article)) + self.assertFalse(ability.can("view", article)) def test_no_abilities_queryset_for(self): - ability = Ability(user=self.user) - validator = AbilityValidator(ability) - self.assertEqual(validator.queryset_for("view", Article).count(), 0) + access_rules = AccessRules(user=self.user) + ability = Ability(access_rules) + self.assertEqual(ability.queryset_for("view", Article).count(), 0) class ModelAbilitiesTestCase(TestCase): @@ -29,68 +30,68 @@ def setUp(self): self.user = User.objects.create(username="user1") def test_no_abilities_when_initialized(self): - ability = Ability(user=self.user) - validator = AbilityValidator(ability) - self.assertFalse(validator.can("view", Article)) + access_rules = AccessRules(user=self.user) + ability = Ability(access_rules) + self.assertFalse(ability.can("view", Article)) def test_happy_path(self): - ability = Ability(user=self.user) - ability.can("view", Article) - validator = AbilityValidator(ability) - self.assertTrue(validator.can("view", Article)) + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article) + ability = Ability(access_rules) + self.assertTrue(ability.can("view", Article)) def test_unknown_name(self): - ability = Ability(user=self.user) - ability.can("view", Article) - validator = AbilityValidator(ability) - self.assertFalse(validator.can("read", Article)) + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article) + ability = Ability(access_rules) + self.assertFalse(ability.can("read", Article)) class ObjectAbilitiesTestCase(TestCase): def setUp(self): self.user = User.objects.create(username="user1") - ability = Ability(user=self.user) - ability.can("view", Article, is_published=True) - self.validator = AbilityValidator(ability) + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article, is_published=True) + self.ability = Ability(access_rules) def test_can_view_published(self): article = Article.objects.create(name="test", is_published=True) - self.assertTrue(self.validator.can("view", article)) + self.assertTrue(self.ability.can("view", article)) def test_cannot_view_unpublished(self): article = Article.objects.create(name="test", is_published=False) - self.assertFalse(self.validator.can("view", article)) + self.assertFalse(self.ability.can("view", article)) def test_get_queryset1(self): article1 = Article.objects.create(name="test", is_published=True) - qs = self.validator.queryset_for("view", Article) + qs = self.ability.queryset_for("view", Article) self.assertEqual(qs.count(), 1) def test_get_queryset2(self): article1 = Article.objects.create(name="test", is_published=False) - qs = self.validator.queryset_for("view", Article) + qs = self.ability.queryset_for("view", Article) self.assertEqual(qs.count(), 0) class ObjectAbilitiesMultipleCanTestCase(TestCase): def setUp(self): self.user = User.objects.create(username="user1") - ability = Ability(user=self.user) - ability.can("view", Article, is_published=True) - ability.can("view", Article, created_by=self.user) - self.validator = AbilityValidator(ability) + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article, is_published=True) + access_rules.allow("view", Article, created_by=self.user) + self.ability = Ability(access_rules) def test_can_view_published(self): article1 = Article.objects.create(name="test", is_published=True) article2 = Article.objects.create(name="test", created_by=self.user) - self.assertTrue(self.validator.can("view", article1)) - self.assertTrue(self.validator.can("view", article2)) + self.assertTrue(self.ability.can("view", article1)) + self.assertTrue(self.ability.can("view", article2)) def test_queryset_contails_all_allowed(self): article1 = Article.objects.create(name="test", is_published=True) article2 = Article.objects.create(name="test", created_by=self.user) article3 = Article.objects.create(name="test", is_published=False) - qs = self.validator.queryset_for("view", Article) + qs = self.ability.queryset_for("view", Article) self.assertEqual(qs.count(), 2) self.assertFalse(article3 in qs.all()) @@ -99,11 +100,11 @@ class AliasesTestCase(TestCase): def setUp(self): self.user = User.objects.create(username="user1") article1 = Article.objects.create(name="test") - ability = Ability(user=self.user) - ability.can("view", Article) - ability.set_alias("list", "view") - self.validator = AbilityValidator(ability) + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article) + access_rules.alias_action("view", "list") + self.ability = Ability(access_rules) def test_can_view_published(self): - self.assertTrue(self.validator.can("list", Article)) - self.assertEqual(self.validator.queryset_for("list", Article).count(), 1) + self.assertTrue(self.ability.can("list", Article)) + self.assertEqual(self.ability.queryset_for("list", Article).count(), 1) diff --git a/cancan/testapp/tests/test_middleware.py b/cancan/testapp/tests/test_middleware.py index 96228a5..c82f3b5 100644 --- a/cancan/testapp/tests/test_middleware.py +++ b/cancan/testapp/tests/test_middleware.py @@ -1,10 +1,10 @@ from django.test import TestCase, Client, override_settings from cancan.testapp.models import Article, User -from cancan.ability import Ability, AbilityValidator +from cancan.ability import Ability, AccessRules -def get_abilities(user, ability): - ability.can("view", Article, is_published=True) +def get_abilities(user, rules): + rules.allow("view", Article, is_published=True) @override_settings( From dcbbcf987c222be63b2fc1a5e2af1a32e84f46c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Wed, 23 Sep 2020 14:49:35 +0200 Subject: [PATCH 3/6] WIP --- .gitignore | 2 + cancan/access_rules.py | 3 + cancan/context_processors.py | 2 + .../templatetags}/__init__.py | 0 cancan/templatetags/cancan_tags.py | 15 +++ example_project/Pipfile | 4 +- example_project/Pipfile.lock | 111 +++++++++++++++++- example_project/README.md | 34 +++++- example_project/abilities.py | 21 ++-- example_project/conftest.py | 6 + .../{sample/migrations => core}/__init__.py | 0 example_project/core/admin.py | 7 ++ example_project/core/apps.py | 5 + .../core/migrations/0001_initial.py | 53 +++++++++ example_project/core/migrations/__init__.py | 0 example_project/core/models.py | 32 +++++ .../core/templates/core/project_detail.html | 7 ++ .../core/templates/core/project_form.html | 16 +++ .../core/templates/core/project_list.html | 32 +++++ example_project/core/templates/home.html | 7 ++ example_project/core/tests.py | 42 +++++++ example_project/core/views.py | 47 ++++++++ example_project/pytest.ini | 4 + example_project/sample/admin.py | 5 - example_project/sample/apps.py | 5 - .../sample/migrations/0001_initial.py | 39 ------ example_project/sample/models.py | 12 -- .../templates/sample/todoitem_detail.html | 12 -- .../templates/sample/todoitem_list.html | 18 --- example_project/sample/tests.py | 3 - example_project/sample/views.py | 23 ---- example_project/settings.py | 6 +- example_project/urls.py | 28 ++--- 33 files changed, 445 insertions(+), 156 deletions(-) create mode 100644 cancan/context_processors.py rename {example_project/sample => cancan/templatetags}/__init__.py (100%) create mode 100644 cancan/templatetags/cancan_tags.py create mode 100644 example_project/conftest.py rename example_project/{sample/migrations => core}/__init__.py (100%) create mode 100644 example_project/core/admin.py create mode 100644 example_project/core/apps.py create mode 100644 example_project/core/migrations/0001_initial.py create mode 100644 example_project/core/migrations/__init__.py create mode 100644 example_project/core/models.py create mode 100644 example_project/core/templates/core/project_detail.html create mode 100644 example_project/core/templates/core/project_form.html create mode 100644 example_project/core/templates/core/project_list.html create mode 100644 example_project/core/templates/home.html create mode 100644 example_project/core/tests.py create mode 100644 example_project/core/views.py create mode 100644 example_project/pytest.ini delete mode 100644 example_project/sample/admin.py delete mode 100644 example_project/sample/apps.py delete mode 100644 example_project/sample/migrations/0001_initial.py delete mode 100644 example_project/sample/models.py delete mode 100644 example_project/sample/templates/sample/todoitem_detail.html delete mode 100644 example_project/sample/templates/sample/todoitem_list.html delete mode 100644 example_project/sample/tests.py delete mode 100644 example_project/sample/views.py diff --git a/.gitignore b/.gitignore index 2f54b33..a138844 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .vscode/ .pytest_cache/ .pyc +*.sqlite3 +*.db build/ dist/ django_cancan.egg-info/ diff --git a/cancan/access_rules.py b/cancan/access_rules.py index 0639c42..cd72710 100644 --- a/cancan/access_rules.py +++ b/cancan/access_rules.py @@ -1,3 +1,6 @@ +from django.apps import apps + + class AccessRules: def __init__(self, user): self.user = user diff --git a/cancan/context_processors.py b/cancan/context_processors.py new file mode 100644 index 0000000..460af89 --- /dev/null +++ b/cancan/context_processors.py @@ -0,0 +1,2 @@ +def abilities(request): + return {"abilities": request.ability.access_rules.rules} diff --git a/example_project/sample/__init__.py b/cancan/templatetags/__init__.py similarity index 100% rename from example_project/sample/__init__.py rename to cancan/templatetags/__init__.py diff --git a/cancan/templatetags/cancan_tags.py b/cancan/templatetags/cancan_tags.py new file mode 100644 index 0000000..2d8ffe9 --- /dev/null +++ b/cancan/templatetags/cancan_tags.py @@ -0,0 +1,15 @@ +from django import template +from django.apps import apps +from django.utils.safestring import SafeString + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def can(context, action, subject): + is_allowed = False + if type(subject) is SafeString: + model = apps.get_model(subject) + print(model) + is_allowed = context["request"].ability.can(action, model) + return f"can {action} {subject}? {is_allowed}" diff --git a/example_project/Pipfile b/example_project/Pipfile index 02d53d1..f8c5973 100644 --- a/example_project/Pipfile +++ b/example_project/Pipfile @@ -7,6 +7,8 @@ verify_ssl = true [packages] django = "*" +django-bulma = "*" +pytest-django = "*" [requires] -python_version = "3.8" +python_version = "3.7" diff --git a/example_project/Pipfile.lock b/example_project/Pipfile.lock index a693b8f..511f084 100644 --- a/example_project/Pipfile.lock +++ b/example_project/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "99c4b9ec1b8891ff787677276760beb6d6d4919c55660da1c713682156a6086c" + "sha256": "9b588aa7cd657029acff3bb52771cbdeb02b970459efd2faef0733921404c32a" }, "pipfile-spec": 6, "requires": { - "python_version": "3.8" + "python_version": "3.7" }, "sources": [ { @@ -23,13 +23,93 @@ ], "version": "==3.2.10" }, + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "version": "==20.2.0" + }, "django": { "hashes": [ - "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", - "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" + "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", + "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f" + ], + "index": "pypi", + "version": "==3.1.1" + }, + "django-bulma": { + "hashes": [ + "sha256:369270933a3150ba7e5f85a160405a1713b405950b6082d72e003548d3403f1e", + "sha256:438ce96cb95228bb3fa30e959255f27bf9443ff5c441843b1e150103cc3205ec" + ], + "index": "pypi", + "version": "==0.8.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", + "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" + ], + "markers": "python_version < '3.8'", + "version": "==2.0.0" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, + "more-itertools": { + "hashes": [ + "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", + "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" + ], + "version": "==8.5.0" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "version": "==20.4" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "version": "==1.9.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", + "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" + ], + "version": "==6.0.2" + }, + "pytest-django": { + "hashes": [ + "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6", + "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4" ], "index": "pypi", - "version": "==3.1" + "version": "==3.10.0" }, "pytz": { "hashes": [ @@ -38,12 +118,33 @@ ], "version": "==2020.1" }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "version": "==1.15.0" + }, "sqlparse": { "hashes": [ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], "version": "==0.3.1" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "zipp": { + "hashes": [ + "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6", + "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f" + ], + "version": "==3.2.0" } }, "develop": {} diff --git a/example_project/README.md b/example_project/README.md index 08843a0..6092215 100644 --- a/example_project/README.md +++ b/example_project/README.md @@ -1 +1,33 @@ -# This is readme for example_project +# Example project + +In this example project we are going to build a simple project management software. +Logged in users will be able to create projects. When a `Project` is created by a `User`, this user becomes a project's owner. +A project owner can add other users to a project and specify their role in a project (member or reporter). +Project members can create and resolve issues, reporters can only create issues. + +Since we are build + +## Access permissions + +Superuser - can do everything + +Project owner - can create new projects, manage project members, CRUD all projects and issues + +- can create new project +- view own projects +- can manage own project including: + - changing project details (name, description) + - managing (CRUD) project members + - managing (CRUD) project issues, including assigning members to issues + +Project member can: + +- view projects he belongs to, +- create issues in a projects he belongs to, +- edit own issues, including assigning issues to other project members, +- change status of issues he is assigned to, + +Project reporter: + +- can view projects he is assigned to, +- can create issues in a projects he is assigned to, diff --git a/example_project/abilities.py b/example_project/abilities.py index c5ff6ff..b7366b9 100644 --- a/example_project/abilities.py +++ b/example_project/abilities.py @@ -1,17 +1,14 @@ -from sample.models import TodoItem +from core.models import Project, Membership, Issue -def declare_abilities(user, ability): - if not user.is_authenticated: +def declare_abilities(user, rules): + if not user.is_authenticated or not user.is_active: + # anonymous/inactive users can do nothing return False - # TODO: - # multiple can will be OR'ed - # after can, you can put cannot which will be overriden by following can + # logged in can view own content + rules.allow("add", Project) + rules.allow("view", Project, created_by=user) - # Logged in user can view his own todos - ability.can("view", TodoItem, created_by=user.id) - - if user.has_perm("sample.view_todo"): - # OR condition - allow to view all todo items - ability.can("view", TodoItem) + if user.is_superuser: + rules.allow("view", Project) diff --git a/example_project/conftest.py b/example_project/conftest.py new file mode 100644 index 0000000..7cc009d --- /dev/null +++ b/example_project/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + pass \ No newline at end of file diff --git a/example_project/sample/migrations/__init__.py b/example_project/core/__init__.py similarity index 100% rename from example_project/sample/migrations/__init__.py rename to example_project/core/__init__.py diff --git a/example_project/core/admin.py b/example_project/core/admin.py new file mode 100644 index 0000000..40edf78 --- /dev/null +++ b/example_project/core/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Project, Membership, Issue + + +admin.site.register(Project) +admin.site.register(Membership) +admin.site.register(Issue) diff --git a/example_project/core/apps.py b/example_project/core/apps.py new file mode 100644 index 0000000..5ef1d60 --- /dev/null +++ b/example_project/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" diff --git a/example_project/core/migrations/0001_initial.py b/example_project/core/migrations/0001_initial.py new file mode 100644 index 0000000..d9996a1 --- /dev/null +++ b/example_project/core/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 3.1.1 on 2020-09-18 11:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True, default='')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)), + ('members', models.ManyToManyField(through='core.Membership', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='membership', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.project'), + ), + migrations.AddField( + model_name='membership', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='Issue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('description', models.TextField(blank=True, default='')), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues_assigned', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues_created', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/example_project/core/migrations/__init__.py b/example_project/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example_project/core/models.py b/example_project/core/models.py new file mode 100644 index 0000000..40d29cb --- /dev/null +++ b/example_project/core/models.py @@ -0,0 +1,32 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Project(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(default="", blank=True) + members = models.ManyToManyField(User, through="Membership") + created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owner") + + def get_absolute_url(self): + return reverse("project-detail", kwargs={"pk": self.pk}) + + +class Membership(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + project = models.ForeignKey(Project, on_delete=models.CASCADE) + + +class Issue(models.Model): + name = models.CharField(max_length=128) + description = models.TextField(default="", blank=True) + assigned_to = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="issues_assigned", + ) + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="issues_created" + ) diff --git a/example_project/core/templates/core/project_detail.html b/example_project/core/templates/core/project_detail.html new file mode 100644 index 0000000..59061b8 --- /dev/null +++ b/example_project/core/templates/core/project_detail.html @@ -0,0 +1,7 @@ +{% extends 'bulma/base.html' %} + +{% block title %}{{object.name}}{% endblock %} + +{% block content %} +
TODO: project details
+{% endblock content %} \ No newline at end of file diff --git a/example_project/core/templates/core/project_form.html b/example_project/core/templates/core/project_form.html new file mode 100644 index 0000000..d92e462 --- /dev/null +++ b/example_project/core/templates/core/project_form.html @@ -0,0 +1,16 @@ +{% extends 'bulma/base.html' %} +{% load bulma_tags %} +{% load cancan_tags %} + +{% block title %}Project Form{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form|bulma }} +
+ +
+ +
+{% endblock content %} \ No newline at end of file diff --git a/example_project/core/templates/core/project_list.html b/example_project/core/templates/core/project_list.html new file mode 100644 index 0000000..0b945b6 --- /dev/null +++ b/example_project/core/templates/core/project_list.html @@ -0,0 +1,32 @@ +{% extends 'bulma/base.html' %} +{% load cancan_tags %} + +{% block title %}Your projects{% endblock %} + +{% block content %} +--->{{ abilities }}<--- +

Your projects

+
    + {% for project in object_list %} +
  • +
    +
    + {{project.name}} +
    +
    +
    + {{project.description}} +
    +
    +
    +
  • + {% endfor %} + + {% can "view" "core.Project" as is_allowed %} + {% if is_allowed %} +
  • + Create new project +
  • + {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/example_project/core/templates/home.html b/example_project/core/templates/home.html new file mode 100644 index 0000000..1fa0b87 --- /dev/null +++ b/example_project/core/templates/home.html @@ -0,0 +1,7 @@ +{% extends 'bulma/base.html' %} + +{% block title %}{{object.name}}{% endblock %} + +{% block content %} +

django-cancan example project. Please log-in to continue

+{% endblock content %} \ No newline at end of file diff --git a/example_project/core/tests.py b/example_project/core/tests.py new file mode 100644 index 0000000..01d03b2 --- /dev/null +++ b/example_project/core/tests.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import AnonymousUser, User +from cancan.middleware import CanCanMiddleware +from core.views import ProjectListView +from core.models import Project + + +def test_login_required_for_projects_page(rf): + request = rf.get("/projects/") + request.user = AnonymousUser() + CanCanMiddleware().process_request(request) + response = ProjectListView.as_view()(request) + assert response.status_code == 302 + + +def test_user_can_see_his_own_projects(rf): + alice = User.objects.create(username="alice") + bob = User.objects.create(username="bob") + + project1 = Project.objects.create(name="Project 1", created_by=alice) + project2 = Project.objects.create(name="Project 2", created_by=bob) + + request = rf.get("/projects/") + request.user = alice + CanCanMiddleware().process_request(request) + response = ProjectListView.as_view()(request) + assert response.status_code == 200 + assert response.context_data["object_list"].count() == 1 + + +def test_admin_can_see_his_own_projects(rf): + alice = User.objects.create(username="alice", is_superuser=True) + bob = User.objects.create(username="bob") + + project1 = Project.objects.create(name="Project 1", created_by=alice) + project2 = Project.objects.create(name="Project 2", created_by=bob) + + request = rf.get("/projects/") + request.user = alice + CanCanMiddleware().process_request(request) + response = ProjectListView.as_view()(request) + assert response.status_code == 200 + assert response.context_data["object_list"].count() == 2 diff --git a/example_project/core/views.py b/example_project/core/views.py new file mode 100644 index 0000000..a270c99 --- /dev/null +++ b/example_project/core/views.py @@ -0,0 +1,47 @@ +from django.shortcuts import render +from django.urls import reverse +from django.contrib.auth.mixins import UserPassesTestMixin +from django.views.generic import ListView, DetailView, TemplateView +from django.views.generic.edit import CreateView +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.middleware import AuthenticationMiddleware +from .models import Project + + +class HomeView(TemplateView): + template_name = "home.html" + + +class ProjectListView(UserPassesTestMixin, ListView): + model = Project + + def get_queryset(self): + return self.request.ability.queryset_for("view", self.model) + + def test_func(self): + return self.request.ability.can("view", self.model) + + +class ProjectDetailView(DetailView): + model = Project + + def get_queryset(self): + return self.request.ability.queryset_for("view", self.model) + + def test_func(self): + return self.request.ability.can("view", self.model) + + +class ProjectCreateView(UserPassesTestMixin, CreateView): + model = Project + fields = ["name", "description"] + + def test_func(self): + return self.request.ability.can("add", self.model) + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + def get_success_url(self): + return reverse("project-detail", args=(self.object.id,)) diff --git a/example_project/pytest.ini b/example_project/pytest.ini new file mode 100644 index 0000000..1be48f9 --- /dev/null +++ b/example_project/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = settings +python_classes = Test* *Tests +python_files = tests.py tests_*.py *_tests.py \ No newline at end of file diff --git a/example_project/sample/admin.py b/example_project/sample/admin.py deleted file mode 100644 index 730ccdb..0000000 --- a/example_project/sample/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from .models import TodoItem - - -admin.site.register(TodoItem) diff --git a/example_project/sample/apps.py b/example_project/sample/apps.py deleted file mode 100644 index 79a288c..0000000 --- a/example_project/sample/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class SampleConfig(AppConfig): - name = "sample" diff --git a/example_project/sample/migrations/0001_initial.py b/example_project/sample/migrations/0001_initial.py deleted file mode 100644 index aabebd4..0000000 --- a/example_project/sample/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 3.1 on 2020-08-27 15:20 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="TodoItem", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128)), - ( - "created_by", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - ] diff --git a/example_project/sample/models.py b/example_project/sample/models.py deleted file mode 100644 index 82240d5..0000000 --- a/example_project/sample/models.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - -# Create your models here. - - -class TodoItem(models.Model): - name = models.CharField(max_length=128) - created_by = models.ForeignKey(User, on_delete=models.CASCADE) - - def __str__(self): - return f"#{self.id} {self.name} (by {self.created_by.username})" diff --git a/example_project/sample/templates/sample/todoitem_detail.html b/example_project/sample/templates/sample/todoitem_detail.html deleted file mode 100644 index 0c02334..0000000 --- a/example_project/sample/templates/sample/todoitem_detail.html +++ /dev/null @@ -1,12 +0,0 @@ -{% if user.is_authenticated %} -

Welcome, {{ user.get_username }}. Thanks for logging in.

-{% else %} -

Welcome, new user. Please log in.

-{% endif %} - -

TODO item #{{object.id}}

- -

{{object.name}}

-

by {{object.created_by.username}}

- -

Back

diff --git a/example_project/sample/templates/sample/todoitem_list.html b/example_project/sample/templates/sample/todoitem_list.html deleted file mode 100644 index 33f7b48..0000000 --- a/example_project/sample/templates/sample/todoitem_list.html +++ /dev/null @@ -1,18 +0,0 @@ -{% if user.is_authenticated %} -

Welcome, {{ user.get_username }}. Thanks for logging in.

-{% else %} -

Welcome, new user. Please log in.

-{% endif %} - -

Todo List

- -
    - {% for todo in object_list %} -
  • - {{ todo.name }}
    - by {{todo.created_by.username }} -
  • - {% empty %} -
  • No todos yet.
  • - {% endfor %} -
diff --git a/example_project/sample/tests.py b/example_project/sample/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/example_project/sample/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/example_project/sample/views.py b/example_project/sample/views.py deleted file mode 100644 index b7ceee9..0000000 --- a/example_project/sample/views.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.shortcuts import render -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.views.generic import ListView, DetailView -from .models import TodoItem - -from django.contrib.auth.backends import ModelBackend -from django.contrib.auth.middleware import AuthenticationMiddleware - - -class TodoIndexView(ListView): - model = TodoItem - - def get_query_set(): - qs = self.request.user.accessible_query_set(TodoItem) - return qs - - -class TodoDetailView(PermissionRequiredMixin, DetailView): - queryset = TodoItem.objects.all() - - def has_permission(self): - obj = self.get_object() - return self.request.user.can("view", obj) diff --git a/example_project/settings.py b/example_project/settings.py index c236899..2fe5dc7 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -44,8 +44,9 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "bulma", "cancan", - "sample", + "core", ] MIDDLEWARE = [ @@ -54,9 +55,9 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "cancan.middleware.CanCanMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "cancan.middleware.CanCanMiddleware", ] ROOT_URLCONF = "urls" @@ -72,6 +73,7 @@ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "cancan.context_processors.abilities", ], }, }, diff --git a/example_project/urls.py b/example_project/urls.py index 5b375f6..4a3278b 100644 --- a/example_project/urls.py +++ b/example_project/urls.py @@ -1,25 +1,17 @@ -"""example_project URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import include, path -from sample.views import TodoIndexView, TodoDetailView +from django.views.generic.base import RedirectView + +import core.views urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/profile/", RedirectView.as_view(url="/projects")), path("accounts/", include("django.contrib.auth.urls")), - path("", TodoIndexView.as_view()), - path("todo//", TodoDetailView.as_view()), + path("", core.views.HomeView.as_view(), name="home"), + path("projects/", core.views.ProjectListView.as_view(), name="project-list"), + path("projects/new/", core.views.ProjectCreateView.as_view(), name="project-new"), + path( + "projects//", core.views.ProjectDetailView.as_view(), name="project-detail" + ), ] From e961fb724db170b8e17de63b76ab829ee857c2d3 Mon Sep 17 00:00:00 2001 From: pgorecki Date: Sat, 26 Sep 2020 14:32:04 +0200 Subject: [PATCH 4/6] filters --- Pipfile.lock | 177 ++++-------------- cancan/ability.py | 12 +- cancan/access_rules.py | 18 +- cancan/context_processors.py | 4 +- cancan/templatetags/cancan_tags.py | 12 ++ cancan/testapp/tests/test_core.py | 13 ++ example_project/__init__.py | 0 example_project/conftest.py | 2 +- .../core/migrations/0001_initial.py | 96 ++++++++-- .../core/templates/core/project_list.html | 8 +- manage.py | 3 +- setup.py | 36 ++-- 12 files changed, 188 insertions(+), 193 deletions(-) delete mode 100644 example_project/__init__.py diff --git a/Pipfile.lock b/Pipfile.lock index 24f13fd..b7c41dd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d4fe4bcf158db1e6ebcddcaef87c168ca113133baec07b5b960e471808d6d37e" + "sha256": "362abb96f88c120f58c300de627bc49d81b0ca9d04acfbfd563a2cfecb58791c" }, "pipfile-spec": 6, "requires": { @@ -16,34 +16,12 @@ ] }, "default": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, "asgiref": { "hashes": [ - "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], "version": "==3.2.10" }, - "black": { - "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" - ], - "index": "pypi", - "version": "==20.8b1" - }, - "click": { - "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" - ], - "version": "==7.1.2" - }, "django": { "hashes": [ "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", @@ -60,100 +38,17 @@ "index": "pypi", "version": "==0.4.5" }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" - ], - "version": "==0.8.0" - }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed" ], "version": "==2020.1" }, - "regex": { - "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" - ], - "version": "==2020.7.14" - }, "sqlparse": { "hashes": [ - "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", - "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e" ], "version": "==0.3.1" - }, - "toml": { - "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" - ], - "version": "==0.10.1" - }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "version": "==1.4.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" - ], - "version": "==3.7.4.3" } }, "develop": { @@ -180,10 +75,10 @@ }, "bleach": { "hashes": [ - "sha256:769483204d247465c0b001ead257fb86bba6944bce6fe1b6759c812cceb54e3d", - "sha256:f9e0205cc57b558c21bdfc11034f9d96b14c4052c25be60885d94f4277c792e0" + "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], - "version": "==3.2.0" + "version": "==3.2.1" }, "certifi": { "hashes": [ @@ -256,30 +151,30 @@ }, "cryptography": { "hashes": [ - "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a", - "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed", - "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36", - "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08", - "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237", - "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618", - "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f", - "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695", - "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c", - "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10", - "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c", - "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1", - "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e", - "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f", - "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791", - "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0", - "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af", - "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8", - "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761", - "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716", - "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32", - "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67" - ], - "version": "==3.1" + "sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499", + "sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154", + "sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6", + "sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49", + "sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f", + "sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396", + "sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719", + "sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db", + "sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70", + "sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536", + "sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe", + "sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba", + "sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d", + "sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7", + "sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490", + "sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8", + "sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921", + "sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118", + "sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba", + "sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3", + "sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc", + "sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2" + ], + "version": "==3.1.1" }, "docutils": { "hashes": [ @@ -297,11 +192,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", + "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" ], "markers": "python_version < '3.8'", - "version": "==1.7.0" + "version": "==2.0.0" }, "jeepney": { "hashes": [ @@ -523,10 +418,10 @@ }, "zipp": { "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6", + "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f" ], - "version": "==3.1.0" + "version": "==3.2.0" } } } diff --git a/cancan/ability.py b/cancan/ability.py index 270b74f..ba6dfde 100644 --- a/cancan/ability.py +++ b/cancan/ability.py @@ -54,12 +54,12 @@ def validate_instance(self, action, instance): return can_query_set.count() > 0 - def can(self, action, model_or_instance) -> bool: + def can(self, action, subject) -> bool: action = self.access_rules.alias_to_action(action) - if inspect.isclass(model_or_instance): - return self.validate_model(action, model_or_instance) + if inspect.isclass(subject): + return self.validate_model(action, subject) else: - return self.validate_instance(action, model_or_instance) + return self.validate_instance(action, subject) def queryset_for(self, action, model): action = self.access_rules.alias_to_action(action) @@ -87,3 +87,7 @@ def queryset_for(self, action, model): can_query_set |= qs return can_query_set + + def __contains__(self, item): + action, subject = item + return self.can(action, subject) \ No newline at end of file diff --git a/cancan/access_rules.py b/cancan/access_rules.py index cd72710..6edf237 100644 --- a/cancan/access_rules.py +++ b/cancan/access_rules.py @@ -1,6 +1,18 @@ from django.apps import apps +def normalize_subject(subject): + if isinstance(subject, str): + try: + app_label, model_name = subject.split('.') + return apps.get_model(app_label, model_name) + except Excepion as e: + pass + return subject + + + + class AccessRules: def __init__(self, user): self.user = user @@ -8,12 +20,10 @@ def __init__(self, user): self.action_aliases = {} def allow(self, action, subject, **kwargs): - if type(subject) is str: - model = apps.get_model(subject) rule = { "type": "can", "action": action, - "model": subject, + "model": normalize_subject(subject), "conditions": kwargs, } self.rules.append(rule) @@ -23,4 +33,4 @@ def alias_action(self, action, alias): self.action_aliases[alias] = action def alias_to_action(self, alias): - return self.action_aliases.get(alias, alias) \ No newline at end of file + return self.action_aliases.get(alias, alias) diff --git a/cancan/context_processors.py b/cancan/context_processors.py index 460af89..9d0cb64 100644 --- a/cancan/context_processors.py +++ b/cancan/context_processors.py @@ -1,2 +1,4 @@ def abilities(request): - return {"abilities": request.ability.access_rules.rules} + return { + "abilities": request.ability.access_rules.rules, + } diff --git a/cancan/templatetags/cancan_tags.py b/cancan/templatetags/cancan_tags.py index 2d8ffe9..0cb797b 100644 --- a/cancan/templatetags/cancan_tags.py +++ b/cancan/templatetags/cancan_tags.py @@ -1,10 +1,22 @@ from django import template from django.apps import apps from django.utils.safestring import SafeString +from cancan.access_rules import normalize_subject register = template.Library() +@register.filter +def can(abilities, action): + matching_rules = list(filter(lambda rule: rule['action'] == action, abilities)) + return matching_rules + +@register.filter +def subject(abilities, subject): + s = normalize_subject(str(subject)) + matching_rules = list(filter(lambda rule: rule['model'] == s, abilities)) + return matching_rules + @register.simple_tag(takes_context=True) def can(context, action, subject): is_allowed = False diff --git a/cancan/testapp/tests/test_core.py b/cancan/testapp/tests/test_core.py index 14754dd..d4e6d19 100644 --- a/cancan/testapp/tests/test_core.py +++ b/cancan/testapp/tests/test_core.py @@ -108,3 +108,16 @@ def setUp(self): def test_can_view_published(self): self.assertTrue(self.ability.can("list", Article)) self.assertEqual(self.ability.queryset_for("list", Article).count(), 1) + + +class MiscTestCase(TestCase): + def setUp(self): + self.user = User.objects.create(username="user1") + article1 = Article.objects.create(name="test") + access_rules = AccessRules(user=self.user) + access_rules.allow("view", Article) + access_rules.alias_action("view", "list") + self.ability = Ability(access_rules) + + def test_in_operator(self): + self.assertTrue(("view", Article) in self.ability) diff --git a/example_project/__init__.py b/example_project/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/example_project/conftest.py b/example_project/conftest.py index 7cc009d..0e0e72c 100644 --- a/example_project/conftest.py +++ b/example_project/conftest.py @@ -3,4 +3,4 @@ @pytest.fixture(autouse=True) def enable_db_access_for_all_tests(db): - pass \ No newline at end of file + pass diff --git a/example_project/core/migrations/0001_initial.py b/example_project/core/migrations/0001_initial.py index d9996a1..d516359 100644 --- a/example_project/core/migrations/0001_initial.py +++ b/example_project/core/migrations/0001_initial.py @@ -15,39 +15,95 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Membership', + name="Membership", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), migrations.CreateModel( - name='Project', + name="Project", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128)), - ('description', models.TextField(blank=True, default='')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)), - ('members', models.ManyToManyField(through='core.Membership', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128)), + ("description", models.TextField(blank=True, default="")), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="owner", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "members", + models.ManyToManyField( + through="core.Membership", to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.AddField( - model_name='membership', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.project'), + model_name="membership", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.project" + ), ), migrations.AddField( - model_name='membership', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="membership", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), migrations.CreateModel( - name='Issue', + name="Issue", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128)), - ('description', models.TextField(blank=True, default='')), - ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues_assigned', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues_created', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128)), + ("description", models.TextField(blank=True, default="")), + ( + "assigned_to", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issues_assigned", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issues_created", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/example_project/core/templates/core/project_list.html b/example_project/core/templates/core/project_list.html index 0b945b6..b9d523d 100644 --- a/example_project/core/templates/core/project_list.html +++ b/example_project/core/templates/core/project_list.html @@ -22,8 +22,12 @@

Your projects

{% endfor %} - {% can "view" "core.Project" as is_allowed %} - {% if is_allowed %} + {% can "add" "core.Project" as is_allowed %} + {% for x in abilities|can:"view"|subject:"core.Project" %} + x + {% endfor %} + + {% if abilities|can:"view"|subject:"core.Project" %}
  • Create new project
  • diff --git a/manage.py b/manage.py index 7f02e57..62f307b 100755 --- a/manage.py +++ b/manage.py @@ -3,8 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "cancan.testapp.testsettings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cancan.testapp.testsettings") from django.core.management import execute_from_command_line diff --git a/setup.py b/setup.py index 76fe14e..54ede14 100644 --- a/setup.py +++ b/setup.py @@ -14,23 +14,23 @@ url="https://github.com/pgorecki/django-cancan", packages=setuptools.find_packages(), classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Internet :: WWW/HTTP', + "Development Status :: 2 - Pre-Alpha", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Internet :: WWW/HTTP", ], - python_requires='>=3.6', + python_requires=">=3.6", ) From 0fc0e79db84ab110ab256391e9ed462ccded817c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Tue, 29 Sep 2020 11:20:34 +0200 Subject: [PATCH 5/6] update filters --- cancan/ability.py | 10 ++-- cancan/access_rules.py | 8 ++- cancan/context_processors.py | 2 +- cancan/templatetags/cancan_tags.py | 54 ++++++++++++++----- example_project/abilities.py | 4 ++ example_project/core/models.py | 2 +- .../core/templates/core/project_detail.html | 17 +++++- .../core/templates/core/project_list.html | 20 ++++--- example_project/core/views.py | 21 ++++++-- example_project/urls.py | 11 ++-- 10 files changed, 108 insertions(+), 41 deletions(-) diff --git a/cancan/ability.py b/cancan/ability.py index ba6dfde..08b012a 100644 --- a/cancan/ability.py +++ b/cancan/ability.py @@ -1,6 +1,6 @@ import inspect from django.apps import apps -from .access_rules import AccessRules +from .access_rules import AccessRules, normalize_subject class Ability: @@ -11,7 +11,7 @@ def validate_model(self, action, model): can_count = 0 cannot_count = 0 model_abilities = filter( - lambda c: c["model"] == model and c["action"] == action, + lambda c: c["subject"] == model and c["action"] == action, self.access_rules.rules, ) for c in model_abilities: @@ -29,7 +29,7 @@ def validate_model(self, action, model): def validate_instance(self, action, instance): model = instance._meta.model model_abilities = filter( - lambda c: c["model"] == model and c["action"] == action, + lambda c: c["subject"] == model and c["action"] == action, self.access_rules.rules, ) @@ -55,6 +55,7 @@ def validate_instance(self, action, instance): return can_query_set.count() > 0 def can(self, action, subject) -> bool: + subject = normalize_subject(subject) action = self.access_rules.alias_to_action(action) if inspect.isclass(subject): return self.validate_model(action, subject) @@ -62,10 +63,11 @@ def can(self, action, subject) -> bool: return self.validate_instance(action, subject) def queryset_for(self, action, model): + model = normalize_subject(model) action = self.access_rules.alias_to_action(action) model_abilities = filter( - lambda c: c["model"] == model and c["action"] == action, + lambda c: c["subject"] == model and c["action"] == action, self.access_rules.rules, ) diff --git a/cancan/access_rules.py b/cancan/access_rules.py index 6edf237..e260195 100644 --- a/cancan/access_rules.py +++ b/cancan/access_rules.py @@ -4,15 +4,13 @@ def normalize_subject(subject): if isinstance(subject, str): try: - app_label, model_name = subject.split('.') + app_label, model_name = subject.split(".") return apps.get_model(app_label, model_name) - except Excepion as e: + except Exception as e: pass return subject - - class AccessRules: def __init__(self, user): self.user = user @@ -23,7 +21,7 @@ def allow(self, action, subject, **kwargs): rule = { "type": "can", "action": action, - "model": normalize_subject(subject), + "subject": normalize_subject(subject), "conditions": kwargs, } self.rules.append(rule) diff --git a/cancan/context_processors.py b/cancan/context_processors.py index 9d0cb64..a974920 100644 --- a/cancan/context_processors.py +++ b/cancan/context_processors.py @@ -1,4 +1,4 @@ def abilities(request): return { - "abilities": request.ability.access_rules.rules, + "ability": request.ability, } diff --git a/cancan/templatetags/cancan_tags.py b/cancan/templatetags/cancan_tags.py index 0cb797b..c0e7b02 100644 --- a/cancan/templatetags/cancan_tags.py +++ b/cancan/templatetags/cancan_tags.py @@ -1,27 +1,53 @@ from django import template from django.apps import apps from django.utils.safestring import SafeString -from cancan.access_rules import normalize_subject +from cancan.ability import Ability register = template.Library() +class AbilityCheck: + """ + This function is used internally to check for ability within a template i. e. + {% if ability|can:"view"|subject:project %} + View + {% endif %} + """ + + def __init__(self, ability): + self.ability = ability + self.action = None + self.subject = None + + def __repr__(self): + return f"{self.ability}(action={self.action},subject={self.subject})" + + def __bool__(self): + return self.ability.can(self.action, self.subject) + + @register.filter -def can(abilities, action): - matching_rules = list(filter(lambda rule: rule['action'] == action, abilities)) - return matching_rules +def can(ability, action): + if not isinstance(ability, AbilityCheck): + assert isinstance( + ability, Ability + ), f"can filter must be applied to Ability instance (you provided {type(ability)})" + ability = AbilityCheck(ability) + ability.action = action + return ability + @register.filter -def subject(abilities, subject): - s = normalize_subject(str(subject)) - matching_rules = list(filter(lambda rule: rule['model'] == s, abilities)) - return matching_rules +def subject(ability, subject): + if not isinstance(ability, AbilityCheck): + assert isinstance( + ability, Ability + ), f"subject filter must be applied to Ability instance (you provided {type(ability)})" + ability = AbilityCheck(ability) + ability.subject = subject + return ability + @register.simple_tag(takes_context=True) def can(context, action, subject): - is_allowed = False - if type(subject) is SafeString: - model = apps.get_model(subject) - print(model) - is_allowed = context["request"].ability.can(action, model) - return f"can {action} {subject}? {is_allowed}" + return context["request"].ability.can(action, subject) diff --git a/example_project/abilities.py b/example_project/abilities.py index b7366b9..5f8c706 100644 --- a/example_project/abilities.py +++ b/example_project/abilities.py @@ -9,6 +9,10 @@ def declare_abilities(user, rules): # logged in can view own content rules.allow("add", Project) rules.allow("view", Project, created_by=user) + rules.allow("change", Project, created_by=user) + rules.allow("delete", Project, created_by=user) if user.is_superuser: rules.allow("view", Project) + rules.allow("change", Project) + rules.allow("delete", Project) diff --git a/example_project/core/models.py b/example_project/core/models.py index 40d29cb..376196c 100644 --- a/example_project/core/models.py +++ b/example_project/core/models.py @@ -9,7 +9,7 @@ class Project(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owner") def get_absolute_url(self): - return reverse("project-detail", kwargs={"pk": self.pk}) + return reverse("project_detail", kwargs={"pk": self.pk}) class Membership(models.Model): diff --git a/example_project/core/templates/core/project_detail.html b/example_project/core/templates/core/project_detail.html index 59061b8..1bc01c8 100644 --- a/example_project/core/templates/core/project_detail.html +++ b/example_project/core/templates/core/project_detail.html @@ -1,7 +1,20 @@ -{% extends 'bulma/base.html' %} +{% extends 'bulma/base.html' %} +{% load cancan_tags %} {% block title %}{{object.name}}{% endblock %} {% block content %} -
    TODO: project details
    +

    {{object.name}}

    +

    {{object.description}}

    +
    + +Back + +{% if ability|can:"change"|subject:object %} + Edit +{% endif %} +{% if ability|can:"delete"|subject:object %} + Delete +{% endif %} + {% endblock content %} \ No newline at end of file diff --git a/example_project/core/templates/core/project_list.html b/example_project/core/templates/core/project_list.html index b9d523d..dd1eff4 100644 --- a/example_project/core/templates/core/project_list.html +++ b/example_project/core/templates/core/project_list.html @@ -4,32 +4,36 @@ {% block title %}Your projects{% endblock %} {% block content %} ---->{{ abilities }}<---

    Your projects

      {% for project in object_list %}
    • - {{project.name}} + {{project.name}}
      {{project.description}}
      +
      + {% if ability|can:"view"|subject:project %} + View + {% endif %} + {% if ability|can:"change"|subject:project %} + Edit + {% endif %} +
    • {% endfor %} - {% can "add" "core.Project" as is_allowed %} - {% for x in abilities|can:"view"|subject:"core.Project" %} - x - {% endfor %} + {% can "add" "core.Project" as can_add_project %} - {% if abilities|can:"view"|subject:"core.Project" %} + {% if can_add_project %}
    • - Create new project + Create new project
    • {% endif %}
    diff --git a/example_project/core/views.py b/example_project/core/views.py index a270c99..27e5bc3 100644 --- a/example_project/core/views.py +++ b/example_project/core/views.py @@ -2,7 +2,7 @@ from django.urls import reverse from django.contrib.auth.mixins import UserPassesTestMixin from django.views.generic import ListView, DetailView, TemplateView -from django.views.generic.edit import CreateView +from django.views.generic.edit import CreateView, UpdateView from django.contrib.auth.backends import ModelBackend from django.contrib.auth.middleware import AuthenticationMiddleware from .models import Project @@ -29,7 +29,7 @@ def get_queryset(self): return self.request.ability.queryset_for("view", self.model) def test_func(self): - return self.request.ability.can("view", self.model) + return self.request.ability.can("view", self.get_object()) class ProjectCreateView(UserPassesTestMixin, CreateView): @@ -44,4 +44,19 @@ def form_valid(self, form): return super().form_valid(form) def get_success_url(self): - return reverse("project-detail", args=(self.object.id,)) + return reverse("project_detail", args=(self.object.id,)) + + +class ProjectUpdateView(UserPassesTestMixin, UpdateView): + model = Project + fields = ["name", "description"] + + def test_func(self): + return self.request.ability.can("change", self.get_object()) + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + def get_success_url(self): + return reverse("project_detail", args=(self.self.get_object().id,)) diff --git a/example_project/urls.py b/example_project/urls.py index 4a3278b..8b0b457 100644 --- a/example_project/urls.py +++ b/example_project/urls.py @@ -9,9 +9,14 @@ path("accounts/profile/", RedirectView.as_view(url="/projects")), path("accounts/", include("django.contrib.auth.urls")), path("", core.views.HomeView.as_view(), name="home"), - path("projects/", core.views.ProjectListView.as_view(), name="project-list"), - path("projects/new/", core.views.ProjectCreateView.as_view(), name="project-new"), + path("projects/", core.views.ProjectListView.as_view(), name="project_list"), + path("projects/new/", core.views.ProjectCreateView.as_view(), name="project_new"), path( - "projects//", core.views.ProjectDetailView.as_view(), name="project-detail" + "projects//", core.views.ProjectDetailView.as_view(), name="project_detail" + ), + path( + "projects//edit/", + core.views.ProjectUpdateView.as_view(), + name="project_edit", ), ] From 6a8221a5bff51198eaf0bc24e285e27a34215116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Tue, 29 Sep 2020 12:16:55 +0200 Subject: [PATCH 6/6] update readme --- Pipfile | 1 + Pipfile.lock | 69 ++++++++++++++----------- README.md | 141 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 166 insertions(+), 45 deletions(-) diff --git a/Pipfile b/Pipfile index c77cefa..db85269 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ setuptools = "*" wheel = "*" twine = "*" black = "*" +django-extensions = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index b7c41dd..ece40ad 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "362abb96f88c120f58c300de627bc49d81b0ca9d04acfbfd563a2cfecb58791c" + "sha256": "9b2ea523e8531bb7c01e7bf82085d82dc83610a176239d911f9de0385e17a0b9" }, "pipfile-spec": 6, "requires": { @@ -18,6 +18,7 @@ "default": { "asgiref": { "hashes": [ + "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], "version": "==3.2.10" @@ -40,13 +41,15 @@ }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed" + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], "version": "==2020.1" }, "sqlparse": { "hashes": [ - "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e" + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], "version": "==0.3.1" } @@ -176,6 +179,14 @@ ], "version": "==3.1.1" }, + "django-extensions": { + "hashes": [ + "sha256:6809c89ca952f0e08d4e0766bc0101dfaf508d7649aced1180c091d737046ea7", + "sha256:dc663652ac9460fd06580a973576820430c6d428720e874ae46b041fa63e0efa" + ], + "index": "pypi", + "version": "==3.0.9" + }, "docutils": { "hashes": [ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", @@ -278,29 +289,29 @@ }, "regex": { "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" - ], - "version": "==2020.7.14" + "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef", + "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c", + "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b", + "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c", + "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63", + "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc", + "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be", + "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab", + "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19", + "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637", + "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc", + "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b", + "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d", + "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b", + "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100", + "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3", + "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121", + "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b", + "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707", + "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7", + "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f" + ], + "version": "==2020.9.27" }, "requests": { "hashes": [ @@ -347,10 +358,10 @@ }, "tqdm": { "hashes": [ - "sha256:8f3c5815e3b5e20bc40463fa6b42a352178859692a68ffaa469706e6d38342a5", - "sha256:faf9c671bd3fad5ebaeee366949d969dca2b2be32c872a7092a1e1a9048d105b" + "sha256:2dd75fdb764f673b8187643496fcfbeac38348015b665878e582b152f3391cdb", + "sha256:93b7a6a9129fce904f6df4cf3ae7ff431d779be681a95c3344c26f3e6c09abfa" ], - "version": "==4.49.0" + "version": "==4.50.0" }, "twine": { "hashes": [ diff --git a/README.md b/README.md index 7883bce..4a6375d 100644 --- a/README.md +++ b/README.md @@ -43,32 +43,43 @@ INSTALLED_APPS = [ ] ``` -2. Create a function that define user abilites. For example, in `abilities.py`: +2. Create a function that define the access rules for a given user. For example, create `abilities.py` in `myapp` module: ```python -def declare_abilities(user, ability): +def define_access_rules(user, rules): + # Anybody can view published articles + rules.allow('view', Article, published=True) + if not user.is_authenticated: - # Allow anonymous users to view published articles - return ability.can('view', Article, published=True) + return + + # Allow logged in user to view his own articles, regardless of the `published` status + rules.allow('view', Article, author=user) - if user.has_perm('article.view_own_article'): - # Allow logged in user to change his articles - return ability.can('change', Article, author=user) + if user.has_perm('article.view_unpublished'): + # You can also check for custom model permissions (i.e. view_unpublished) + rules.allow('view', Article, published=False) + if user.is_superuser: - # Allow superuser change all articles - return ability.can('change', Article) + # Superuser gets unlimited access to all articles + rules.allow('add', Article) + rules.allow('view', Article) + rules.allow('change', Article) + rules.allow('delete', Article) ``` -3. Configure `cancan` by adding `CANCAN` section in `settings.py`: +3. In `settings.py` add `CANCAN` section, so that `cancan` library will know where to search for `define_access_rules` function from the previous step: ```python CANCAN = { - 'ABILITIES': 'myapp.abilities.declare_abilities' + 'ABILITIES': 'myapp.abilities.define_access_rules' } ``` -Next, add `cancan` middleware after `AuthenticationMiddleware`: +The `define_access_rules` function will be executed automatically per each request by the `cancan` middleware. The middleware will call the function to determine the abilities of a current user. + +Let's add `cancan` middleware, just after `AuthenticationMiddleware`: ```python MIDDLEWARE = [ @@ -79,10 +90,13 @@ MIDDLEWARE = [ ] ``` -Adding the middleware adds `request.ability` instance which you can use -to check for: model permissions, object permissions and model querysets. +By adding the middleware you will also get an access to `request.ability` instance which you can use +to: + - check model permissions, + - check object permissions, + - generate model querysets (i.e. in case of `ListView`s -4. Check abilities in views: +4. Check for abilities in views: ```python @@ -90,7 +104,7 @@ class ArticleListView(ListView): model = Article def get_queryset(): - # this is how you can retrieve all objects a user can access + # this is how you can retrieve all objects that current user can access qs = self.request.ability.queryset_for('view', Article) return qs @@ -104,6 +118,101 @@ class ArticleDetailView(PermissionRequiredMixin, DetailView): return self.request.ability.can('view', article) ``` +5. Check for abilities in templates + +You can also check for abilities in template files, i. e. to show/hide/disable buttons or links. + +First you need to add `cancan` processor to `context_processors` in `TEMPLATES` section of `settings.py`: + +```python +TEMPLATES = [ + { + ..., + "OPTIONS": { + "context_processors": [ + ..., + "cancan.context_processors.abilities", + ], + }, + }, +] +``` + +This will give you access to `ability` object in a template. You also need add `{% load cancan_tags %}` at the beginning +of the template file. + +Next you can check for object permissions: + +``` +{% if ability|can:"change"|subject:article %} + Edit article +{% endif %} +``` + +or model permissions: + +``` +{% load cancan_tags %} + +... + +{% if ability|can:"add"|"myapp.Article" %} + Create new article +{% endif %} +``` + +You can also use `can` template tag to create a reusable variable: + +``` +{% can "add" "core.Project" as can_add_project %} +... +{% if can_add_project %} + ... +{% endif %} +``` + +## Checking for abilities in Django Rest Framework + +Let's start by creating a pemission class: + +```python +from rest_framework import permissions + +def set_aliases_for_drf_actions(ability): + """ + map DRF actions to default Django permissions + """ + ability.access_rules.set_alias("list", "view") + ability.access_rules.set_alias("retrieve", "view") + ability.access_rules.set_alias("create", "add") + ability.access_rules.set_alias("update", "change") + ability.access_rules.set_alias("partial_update", "change") + ability.access_rules.set_alias("destroy", "delete") + + +class AbilityPermission(permissions.BasePermission): + def has_permission(self, request, view=None): + ability = request.ability + set_aliases_for_drf_actions(ability) + return ability.can(view.action, view.get_queryset().model) + + def has_object_permission(self, request, view, obj): + ability = request.ability + set_aliases_for_drf_actions(ability) + return ability.can(view.action, obj) +``` + +Next, secure the ViewSet with `AbilityPermission` and override `get_queryset` method to list objects based on the access rights. + +```python +class ArticleViewset(ModelViewSet): + permission_classes = [AbilityPermission] + + def get_queryset(self): + return self.request.ability.queryset_for(self.action, Article).distinct() +``` + + ## Sponsors