Add mypy
This commit is contained in:
parent
8b32e2e9a4
commit
40a4a0a308
19 changed files with 336 additions and 83 deletions
|
@ -1,7 +1,7 @@
|
||||||
exclude: (\.min\.(js|css)(\.map)?$|/vendor/)
|
exclude: (\.min\.(js|css)(\.map)?$|/vendor/)
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.0.1
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
@ -29,7 +29,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.29.1
|
rev: v2.30.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
|
@ -40,7 +40,7 @@ repos:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args: [--target-version, "4.0"]
|
args: [--target-version, "4.0"]
|
||||||
- repo: https://github.com/rtts/djhtml
|
- repo: https://github.com/rtts/djhtml
|
||||||
rev: v1.4.10
|
rev: v1.4.11
|
||||||
hooks:
|
hooks:
|
||||||
- id: djhtml
|
- id: djhtml
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
@ -60,7 +60,7 @@ repos:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or: [javascript, css]
|
types_or: [javascript, css]
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
rev: v8.4.1
|
rev: v8.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
|
|
198
poetry.lock
generated
198
poetry.lock
generated
|
@ -253,6 +253,35 @@ Django = ">=2.2"
|
||||||
phonenumbers = ["phonenumbers (>=7.0.2)"]
|
phonenumbers = ["phonenumbers (>=7.0.2)"]
|
||||||
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
|
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs"
|
||||||
|
version = "1.9.0"
|
||||||
|
description = "Mypy stubs for Django"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = "*"
|
||||||
|
django-stubs-ext = ">=0.3.0"
|
||||||
|
mypy = ">=0.910"
|
||||||
|
toml = "*"
|
||||||
|
types-pytz = "*"
|
||||||
|
types-PyYAML = "*"
|
||||||
|
typing-extensions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-stubs-ext"
|
||||||
|
version = "0.3.1"
|
||||||
|
description = "Monkey-patching and extensions for django-stubs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = "*"
|
||||||
|
typing-extensions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-two-factor-auth"
|
name = "django-two-factor-auth"
|
||||||
version = "1.13"
|
version = "1.13"
|
||||||
|
@ -396,6 +425,31 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "0.930"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
tomli = ">=1.1.0"
|
||||||
|
typing-extensions = ">=3.10"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
python2 = ["typed-ast (>=1.4.0,<2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "0.4.3"
|
||||||
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -758,6 +812,78 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-beautifulsoup4"
|
||||||
|
version = "4.10.7"
|
||||||
|
description = "Typing stubs for beautifulsoup4"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-markdown"
|
||||||
|
version = "3.3.10"
|
||||||
|
description = "Typing stubs for Markdown"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pillow"
|
||||||
|
version = "8.3.11"
|
||||||
|
description = "Typing stubs for Pillow"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pytz"
|
||||||
|
version = "2021.3.3"
|
||||||
|
description = "Typing stubs for pytz"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-pyyaml"
|
||||||
|
version = "6.0.1"
|
||||||
|
description = "Typing stubs for PyYAML"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-requests"
|
||||||
|
version = "2.26.3"
|
||||||
|
description = "Typing stubs for requests"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-setuptools"
|
||||||
|
version = "57.4.5"
|
||||||
|
description = "Typing stubs for setuptools"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-toml"
|
||||||
|
version = "0.10.1"
|
||||||
|
description = "Typing stubs for toml"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.0.1"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2021.5"
|
version = "2021.5"
|
||||||
|
@ -849,7 +975,7 @@ multidict = ">=4.0"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "177905663c5207bbaed9cc8a093d99b96e932297e9901d8f7b7985583583fe14"
|
content-hash = "35d359b39bfd7c907a1119797a4badf33b3f3c964a5ec90e85c781cf99fecf7e"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -1015,6 +1141,14 @@ django-phonenumber-field = [
|
||||||
{file = "django-phonenumber-field-5.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"},
|
{file = "django-phonenumber-field-5.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"},
|
||||||
{file = "django_phonenumber_field-5.2.0-py3-none-any.whl", hash = "sha256:5547fb2b2cc690a306ba77a5038419afc8fa8298a486fb7895008e9067cc7e75"},
|
{file = "django_phonenumber_field-5.2.0-py3-none-any.whl", hash = "sha256:5547fb2b2cc690a306ba77a5038419afc8fa8298a486fb7895008e9067cc7e75"},
|
||||||
]
|
]
|
||||||
|
django-stubs = [
|
||||||
|
{file = "django-stubs-1.9.0.tar.gz", hash = "sha256:664843091636a917faf5256d028476559dc360fdef9050b6df87ab61b21607bf"},
|
||||||
|
{file = "django_stubs-1.9.0-py3-none-any.whl", hash = "sha256:59c9f81af64d214b1954eaf90f037778c8d2b9c2de946a3cda177fefcf588fbd"},
|
||||||
|
]
|
||||||
|
django-stubs-ext = [
|
||||||
|
{file = "django-stubs-ext-0.3.1.tar.gz", hash = "sha256:783c198d7e39a41be0b90fd843fa2770243a642922af679be4b19e03b82c8c28"},
|
||||||
|
{file = "django_stubs_ext-0.3.1-py3-none-any.whl", hash = "sha256:a51a3e9e844d4e1cacaaedbb33bf3def78a3956eed5d9575a640bd97ccd99cec"},
|
||||||
|
]
|
||||||
django-two-factor-auth = []
|
django-two-factor-auth = []
|
||||||
filelock = [
|
filelock = [
|
||||||
{file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"},
|
{file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"},
|
||||||
|
@ -1189,6 +1323,32 @@ multidict = [
|
||||||
{file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"},
|
{file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"},
|
||||||
{file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"},
|
{file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"},
|
||||||
]
|
]
|
||||||
|
mypy = [
|
||||||
|
{file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"},
|
||||||
|
{file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"},
|
||||||
|
{file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"},
|
||||||
|
{file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"},
|
||||||
|
{file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"},
|
||||||
|
{file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"},
|
||||||
|
{file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"},
|
||||||
|
{file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"},
|
||||||
|
{file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"},
|
||||||
|
{file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"},
|
||||||
|
{file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"},
|
||||||
|
{file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"},
|
||||||
|
{file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"},
|
||||||
|
{file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"},
|
||||||
|
{file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"},
|
||||||
|
{file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"},
|
||||||
|
{file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"},
|
||||||
|
{file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"},
|
||||||
|
{file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"},
|
||||||
|
{file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
]
|
||||||
nodeenv = [
|
nodeenv = [
|
||||||
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
||||||
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
||||||
|
@ -1406,6 +1566,42 @@ tomli = [
|
||||||
{file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"},
|
{file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"},
|
||||||
{file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"},
|
{file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"},
|
||||||
]
|
]
|
||||||
|
types-beautifulsoup4 = [
|
||||||
|
{file = "types-beautifulsoup4-4.10.7.tar.gz", hash = "sha256:8a267194e405b7163bde055ef1120988c0221e5d58e1c5bfe08d601a21e63daf"},
|
||||||
|
{file = "types_beautifulsoup4-4.10.7-py3-none-any.whl", hash = "sha256:b450b70935b901f500125662ca1856fe6a3ba73ccc88b4fcd577a2b927cc8c14"},
|
||||||
|
]
|
||||||
|
types-markdown = [
|
||||||
|
{file = "types-Markdown-3.3.10.tar.gz", hash = "sha256:d4004e36a33cd3417cd299324232fc28e9915171a7ce46b43bf5b0c579db459d"},
|
||||||
|
{file = "types_Markdown-3.3.10-py3-none-any.whl", hash = "sha256:0e3153dc4ad3454326465e4e4e21fd90e8fc74966225ec5aeef15895a2c5d94a"},
|
||||||
|
]
|
||||||
|
types-pillow = [
|
||||||
|
{file = "types-Pillow-8.3.11.tar.gz", hash = "sha256:aa96a739184f48f69e6f30218400623fc5a95f5fec199c447663a32538440405"},
|
||||||
|
{file = "types_Pillow-8.3.11-py3-none-any.whl", hash = "sha256:998189334e616b1dd42c9634669efbf726184039e96e9a23ec95246e0ecff3fc"},
|
||||||
|
]
|
||||||
|
types-pytz = [
|
||||||
|
{file = "types-pytz-2021.3.3.tar.gz", hash = "sha256:f6d21d6687935a1615db464b1e1df800d19502c36bc0486f43be7dfd2c404947"},
|
||||||
|
{file = "types_pytz-2021.3.3-py3-none-any.whl", hash = "sha256:75859c64c9a97d68259af6da208e8f5aaf4be4536e4d431a82a6e8b848fc183d"},
|
||||||
|
]
|
||||||
|
types-pyyaml = [
|
||||||
|
{file = "types-PyYAML-6.0.1.tar.gz", hash = "sha256:2e27b0118ca4248a646101c5c318dc02e4ca2866d6bc42e84045dbb851555a76"},
|
||||||
|
{file = "types_PyYAML-6.0.1-py3-none-any.whl", hash = "sha256:d5b318269652e809b5c30a5fe666c50159ab80bfd41cd6bafe655bf20b29fcba"},
|
||||||
|
]
|
||||||
|
types-requests = [
|
||||||
|
{file = "types-requests-2.26.3.tar.gz", hash = "sha256:d63fa617846dcefff5aa2d59e47ab4ffd806e4bb0567115f7adbb5e438302fe4"},
|
||||||
|
{file = "types_requests-2.26.3-py3-none-any.whl", hash = "sha256:ad18284931c5ddbf050ccdd138f200d18fd56f88aa3567019d8da9b2d4fe0344"},
|
||||||
|
]
|
||||||
|
types-setuptools = [
|
||||||
|
{file = "types-setuptools-57.4.5.tar.gz", hash = "sha256:a4600efdca68a33204ad9c083fd9966d63aee61a7d007e912b6afc6ff57d6e02"},
|
||||||
|
{file = "types_setuptools-57.4.5-py3-none-any.whl", hash = "sha256:920a7c1ee120025e939a1707f8fd09a1266edbf7848eae7b8de7c5909a824cc8"},
|
||||||
|
]
|
||||||
|
types-toml = [
|
||||||
|
{file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"},
|
||||||
|
{file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
|
||||||
|
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
|
||||||
|
]
|
||||||
tzdata = [
|
tzdata = [
|
||||||
{file = "tzdata-2021.5-py2.py3-none-any.whl", hash = "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5"},
|
{file = "tzdata-2021.5-py2.py3-none-any.whl", hash = "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5"},
|
||||||
{file = "tzdata-2021.5.tar.gz", hash = "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21"},
|
{file = "tzdata-2021.5.tar.gz", hash = "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21"},
|
||||||
|
|
|
@ -36,6 +36,14 @@ pytest-rerunfailures = "^10.2"
|
||||||
pytest-env = "^0.6.2"
|
pytest-env = "^0.6.2"
|
||||||
poetry-deps-scanner = "^1.0.1"
|
poetry-deps-scanner = "^1.0.1"
|
||||||
invoke = "^1.6.0"
|
invoke = "^1.6.0"
|
||||||
|
mypy = "^0.930"
|
||||||
|
django-stubs = "^1.9.0"
|
||||||
|
types-Markdown = "^3.3.10"
|
||||||
|
types-requests = "^2.26.3"
|
||||||
|
types-setuptools = "^57.4.5"
|
||||||
|
types-toml = "^0.10.1"
|
||||||
|
types-beautifulsoup4 = "^4.10.7"
|
||||||
|
types-Pillow = "^8.3.11"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py38']
|
target-version = ['py38']
|
||||||
|
@ -54,6 +62,25 @@ env = [
|
||||||
"GOATCOUNTER_DOMAIN=gc.gabnotes.org"
|
"GOATCOUNTER_DOMAIN=gc.gabnotes.org"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
plugins = ["mypy_django_plugin.main"]
|
||||||
|
mypy_path = "$MYPY_CONFIG_FILE_DIR/stubs:$MYPY_CONFIG_FILE_DIR/src"
|
||||||
|
exclude = "/migrations/"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"environ",
|
||||||
|
"django_otp.plugins.otp_static.models",
|
||||||
|
"two_factor.models",
|
||||||
|
"django_otp.plugins.otp_totp.models",
|
||||||
|
"model_bakery"
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.django-stubs]
|
||||||
|
django_settings_module = "blog.settings"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
|
@ -78,6 +78,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
queryset = queryset.prefetch_related("tags")
|
queryset = queryset.prefetch_related("tags")
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@admin.action(description="Publish selected articles")
|
||||||
def publish(self, request, queryset):
|
def publish(self, request, queryset):
|
||||||
if not request.user.has_perm("articles.change_article"):
|
if not request.user.has_perm("articles.change_article"):
|
||||||
messages.warning(request, "You're not allowed to do this.")
|
messages.warning(request, "You're not allowed to do this.")
|
||||||
|
@ -86,8 +87,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
article.publish()
|
article.publish()
|
||||||
messages.success(request, f"{len(queryset)} articles published.")
|
messages.success(request, f"{len(queryset)} articles published.")
|
||||||
|
|
||||||
publish.short_description = "Publish selected articles"
|
@admin.action(description="Unpublish selected articles")
|
||||||
|
|
||||||
def unpublish(self, request, queryset):
|
def unpublish(self, request, queryset):
|
||||||
if not request.user.has_perm("articles.change_article"):
|
if not request.user.has_perm("articles.change_article"):
|
||||||
messages.warning(request, "You're not allowed to do this.")
|
messages.warning(request, "You're not allowed to do this.")
|
||||||
|
@ -96,8 +96,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
article.unpublish()
|
article.unpublish()
|
||||||
messages.success(request, f"{len(queryset)} articles unpublished.")
|
messages.success(request, f"{len(queryset)} articles unpublished.")
|
||||||
|
|
||||||
unpublish.short_description = "Unpublish selected articles"
|
@admin.action(description="Refresh draft key of selected articles")
|
||||||
|
|
||||||
def refresh_draft_key(self, request, queryset):
|
def refresh_draft_key(self, request, queryset):
|
||||||
if not request.user.has_perm("articles.change_article"):
|
if not request.user.has_perm("articles.change_article"):
|
||||||
messages.warning(request, "You're not allowed to do this.")
|
messages.warning(request, "You're not allowed to do this.")
|
||||||
|
@ -106,7 +105,6 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
article.refresh_draft_key()
|
article.refresh_draft_key()
|
||||||
messages.success(request, f"{len(queryset)} draft keys refreshed.")
|
messages.success(request, f"{len(queryset)} draft keys refreshed.")
|
||||||
|
|
||||||
refresh_draft_key.short_description = "Refresh draft key of selected articles"
|
|
||||||
actions = [publish, unpublish, refresh_draft_key]
|
actions = [publish, unpublish, refresh_draft_key]
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -134,11 +132,10 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
def read_time(self, instance: Article):
|
def read_time(self, instance: Article):
|
||||||
return f"{instance.get_read_time()} min"
|
return f"{instance.get_read_time()} min"
|
||||||
|
|
||||||
|
@admin.display(boolean=True)
|
||||||
def has_custom_css(self, instance: Article):
|
def has_custom_css(self, instance: Article):
|
||||||
return bool(instance.custom_css)
|
return bool(instance.custom_css)
|
||||||
|
|
||||||
has_custom_css.boolean = True
|
|
||||||
|
|
||||||
|
|
||||||
@register(Tag)
|
@register(Tag)
|
||||||
class TagAdmin(admin.ModelAdmin):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from articles.models import Article
|
from articles.models import Article
|
||||||
from attachments.models import Attachment
|
from attachments.models import Attachment
|
||||||
|
@ -8,7 +11,7 @@ IGNORED_PATHS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def drafts_count(request):
|
def drafts_count(request: HttpRequest) -> dict[str, Any]:
|
||||||
if request.path in IGNORED_PATHS:
|
if request.path in IGNORED_PATHS:
|
||||||
return {}
|
return {}
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
@ -16,13 +19,13 @@ def drafts_count(request):
|
||||||
return {"drafts_count": Article.objects.filter(status=Article.DRAFT).count()}
|
return {"drafts_count": Article.objects.filter(status=Article.DRAFT).count()}
|
||||||
|
|
||||||
|
|
||||||
def date_format(request):
|
def date_format(request: HttpRequest) -> dict[str, Any]:
|
||||||
if request.path in IGNORED_PATHS:
|
if request.path in IGNORED_PATHS:
|
||||||
return {}
|
return {}
|
||||||
return {"CUSTOM_ISO": r"Y-m-d\TH:i:sO", "ISO_DATE": "Y-m-d"}
|
return {"CUSTOM_ISO": r"Y-m-d\TH:i:sO", "ISO_DATE": "Y-m-d"}
|
||||||
|
|
||||||
|
|
||||||
def git_version(request):
|
def git_version(request: HttpRequest) -> dict[str, Any]:
|
||||||
if request.path in IGNORED_PATHS:
|
if request.path in IGNORED_PATHS:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
|
@ -36,13 +39,13 @@ def git_version(request):
|
||||||
return {"git_version": version, "git_version_url": url}
|
return {"git_version": version, "git_version_url": url}
|
||||||
|
|
||||||
|
|
||||||
def analytics(request):
|
def analytics(request: HttpRequest) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"goatcounter_domain": settings.GOATCOUNTER_DOMAIN,
|
"goatcounter_domain": settings.GOATCOUNTER_DOMAIN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def open_graph_image_url(request):
|
def open_graph_image_url(request: HttpRequest) -> dict[str, Any]:
|
||||||
if request.path in IGNORED_PATHS:
|
if request.path in IGNORED_PATHS:
|
||||||
return {}
|
return {}
|
||||||
open_graph_image = Attachment.objects.get_open_graph_image()
|
open_graph_image = Attachment.objects.get_open_graph_image()
|
||||||
|
@ -52,11 +55,11 @@ def open_graph_image_url(request):
|
||||||
return {"open_graph_image_url": url}
|
return {"open_graph_image_url": url}
|
||||||
|
|
||||||
|
|
||||||
def blog_metadata(request):
|
def blog_metadata(request: HttpRequest) -> dict[str, Any]:
|
||||||
context = {}
|
return {
|
||||||
context["blog_title"] = settings.BLOG["title"]
|
"blog_title": settings.BLOG["title"],
|
||||||
context["blog_description"] = settings.BLOG["description"]
|
"blog_description": settings.BLOG["description"],
|
||||||
context["blog_author"] = settings.BLOG["author"]
|
"blog_author": settings.BLOG["author"],
|
||||||
context["blog_repo_homepage"] = settings.BLOG["repo"]["homepage"]
|
"blog_repo_homepage": settings.BLOG["repo"]["homepage"],
|
||||||
context["blog_status_url"] = settings.BLOG["status_url"]
|
"blog_status_url": settings.BLOG["status_url"],
|
||||||
return context
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import rcssmin
|
import rcssmin
|
||||||
import readtime
|
import readtime
|
||||||
|
@ -32,16 +35,16 @@ class Tag(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("tag", kwargs={"slug": self.slug})
|
return reverse("tag", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
def get_feed_title(self):
|
def get_feed_title(self) -> str:
|
||||||
return f"{self.name} - {settings.BLOG['title']}"
|
return f"{self.name} - {settings.BLOG['title']}"
|
||||||
|
|
||||||
def get_feed_url(self):
|
def get_feed_url(self) -> str:
|
||||||
return reverse("tag-feed", kwargs={"slug": self.slug})
|
return reverse("tag-feed", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,65 +78,65 @@ class Article(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-published_at"]
|
ordering = ["-published_at"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("article-detail", kwargs={"slug": self.slug})
|
return reverse("article-detail", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
def get_mailto_url(self):
|
def get_mailto_url(self) -> str:
|
||||||
email = settings.BLOG["email"]
|
email = settings.BLOG["email"]
|
||||||
return f"mailto:{email}?subject={self.title}"
|
return f"mailto:{email}?subject={self.title}"
|
||||||
|
|
||||||
def get_abstract(self):
|
def get_abstract(self) -> str:
|
||||||
html = self.get_formatted_content
|
html = self.get_formatted_content
|
||||||
return html.split("<!--more-->")[0]
|
return html.split("<!--more-->")[0]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_description(self):
|
def get_description(self) -> str:
|
||||||
html = self.get_formatted_content
|
html = self.get_formatted_content
|
||||||
text = find_first_paragraph_with_text(html)
|
text = find_first_paragraph_with_text(html)
|
||||||
return truncate_words_after_char_count(text, 160)
|
return truncate_words_after_char_count(text, 160)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_formatted_content(self):
|
def get_formatted_content(self) -> str:
|
||||||
return format_article_content(self.content)
|
return format_article_content(self.content)
|
||||||
|
|
||||||
def publish(self):
|
def publish(self) -> Article:
|
||||||
if not self.published_at:
|
if not self.published_at:
|
||||||
self.published_at = timezone.now()
|
self.published_at = timezone.now()
|
||||||
self.status = self.PUBLISHED
|
self.status = self.PUBLISHED
|
||||||
self.save()
|
self.save()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def unpublish(self):
|
def unpublish(self) -> Article:
|
||||||
self.published_at = None
|
self.published_at = None
|
||||||
self.status = self.DRAFT
|
self.status = self.DRAFT
|
||||||
self.save()
|
self.save()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs) -> None:
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(self.title)
|
self.slug = slugify(self.title)
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def draft_public_url(self):
|
def draft_public_url(self) -> str:
|
||||||
url = self.get_absolute_url() + f"?draft_key={self.draft_key}"
|
url = self.get_absolute_url() + f"?draft_key={self.draft_key}"
|
||||||
return build_full_absolute_url(request=None, url=url)
|
return build_full_absolute_url(request=None, url=url)
|
||||||
|
|
||||||
def refresh_draft_key(self):
|
def refresh_draft_key(self) -> None:
|
||||||
self.draft_key = uuid.uuid4()
|
self.draft_key = uuid.uuid4()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def get_read_time(self):
|
def get_read_time(self) -> int:
|
||||||
content = self.get_formatted_content
|
content = self.get_formatted_content
|
||||||
if content:
|
if content:
|
||||||
return readtime.of_html(content).minutes
|
return readtime.of_html(content).minutes
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_related_articles(self):
|
def get_related_articles(self) -> Sequence[Article]:
|
||||||
related_articles = set()
|
related_articles = set()
|
||||||
published_articles = Article.objects.filter(status=Article.PUBLISHED).exclude(
|
published_articles = Article.objects.filter(status=Article.PUBLISHED).exclude(
|
||||||
pk=self.pk
|
pk=self.pk
|
||||||
|
@ -141,19 +144,19 @@ class Article(models.Model):
|
||||||
for tag in self.tags.all().prefetch_related(
|
for tag in self.tags.all().prefetch_related(
|
||||||
Prefetch("articles", published_articles, to_attr="published_articles")
|
Prefetch("articles", published_articles, to_attr="published_articles")
|
||||||
):
|
):
|
||||||
related_articles.update(tag.published_articles)
|
related_articles.update(tag.published_articles) # type: ignore
|
||||||
sample_size = min([len(related_articles), 3])
|
sample_size = min([len(related_articles), 3])
|
||||||
return random.sample(list(related_articles), sample_size)
|
return random.sample(list(related_articles), sample_size)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def keywords(self):
|
def keywords(self) -> str:
|
||||||
return ", ".join(map(lambda tag: tag.name, self.tags.all()))
|
return ", ".join(map(lambda tag: tag.name, self.tags.all()))
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def get_minified_custom_css(self):
|
def get_minified_custom_css(self) -> str:
|
||||||
return rcssmin.cssmin(self.custom_css)
|
return rcssmin.cssmin(self.custom_css)
|
||||||
|
|
||||||
def get_admin_url(self):
|
def get_admin_url(self) -> str:
|
||||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||||
return reverse(
|
return reverse(
|
||||||
f"admin:{content_type.app_label}_{content_type.model}_change",
|
f"admin:{content_type.app_label}_{content_type.model}_change",
|
||||||
|
|
|
@ -25,11 +25,11 @@ def test_render_article_same_content(published_article: Article, client: Client)
|
||||||
assert api_res.status_code == 200
|
assert api_res.status_code == 200
|
||||||
assert standard_res.status_code == 200
|
assert standard_res.status_code == 200
|
||||||
# ignore an expected difference
|
# ignore an expected difference
|
||||||
api_content = api_res.content.decode("utf-8") # type: str
|
api_content: str = api_res.content.decode("utf-8")
|
||||||
standard_content = standard_res.content.decode("utf-8")
|
standard_content: str = standard_res.content.decode("utf-8")
|
||||||
api_content = api_content.replace(
|
api_content = api_content.replace(
|
||||||
"?next=/api/render/1/",
|
"/api/render/1/",
|
||||||
"?next=/some-article-slug/",
|
"/some-article-slug/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert api_content == standard_content
|
assert api_content == standard_content
|
||||||
|
@ -41,7 +41,7 @@ def test_render_article_change_content(published_article: Article, client: Clien
|
||||||
preview_content = "This is a different content **with strong emphasis**"
|
preview_content = "This is a different content **with strong emphasis**"
|
||||||
api_res = post_article(client, published_article, preview_content)
|
api_res = post_article(client, published_article, preview_content)
|
||||||
assert api_res.status_code == 200
|
assert api_res.status_code == 200
|
||||||
api_content = api_res.content.decode("utf-8") # type: str
|
api_content: str = api_res.content.decode("utf-8")
|
||||||
html_preview_content = format_article_content(preview_content)
|
html_preview_content = format_article_content(preview_content)
|
||||||
assert html_preview_content in api_content
|
assert html_preview_content in api_content
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ def test_only_title_shown_on_list(client: Client, author: User):
|
||||||
status=Article.PUBLISHED,
|
status=Article.PUBLISHED,
|
||||||
author=author,
|
author=author,
|
||||||
content=f"{abstract}\n<!--more-->\n{after}",
|
content=f"{abstract}\n<!--more-->\n{after}",
|
||||||
) # type: Article
|
)
|
||||||
res = client.get(reverse("articles-list"))
|
res = client.get(reverse("articles-list"))
|
||||||
content = res.content.decode("utf-8")
|
content = res.content.decode("utf-8")
|
||||||
assert title in content
|
assert title in content
|
||||||
|
|
|
@ -9,14 +9,14 @@ from markdown.extensions.toc import TocExtension
|
||||||
from articles.markdown import LazyLoadingImageExtension
|
from articles.markdown import LazyLoadingImageExtension
|
||||||
|
|
||||||
|
|
||||||
def build_full_absolute_url(request, url):
|
def build_full_absolute_url(request, url: str) -> str:
|
||||||
if request:
|
if request:
|
||||||
return request.build_absolute_uri(url)
|
return request.build_absolute_uri(url)
|
||||||
else:
|
else:
|
||||||
return (settings.BLOG["base_url"] + url)[::-1].replace("//", "/", 1)[::-1]
|
return (settings.BLOG["base_url"] + url)[::-1].replace("//", "/", 1)[::-1]
|
||||||
|
|
||||||
|
|
||||||
def format_article_content(content):
|
def format_article_content(content: str) -> str:
|
||||||
md = markdown.Markdown(
|
md = markdown.Markdown(
|
||||||
extensions=[
|
extensions=[
|
||||||
"extra",
|
"extra",
|
||||||
|
@ -30,7 +30,7 @@ def format_article_content(content):
|
||||||
return md.convert(content)
|
return md.convert(content)
|
||||||
|
|
||||||
|
|
||||||
def truncate_words_after_char_count(text, char_count):
|
def truncate_words_after_char_count(text: str, char_count: int) -> str:
|
||||||
total_length = 0
|
total_length = 0
|
||||||
text_result = []
|
text_result = []
|
||||||
for word in text.split():
|
for word in text.split():
|
||||||
|
@ -41,14 +41,10 @@ def truncate_words_after_char_count(text, char_count):
|
||||||
return " ".join(text_result) + "..."
|
return " ".join(text_result) + "..."
|
||||||
|
|
||||||
|
|
||||||
def find_first_paragraph_with_text(html):
|
def find_first_paragraph_with_text(html: str) -> str:
|
||||||
bs = BeautifulSoup(html, "html.parser")
|
bs = BeautifulSoup(html, "html.parser")
|
||||||
paragraph = bs.find("p", recursive=False)
|
paragraphs = bs.find_all("p", recursive=False)
|
||||||
text = paragraph.text.strip()
|
for paragraph in paragraphs:
|
||||||
while not text:
|
if paragraph.text.strip():
|
||||||
try:
|
return paragraph.text
|
||||||
paragraph = paragraph.next_sibling
|
return ""
|
||||||
text = paragraph.text.strip()
|
|
||||||
except Exception:
|
|
||||||
break
|
|
||||||
return text
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
@ -8,7 +10,7 @@ from articles.models import Article, Tag
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def render_article(request, article_pk):
|
def render_article(request: HttpRequest, article_pk: int) -> HttpResponse:
|
||||||
template = "articles/article_detail.html"
|
template = "articles/article_detail.html"
|
||||||
article = Article.objects.get(pk=article_pk)
|
article = Article.objects.get(pk=article_pk)
|
||||||
article.content = request.POST.get("content", article.content)
|
article.content = request.POST.get("content", article.content)
|
||||||
|
@ -17,10 +19,9 @@ def render_article(request, article_pk):
|
||||||
has_code = request.POST.get("has_code")
|
has_code = request.POST.get("has_code")
|
||||||
if has_code is not None:
|
if has_code is not None:
|
||||||
article.has_code = has_code == "true"
|
article.has_code = has_code == "true"
|
||||||
context = {"article": article}
|
context: dict[str, Any] = {"article": article}
|
||||||
tags = request.POST.get("tag_ids")
|
tags = request.POST.get("tag_ids")
|
||||||
if tags:
|
if tags:
|
||||||
tags = Tag.objects.filter(pk__in=map(int, tags.split(",")))
|
context["tags"] = Tag.objects.filter(pk__in=map(int, tags.split(",")))
|
||||||
context["tags"] = tags
|
|
||||||
html = render(request, template, context=context)
|
html = render(request, template, context=context)
|
||||||
return HttpResponse(html)
|
return HttpResponse(html)
|
||||||
|
|
|
@ -18,7 +18,7 @@ class CompleteFeed(Feed):
|
||||||
def items(self, obj):
|
def items(self, obj):
|
||||||
return self.get_queryset(obj)[: self.FEED_LIMIT]
|
return self.get_queryset(obj)[: self.FEED_LIMIT]
|
||||||
|
|
||||||
def item_description(self, item: Article):
|
def item_description(self, item: Article): # type: ignore[override]
|
||||||
return item.get_formatted_content
|
return item.get_formatted_content
|
||||||
|
|
||||||
def item_pubdate(self, item: Article):
|
def item_pubdate(self, item: Article):
|
||||||
|
|
|
@ -50,9 +50,9 @@ class PublicArticleListView(BaseArticleListView):
|
||||||
class ArticlesListView(PublicArticleListView):
|
class ArticlesListView(PublicArticleListView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
home_article = Article.objects.filter(
|
home_article: Article = Article.objects.filter(
|
||||||
status=Article.PUBLISHED, is_home=True
|
status=Article.PUBLISHED, is_home=True
|
||||||
).first() # type: Article
|
).first()
|
||||||
context["article"] = home_article
|
context["article"] = home_article
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -92,8 +92,8 @@ class SearchArticlesListView(PublicArticleListView):
|
||||||
|
|
||||||
class TagArticlesListView(PublicArticleListView):
|
class TagArticlesListView(PublicArticleListView):
|
||||||
tag = None
|
tag = None
|
||||||
main_title = None
|
main_title = ""
|
||||||
html_title = None
|
html_title = ""
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.tag = get_object_or_404(Tag, slug=self.kwargs.get("slug"))
|
self.tag = get_object_or_404(Tag, slug=self.kwargs.get("slug"))
|
||||||
|
@ -136,7 +136,7 @@ class ArticleDetailView(generic.DetailView):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Article:
|
def get_object(self, queryset=None) -> Article:
|
||||||
obj = super().get_object(queryset) # type: Article
|
obj: Article = super().get_object(queryset)
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
obj.views_count = F("views_count") + 1
|
obj.views_count = F("views_count") + 1
|
||||||
obj.save(update_fields=["views_count"])
|
obj.save(update_fields=["views_count"])
|
||||||
|
|
|
@ -56,6 +56,7 @@ class AttachmentAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@admin.action(description="Set as open graph image")
|
||||||
def set_as_open_graph_image(self, request, queryset):
|
def set_as_open_graph_image(self, request, queryset):
|
||||||
if len(queryset) != 1:
|
if len(queryset) != 1:
|
||||||
messages.error(request, "You must select only one attachment")
|
messages.error(request, "You must select only one attachment")
|
||||||
|
@ -64,8 +65,7 @@ class AttachmentAdmin(admin.ModelAdmin):
|
||||||
queryset.update(open_graph_image=True)
|
queryset.update(open_graph_image=True)
|
||||||
messages.success(request, "Done")
|
messages.success(request, "Done")
|
||||||
|
|
||||||
set_as_open_graph_image.short_description = "Set as open graph image"
|
@admin.action(description="Reprocess selected attachments")
|
||||||
|
|
||||||
def reprocess_selected_attachments(self, request, queryset):
|
def reprocess_selected_attachments(self, request, queryset):
|
||||||
if len(queryset) == 0:
|
if len(queryset) == 0:
|
||||||
messages.error(request, "You must select at least one attachment")
|
messages.error(request, "You must select at least one attachment")
|
||||||
|
@ -73,5 +73,3 @@ class AttachmentAdmin(admin.ModelAdmin):
|
||||||
for attachment in queryset:
|
for attachment in queryset:
|
||||||
attachment.reprocess()
|
attachment.reprocess()
|
||||||
messages.success(request, "Attachments were successfully reprocessed.")
|
messages.success(request, "Attachments were successfully reprocessed.")
|
||||||
|
|
||||||
reprocess_selected_attachments.short_description = "Reprocess selected attachments"
|
|
||||||
|
|
|
@ -13,12 +13,12 @@ Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
import debug_toolbar
|
import debug_toolbar # type: ignore
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from two_factor.urls import urlpatterns as tf_urls
|
from two_factor.urls import urlpatterns as tf_urls # type: ignore
|
||||||
|
|
||||||
from blog import settings
|
from blog import settings
|
||||||
|
|
||||||
|
|
3
stubs/rcssmin.pyi
Normal file
3
stubs/rcssmin.pyi
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
cssmin: Callable[[str], str]
|
3
stubs/readtime/__init__.pyi
Normal file
3
stubs/readtime/__init__.pyi
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .api import of_html as of_html
|
||||||
|
from .api import of_markdown as of_markdown
|
||||||
|
from .api import of_text as of_text
|
5
stubs/readtime/api.pyi
Normal file
5
stubs/readtime/api.pyi
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .result import Result
|
||||||
|
|
||||||
|
def of_text(text, wpm: int | None = ...) -> Result: ...
|
||||||
|
def of_html(html, wpm: int | None = ...) -> Result: ...
|
||||||
|
def of_markdown(markdown, wpm: int | None = ...) -> Result: ...
|
14
stubs/readtime/result.pyi
Normal file
14
stubs/readtime/result.pyi
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
class Result:
|
||||||
|
delta: timedelta | None
|
||||||
|
wpm: int | None
|
||||||
|
def __init__(self, seconds: int | None = ..., wpm: int | None = ...) -> None: ...
|
||||||
|
def __unicode__(self): ...
|
||||||
|
@property
|
||||||
|
def seconds(self) -> int: ...
|
||||||
|
@property
|
||||||
|
def minutes(self) -> int: ...
|
||||||
|
@property
|
||||||
|
def text(self) -> str: ...
|
||||||
|
def total_seconds(self, delta) -> int: ...
|
9
tasks.py
9
tasks.py
|
@ -22,6 +22,13 @@ def test_cov(ctx):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task(post=[test_cov])
|
||||||
|
def check(ctx):
|
||||||
|
with ctx.cd(BASE_DIR):
|
||||||
|
ctx.run("pre-commit run --all-files", pty=True)
|
||||||
|
ctx.run("mypy src", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def build(ctx):
|
def build(ctx):
|
||||||
with ctx.cd(BASE_DIR):
|
with ctx.cd(BASE_DIR):
|
||||||
|
@ -39,7 +46,7 @@ def deploy(ctx):
|
||||||
ctx.run("ssh ubuntu /home/gaugendre/blog/update", pty=True, echo=True)
|
ctx.run("ssh ubuntu /home/gaugendre/blog/update", pty=True, echo=True)
|
||||||
|
|
||||||
|
|
||||||
@task(pre=[build, publish, deploy])
|
@task(pre=[check, build, publish, deploy])
|
||||||
def beam(ctx):
|
def beam(ctx):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Reference in a new issue