Compare commits
No commits in common. "main" and "master" have entirely different histories.
140 changed files with 2406 additions and 3021 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
# Created by https://www.gitignore.io/api/django
|
# Created by https://www.gitignore.io/api/django
|
||||||
|
|
||||||
### Django ###
|
### Django ###
|
||||||
|
@ -14,10 +15,8 @@ media
|
||||||
# <django-project-name>/staticfiles/
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/django
|
# End of https://www.gitignore.io/api/django
|
||||||
|
|
||||||
/.pdm-python
|
|
||||||
/.env
|
/.env
|
||||||
/.mypy_cache
|
/.mypy_cache
|
||||||
/.pytest_cache
|
/.pytest_cache
|
||||||
/static
|
/static
|
||||||
/node_modules
|
node_modules
|
||||||
|
|
21
.gitlab-ci.yml
Normal file
21
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
image: python:3.6
|
||||||
|
services:
|
||||||
|
- postgres:latest
|
||||||
|
variables:
|
||||||
|
GIT_SUBMODULE_STRATEGY: normal
|
||||||
|
PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
|
||||||
|
PIPENV_CACHE_DIR: $CI_PROJECT_DIR/.cache/pipenv
|
||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_DB: nice_marmot
|
||||||
|
POSTGRES_USER: runner
|
||||||
|
POSTGRES_PASSWORD: ''
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
|
||||||
|
test:
|
||||||
|
script:
|
||||||
|
- pip install pipenv
|
||||||
|
- pipenv sync --dev
|
||||||
|
- pipenv run pytest
|
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
||||||
[submodule "lemoncurry/static/base16-materialtheme-scheme"]
|
[submodule "lemoncurry/static/base16-materialtheme-scheme"]
|
||||||
path = lemoncurry/static/base16-materialtheme-scheme
|
path = lemoncurry/static/base16-materialtheme-scheme
|
||||||
url = https://github.com/ntpeters/base16-materialtheme-scheme
|
url = git://github.com/ntpeters/base16-materialtheme-scheme.git
|
||||||
|
|
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.1.0
|
||||||
|
hooks:
|
||||||
|
- id: check-byte-order-marker
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
- id: check-json
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: flake8
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args:
|
||||||
|
- --fix=lf
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: pytest
|
||||||
|
name: Check pytest unit tests pass
|
||||||
|
entry: pipenv run pytest
|
||||||
|
pass_filenames: false
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
- id: mypy
|
||||||
|
name: Check mypy static types match
|
||||||
|
entry: pipenv run mypy . --ignore-missing-imports
|
||||||
|
pass_filenames: false
|
||||||
|
language: system
|
||||||
|
types: [python]
|
3
.pyup.yml
Normal file
3
.pyup.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
requirements:
|
||||||
|
- Pipfile
|
||||||
|
- Pipfile.lock
|
16
.travis.yml
Normal file
16
.travis.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
language: python
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $PIP_CACHE_DIR
|
||||||
|
- $PIPENV_CACHE_DIR
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- PIP_CACHE_DIR=$HOME/.cache/pip
|
||||||
|
- PIPENV_CACHE_DIR=$HOME/.cache/pipenv
|
||||||
|
python:
|
||||||
|
- '3.6'
|
||||||
|
install:
|
||||||
|
- pip install pipenv
|
||||||
|
- pipenv install --dev
|
||||||
|
script:
|
||||||
|
- pipenv run pytest
|
4
Forwardfile
Normal file
4
Forwardfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# vim: set ft=yaml :
|
||||||
|
host: 00dani.dev
|
||||||
|
port: 443
|
||||||
|
cname: dev.00dani.me
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 - 2024 Danielle McLean
|
Copyright (c) 2017 - 2018 Danielle McLean
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
58
Pipfile
Normal file
58
Pipfile
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = '3.7'
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
django = "*"
|
||||||
|
django-compressor = "*"
|
||||||
|
gunicorn = {extras = ["gevent"]}
|
||||||
|
"psycopg2-binary" = "*"
|
||||||
|
pillow = "*"
|
||||||
|
django-meta = "*"
|
||||||
|
django-activeurl = "*"
|
||||||
|
django-otp = "*"
|
||||||
|
qrcode = "*"
|
||||||
|
django-otp-agents = "*"
|
||||||
|
python-slugify = "*"
|
||||||
|
"mf2py" = "*"
|
||||||
|
markdown = "*"
|
||||||
|
bleach = "*"
|
||||||
|
django-debug-toolbar = "*"
|
||||||
|
xrd = "*"
|
||||||
|
django-push = "*"
|
||||||
|
pyyaml = "*"
|
||||||
|
django-annoying = "*"
|
||||||
|
accept-types = "*"
|
||||||
|
django-analytical = "*"
|
||||||
|
django-model-utils = "*"
|
||||||
|
django-rq = "*"
|
||||||
|
ronkyuu = "*"
|
||||||
|
cachecontrol = "*"
|
||||||
|
hiredis = "*"
|
||||||
|
"mf2util" = "*"
|
||||||
|
django-cors-headers = "*"
|
||||||
|
"argon2-cffi" = "*"
|
||||||
|
python-baseconv = "*"
|
||||||
|
django-computed-property = "*"
|
||||||
|
docutils = "*"
|
||||||
|
django-super-favicon = "*"
|
||||||
|
django-redis = "*"
|
||||||
|
gevent = "*"
|
||||||
|
django-extensions = "*"
|
||||||
|
python-magic = "*"
|
||||||
|
pyup-django = "*"
|
||||||
|
"jinja2" = "*"
|
||||||
|
msgpack = "*"
|
||||||
|
django-randomslugfield = "*"
|
||||||
|
ago = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
ptpython = "*"
|
||||||
|
pytest-django = "*"
|
||||||
|
werkzeug = "*"
|
||||||
|
watchdog = "*"
|
||||||
|
mypy = "*"
|
989
Pipfile.lock
generated
Normal file
989
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,989 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "1c5a457377ca29205680f06286e9ef47d6ff08d1de75b2e7371ca3993bf75a1c"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.7"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.python.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"accept-types": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9ae86512bf3a3eaad6a2793617a34eb15b384593e6c28697bef9b15ac237017a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
|
"ago": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9d1956edd8103c266d968ae2a7eaf2f23470b6384e655aaaf54d1158408178ad"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.0.93"
|
||||||
|
},
|
||||||
|
"argon2-cffi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:003f588de43a817af6ecc1c06103fa0801de63849db3cb0f37576bb2da29043d",
|
||||||
|
"sha256:04528ebbcc5d77eb49e7c2560fcf9d489cdc3b14f89fdd975c72c0a12934025a",
|
||||||
|
"sha256:04ead34244af38d79742cc46a212fec94daf99b49add66878f5d4b22da72d4aa",
|
||||||
|
"sha256:0529aeb71b50e068d300c992f850387c2456f2d3d4083d17d18e75710d057682",
|
||||||
|
"sha256:0d18a3dcb4ca7f3717155994a4f131a43072e47b708e57c4be16f60253337dfd",
|
||||||
|
"sha256:0ecbd2346da3e5af84427fd8df3ece484c903a9dafd9470571def47df54f2780",
|
||||||
|
"sha256:193de795483b00d752d16ec5df11d119a3a2c43f5464edfaf919a2ca9cc5b991",
|
||||||
|
"sha256:22a99f90da7176ee86fbdfb0a95411bc807b9d795b89495ee88c2e0468a496e8",
|
||||||
|
"sha256:2829d648dfa4d42ce33ec0f36e863d1068fd729b38ef6f830262b43e04f9ba1c",
|
||||||
|
"sha256:3b61a4ef1eb785d41f190520db716aa598d15f147419cbbdc9061dc232126f09",
|
||||||
|
"sha256:3ddcdde047cd4dba2bcce7d890dcefd6723548b849fa82ba87e04a468079b9b1",
|
||||||
|
"sha256:457c5db9bb99f2ffb7ce9ebf923b523898e75464dd019fbebdd1c6096ddcf044",
|
||||||
|
"sha256:51d78eedbba1f9e45a1c3fb1470ad6d1faafc6ec42eabb969df29c2aa848b645",
|
||||||
|
"sha256:af0d3dbc8f32d95be480eedd5d77fe8714f5441a28b9abcfa687ecf5301a1abd",
|
||||||
|
"sha256:ca65f736d2129687008178e3d9956264fd2be2f69429edf0d755c2f97cd003f1",
|
||||||
|
"sha256:d371fcd42e01c78c76397120d07c67f6e16f5fef97d327ad372c8debe38f9f56",
|
||||||
|
"sha256:ec12248d4c1e045a736beebf55daf1430c45a29ab8d773d8540c224555784275"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==18.3.0"
|
||||||
|
},
|
||||||
|
"beautifulsoup4": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858",
|
||||||
|
"sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348",
|
||||||
|
"sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718"
|
||||||
|
],
|
||||||
|
"version": "==4.7.1"
|
||||||
|
},
|
||||||
|
"bleach": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
||||||
|
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.1.0"
|
||||||
|
},
|
||||||
|
"cachecontrol": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cef77effdf51b43178f6a2d3b787e3734f98ade253fa3187f3bb7315aaa42ff7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.12.5"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
||||||
|
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
||||||
|
],
|
||||||
|
"version": "==2018.11.29"
|
||||||
|
},
|
||||||
|
"cffi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
||||||
|
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
||||||
|
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
||||||
|
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
||||||
|
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
|
||||||
|
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
||||||
|
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
||||||
|
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
|
||||||
|
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
||||||
|
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
|
||||||
|
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
||||||
|
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
||||||
|
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
||||||
|
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
||||||
|
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
||||||
|
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
||||||
|
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
||||||
|
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
||||||
|
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
|
||||||
|
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
||||||
|
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
||||||
|
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
||||||
|
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
||||||
|
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
||||||
|
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
|
||||||
|
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
|
||||||
|
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
|
||||||
|
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
|
||||||
|
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
|
||||||
|
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
|
||||||
|
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
||||||
|
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.5"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||||
|
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||||
|
],
|
||||||
|
"version": "==7.0"
|
||||||
|
},
|
||||||
|
"django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8",
|
||||||
|
"sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.1.5"
|
||||||
|
},
|
||||||
|
"django-activeurl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ad5498bf589afaa117fe1c80d1a4fdbef29185cee47517254cd8f273b8a0140d",
|
||||||
|
"sha256:ebb3f2746fdc76fee2095b75cad713e746378393c6c2b8e36455919a780acd50"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.1.12"
|
||||||
|
},
|
||||||
|
"django-agent-trust": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f0ded1c2e1e8b06ea050f48c8db931ab7d2f85c566065dceb8e827d0690d87c5",
|
||||||
|
"sha256:f7e24d3f50a0727c6a70d671778de3cca23a0c87bedc6e3dae1f61af7e759fc1"
|
||||||
|
],
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"django-analytical": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0871db04aa8c864b0b44c98b462d2b8682ba9c31ab89f50d54a5ddfdc3ff6223",
|
||||||
|
"sha256:bbe09b15cf22d7d4ab4cd0a5cc454aa912f08691d86886a0652081b9fd7a1ff0",
|
||||||
|
"sha256:d9e475567fc021ed9e3bd1fa77db11a14b12c85fbc5b56121897b0fa571e6ed6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.5.0"
|
||||||
|
},
|
||||||
|
"django-annoying": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:93f54d244d453cba28d6cfb9deae7bba27e859762adee9ff7de4706017940931",
|
||||||
|
"sha256:ee620f9bfe439061010c7d5ccc8c69514844d95c370c130c61938205fcfc4cc9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.10.4"
|
||||||
|
},
|
||||||
|
"django-appconf": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a4d9aea683b4c224d97ab8ee11ad2d29a37072c0c6c509896dd9857466fb261",
|
||||||
|
"sha256:ddab987d14b26731352c01ee69c090a4ebfc9141ed223bef039d79587f22acd9"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"django-classy-tags": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:792f9161d0e22d55b4fab6fc297bab8ab072ffaa3075b227613a6d8473624db8",
|
||||||
|
"sha256:f6d12f5a4df3e387795a0d9ef2836af389cae9a1fbebda035dac043d4722b1f7"
|
||||||
|
],
|
||||||
|
"version": "==0.8.0"
|
||||||
|
},
|
||||||
|
"django-compressor": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7732676cfb9d58498dfb522b036f75f3f253f72ea1345ac036434fdc418c2e57",
|
||||||
|
"sha256:9616570e5b08e92fa9eadc7a1b1b49639cce07ef392fc27c74230ab08075b30f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.2"
|
||||||
|
},
|
||||||
|
"django-computed-property": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f848873b03e87d4aa5ac9f945e72be3a0daf0451468e186fad3c2ee0a21ff99",
|
||||||
|
"sha256:a464f4e87b50eda415b9a4b4e1a67574cf702b8958ab7cb0aa90e4862912eb29"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.3"
|
||||||
|
},
|
||||||
|
"django-cors-headers": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5545009c9b233ea7e70da7dbab7cb1c12afa01279895086f98ec243d7eab46fa",
|
||||||
|
"sha256:c4c2ee97139d18541a1be7d96fe337d1694623816d83f53cb7c00da9b94acae1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.0"
|
||||||
|
},
|
||||||
|
"django-debug-toolbar": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736",
|
||||||
|
"sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.11"
|
||||||
|
},
|
||||||
|
"django-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8317a3fe479b1ba3e3a04ecf33fb8d6ccf09bb18f30eab64e34c40a593741d26",
|
||||||
|
"sha256:a76a61566f1c8d96acc7bcf765080b8e91367a25a2c6f8c5bddd574493839180"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.1.4"
|
||||||
|
},
|
||||||
|
"django-meta": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:21fc5d0d5fcacda5d038af0babd08afaa4d5bed1b746edb6522c4d3435da8db6",
|
||||||
|
"sha256:dd4a440223cc6243a7815c183b6ada1f11b99b4d672471e67f8db4d8b48a5674"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.4.1"
|
||||||
|
},
|
||||||
|
"django-model-utils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c057f3bf0859aba27f04389f0cedd2d48f8c9b3848acb86fd9970794e58f477",
|
||||||
|
"sha256:8cd377744aa45f9f131d652ec460c57d1aaa88d3e9b586c8e27eb709341b9084"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
|
"django-otp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:529f936e9b6acbb011ac575d2e7e1a6c823661627ebcb314c11cd721237da433",
|
||||||
|
"sha256:9b67805244b8bc48bb5b14600dc1d05a8dca54ad2ff8d6ded758c39856b860e1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"django-otp-agents": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:318254a022e79a565d2fa15ff1e4ef20b92a1284293496ffec044685972ad02e",
|
||||||
|
"sha256:7811706e8b2bfc0185eccac23a9d74b202db443004099e570fabf4a1423f0971"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.5.0"
|
||||||
|
},
|
||||||
|
"django-push": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9d73a27f147ea46f5e92d6ab36c19640b11214b43b378693c8961aaf8bea5b60",
|
||||||
|
"sha256:d5442fcb6d8254a7e837383ce766a72e8fb921f3bcfc2355440c2da8fbcf07b4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.1"
|
||||||
|
},
|
||||||
|
"django-randomslugfield": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8f5866d9383f020fb7f270a218ddc65b6a33d3833633a0c995c68f28cac59efb"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
|
"django-redis": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:af0b393864e91228dd30d8c85b5c44d670b5524cb161b7f9e41acc98b6e5ace7",
|
||||||
|
"sha256:f46115577063d00a890867c6964ba096057f07cb756e78e0503b89cd18e4e083"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.10.0"
|
||||||
|
},
|
||||||
|
"django-rq": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3ab01d75694c4124baa497ebd4d6c7ebbf9d93cb4b390bfe253b62220562a299",
|
||||||
|
"sha256:48a41c464194096af34be7dfba2b1a71a1cec1f466734e51813b9d138fc20676"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
|
"django-super-favicon": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:56cb5268ea73ef3cbde5cb01fef02fea2ec00739cdae0566d3102009f052f683"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||||
|
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||||
|
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.14"
|
||||||
|
},
|
||||||
|
"gevent": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64",
|
||||||
|
"sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea",
|
||||||
|
"sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c",
|
||||||
|
"sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51",
|
||||||
|
"sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e",
|
||||||
|
"sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917",
|
||||||
|
"sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1",
|
||||||
|
"sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c",
|
||||||
|
"sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909",
|
||||||
|
"sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12",
|
||||||
|
"sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8",
|
||||||
|
"sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942",
|
||||||
|
"sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950",
|
||||||
|
"sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8",
|
||||||
|
"sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee",
|
||||||
|
"sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922",
|
||||||
|
"sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e",
|
||||||
|
"sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0",
|
||||||
|
"sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad",
|
||||||
|
"sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51",
|
||||||
|
"sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1",
|
||||||
|
"sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05",
|
||||||
|
"sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.4.0"
|
||||||
|
},
|
||||||
|
"greenlet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
|
||||||
|
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
|
||||||
|
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
|
||||||
|
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
|
||||||
|
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
|
||||||
|
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
|
||||||
|
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
|
||||||
|
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
|
||||||
|
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
|
||||||
|
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
|
||||||
|
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
|
||||||
|
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
|
||||||
|
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
|
||||||
|
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
|
||||||
|
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
|
||||||
|
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
|
||||||
|
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
|
||||||
|
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
|
||||||
|
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"
|
||||||
|
],
|
||||||
|
"markers": "platform_python_implementation == 'CPython'",
|
||||||
|
"version": "==0.4.15"
|
||||||
|
},
|
||||||
|
"gunicorn": {
|
||||||
|
"extras": [
|
||||||
|
"gevent"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
|
||||||
|
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==19.9.0"
|
||||||
|
},
|
||||||
|
"hiredis": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:098d0c767115fb2ffee4f142d76ad86785829ef22b07a0ce2d289eef78186705",
|
||||||
|
"sha256:09f2d7bc980b2d1f402b39d4cc7841988493cf06580fd4b3120cbcf8a73402e5",
|
||||||
|
"sha256:1875ca8a843efd3220e1a39c0e754847030b7fd8ff4102c23d7ab7c791ccf0c2",
|
||||||
|
"sha256:2014924fb557541ee239e5035ddf8c4c03b166a0d6f45b265b1cad867091a617",
|
||||||
|
"sha256:22d814b567774c04459d7ff36979bacc4ada490d316579b3c357cfd949677c30",
|
||||||
|
"sha256:28c9da46e2f51d9a3b59dbf42e41278c979413bc0067a119fd51cf28a2f36379",
|
||||||
|
"sha256:4c0f4f3c94f4c9071e3f58546d05757a9e2e49e64769395963238548807ecd22",
|
||||||
|
"sha256:5ae5dd2d78ec1ea593207fa1c9bdc46da8b1713f038695f83457bc8307243ce2",
|
||||||
|
"sha256:602f667281cedf0e21c8648aaf72e70d6a0ccf7ee274b4bc9ea6aa889f27b66c",
|
||||||
|
"sha256:66344a5bdf19b39ba3c00101663806ef92fd75b1451a2f3f33ca73352fabaa01",
|
||||||
|
"sha256:691e5c4efca2cf974ed24414af4d32c91bbf7650353504da918e0c29ad171d8b",
|
||||||
|
"sha256:69a9d6c742ffd586093def8ac3dd1907907c8396b36af8b0490e9e52f0015e99",
|
||||||
|
"sha256:726e546d6b2133787b6c690176ed54d969f5c90f5bda650c5cbaa44bf18e4507",
|
||||||
|
"sha256:73227ed9d34648eae694cf3b4683840ea0e7dace682dcf3f1f732b42f9f0631c",
|
||||||
|
"sha256:736a30992ede64b79989869e1a068fec93405dac71d87a316f22ee72e75cb6a5",
|
||||||
|
"sha256:79742f23dbb6a8dcf8b857ced49e81f872a792f587a98f0a4fe620a175eb8b70",
|
||||||
|
"sha256:7bf565198d612ad6b19a83b2d65a8e88d2b82608b0c4513c89ecfc25bd1dcce0",
|
||||||
|
"sha256:ae104a4a622320e0174fb82874d383b6b5fef0e3a38375695e01300107cc3e67",
|
||||||
|
"sha256:bdd78231aac38eadd00112c6eb8f6862ce9264a397602cd7eb6fe47e1c7273ba",
|
||||||
|
"sha256:c6a7ab327dd01d77096f60bfaff1d0d5386960190fbd8a846ec8daafd529e0e3",
|
||||||
|
"sha256:cac4abfbd42d3bb7a363c51752f909645559617e3cbd2c9fd175159cd8bbc102",
|
||||||
|
"sha256:cf216d6fbacef0a20d23755d2d453dc6672ba5fd2b0cf76c548f4f620fce2cfe",
|
||||||
|
"sha256:d8222d09da762891d4ef39374b1f2318da0ba02faf609d254438672892155eee",
|
||||||
|
"sha256:dac079ff66c2d1c56f62f0a9b0a7a01784ad7b9eb136b9a931f7ab57463e70de",
|
||||||
|
"sha256:e68638e460e95a68dbcef6f78be8210f4405eac020f10ee52cafee54084d3699",
|
||||||
|
"sha256:fb8b80d2fb45857fd19d04387e4207412124e470c3b5d4d3568a6d9b74a72e99",
|
||||||
|
"sha256:fc3eb53b51acf467ae7d60e92af23dee33b20871d84ce33e6e61b9e00077ff48",
|
||||||
|
"sha256:ffe6d9bc7f3637d962eb4706a2908ba4064ec997beb99540bb385a0f7d7fe39a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"html5lib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
|
||||||
|
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||||
|
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.8"
|
||||||
|
},
|
||||||
|
"isodate": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
|
||||||
|
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
|
||||||
|
],
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||||
|
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"lxml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21",
|
||||||
|
"sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0",
|
||||||
|
"sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297",
|
||||||
|
"sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591",
|
||||||
|
"sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801",
|
||||||
|
"sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f",
|
||||||
|
"sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541",
|
||||||
|
"sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58",
|
||||||
|
"sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6",
|
||||||
|
"sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730",
|
||||||
|
"sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49",
|
||||||
|
"sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc",
|
||||||
|
"sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879",
|
||||||
|
"sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6",
|
||||||
|
"sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e",
|
||||||
|
"sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c",
|
||||||
|
"sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2",
|
||||||
|
"sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d",
|
||||||
|
"sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485",
|
||||||
|
"sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e",
|
||||||
|
"sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73",
|
||||||
|
"sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488",
|
||||||
|
"sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5",
|
||||||
|
"sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11",
|
||||||
|
"sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298",
|
||||||
|
"sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af"
|
||||||
|
],
|
||||||
|
"version": "==4.3.0"
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
|
||||||
|
"sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.1"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||||
|
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||||
|
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||||
|
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||||
|
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||||
|
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||||
|
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||||
|
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||||
|
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||||
|
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||||
|
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||||
|
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||||
|
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||||
|
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||||
|
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||||
|
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||||
|
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||||
|
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||||
|
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||||
|
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||||
|
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||||
|
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||||
|
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||||
|
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||||
|
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||||
|
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||||
|
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||||
|
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"mf2py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84f1f8f2ff3f1deb1c30be497e7ccd805452996a662fd4a77f09e0105bede2c9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.1.2"
|
||||||
|
},
|
||||||
|
"mf2util": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70b5e08dd553e19b1cb46cd6060ff6a41e57859588364efece6e52d14267c859"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"msgpack": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:102802a9433dcf36f939b632cce9dea87310b2f163bb37ffc8bc343677726e88",
|
||||||
|
"sha256:3055c44f39833b6edb27fd48028dc7822d1fd75bfeef8a2434caed8d62bb24ee",
|
||||||
|
"sha256:3b7fd45c8e9e537640f541d3699b1773cf5cb9345d4a75f93baa8f055084e59c",
|
||||||
|
"sha256:64abc6bf3a2ac301702f5760f4e6e227d0fd4d84d9014ef9a40faa9d43365259",
|
||||||
|
"sha256:6e962c4adc7970af5a3d6a4f9bb87c617b1bd041fd9ab42355a263d421017ed9",
|
||||||
|
"sha256:72259661a83f8b08ef6ee83927ce4937f841226735824af5b10a536d886eeb36",
|
||||||
|
"sha256:78e297c3996fd9f35090fbddd1c148c2a71e0d6024500bcf3af90a4b9698bc19",
|
||||||
|
"sha256:85f1342b9d7549dd3daf494100d47a3dc7daae703cdbfc2c9ee7bbdc8a492cba",
|
||||||
|
"sha256:8ce9f88b6cb75d74eda2a5522e5c2e5ec0f17fd78605d6502abb61f46b306865",
|
||||||
|
"sha256:8d0af8d64198e4b4f942a15ea9cb0dd9c4a0bd3e4e2ba57425e108bdbd4c3a0f",
|
||||||
|
"sha256:9936ce3a530ca78db60b6631003b5f4ba383cfb1d9830a27d1b5c61857226e2f",
|
||||||
|
"sha256:b688721df31c4bad6f508fb262719eb7e4a3532024c66d3c44ad6a4704519dda",
|
||||||
|
"sha256:c28478328e9cd868ce54e8465eae9fa3605790450c66cc7e8bc416526917ef6e",
|
||||||
|
"sha256:cb4e228f3d93779a1d77a1e9d72759b79dfa2975c1a5bd2a090eaa98239fa4b1",
|
||||||
|
"sha256:d03d0b6e4adf5bd1cbf7a81a20a56c883351947a57b7b85235181b057adf1120",
|
||||||
|
"sha256:d2b179faebd278e5f4e255a6bbc7ccb467f02ed5c4c00c8a68dc926002223a20",
|
||||||
|
"sha256:f1a8f7bd84be103979a73da57be3cb929d702a656162ee466597b816fa9eec97"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||||
|
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||||
|
],
|
||||||
|
"version": "==18.0"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e",
|
||||||
|
"sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7",
|
||||||
|
"sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a",
|
||||||
|
"sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3",
|
||||||
|
"sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1",
|
||||||
|
"sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1",
|
||||||
|
"sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7",
|
||||||
|
"sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1",
|
||||||
|
"sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3",
|
||||||
|
"sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055",
|
||||||
|
"sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf",
|
||||||
|
"sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f",
|
||||||
|
"sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f",
|
||||||
|
"sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239",
|
||||||
|
"sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe",
|
||||||
|
"sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c",
|
||||||
|
"sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697",
|
||||||
|
"sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494",
|
||||||
|
"sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356",
|
||||||
|
"sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6",
|
||||||
|
"sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000",
|
||||||
|
"sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f",
|
||||||
|
"sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c",
|
||||||
|
"sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca",
|
||||||
|
"sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8",
|
||||||
|
"sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3",
|
||||||
|
"sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad",
|
||||||
|
"sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9",
|
||||||
|
"sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc",
|
||||||
|
"sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.4.1"
|
||||||
|
},
|
||||||
|
"psycopg2-binary": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:036bcb198a7cc4ce0fe43344f8c2c9a8155aefa411633f426c8c6ed58a6c0426",
|
||||||
|
"sha256:1d770fcc02cdf628aebac7404d56b28a7e9ebec8cfc0e63260bd54d6edfa16d4",
|
||||||
|
"sha256:1fdc6f369dcf229de6c873522d54336af598b9470ccd5300e2f58ee506f5ca13",
|
||||||
|
"sha256:21f9ddc0ff6e07f7d7b6b484eb9da2c03bc9931dd13e36796b111d631f7135a3",
|
||||||
|
"sha256:247873cda726f7956f745a3e03158b00de79c4abea8776dc2f611d5ba368d72d",
|
||||||
|
"sha256:3aa31c42f29f1da6f4fd41433ad15052d5ff045f2214002e027a321f79d64e2c",
|
||||||
|
"sha256:475f694f87dbc619010b26de7d0fc575a4accf503f2200885cc21f526bffe2ad",
|
||||||
|
"sha256:4b5e332a24bf6e2fda1f51ca2a57ae1083352293a08eeea1fa1112dc7dd542d1",
|
||||||
|
"sha256:570d521660574aca40be7b4d532dfb6f156aad7b16b5ed62d1534f64f1ef72d8",
|
||||||
|
"sha256:59072de7def0690dd13112d2bdb453e20570a97297070f876fbbb7cbc1c26b05",
|
||||||
|
"sha256:5f0b658989e918ef187f8a08db0420528126f2c7da182a7b9f8bf7f85144d4e4",
|
||||||
|
"sha256:649199c84a966917d86cdc2046e03d536763576c0b2a756059ae0b3a9656bc20",
|
||||||
|
"sha256:6645fc9b4705ae8fbf1ef7674f416f89ae1559deec810f6dd15197dfa52893da",
|
||||||
|
"sha256:6872dd54d4e398d781efe8fe2e2d7eafe4450d61b5c4898aced7610109a6df75",
|
||||||
|
"sha256:6ce34fbc251fc0d691c8d131250ba6f42fd2b28ef28558d528ba8c558cb28804",
|
||||||
|
"sha256:73920d167a0a4d1006f5f3b9a3efce6f0e5e883a99599d38206d43f27697df00",
|
||||||
|
"sha256:8a671732b87ae423e34b51139628123bc0306c2cb85c226e71b28d3d57d7e42a",
|
||||||
|
"sha256:8d517e8fda2efebca27c2018e14c90ed7dc3f04d7098b3da2912e62a1a5585fe",
|
||||||
|
"sha256:9475a008eb7279e20d400c76471843c321b46acacc7ee3de0b47233a1e3fa2cf",
|
||||||
|
"sha256:96947b8cd7b3148fb0e6549fcb31258a736595d6f2a599f8cd450e9a80a14781",
|
||||||
|
"sha256:abf229f24daa93f67ac53e2e17c8798a71a01711eb9fcdd029abba8637164338",
|
||||||
|
"sha256:b1ab012f276df584beb74f81acb63905762c25803ece647016613c3d6ad4e432",
|
||||||
|
"sha256:b22b33f6f0071fe57cb4e9158f353c88d41e739a3ec0d76f7b704539e7076427",
|
||||||
|
"sha256:b3b2d53274858e50ad2ffdd6d97ce1d014e1e530f82ec8b307edd5d4c921badf",
|
||||||
|
"sha256:bab26a729befc7b9fab9ded1bba9c51b785188b79f8a2796ba03e7e734269e2e",
|
||||||
|
"sha256:daa1a593629aa49f506eddc9d23dc7f89b35693b90e1fbcd4480182d1203ea90",
|
||||||
|
"sha256:dd111280ce40e89fd17b19c1269fd1b74a30fce9d44a550840e86edb33924eb8",
|
||||||
|
"sha256:e0b86084f1e2e78c451994410de756deba206884d6bed68d5a3d7f39ff5fea1d",
|
||||||
|
"sha256:eb86520753560a7e89639500e2a254bb6f683342af598088cb72c73edcad21e6",
|
||||||
|
"sha256:ff18c5c40a38d41811c23e2480615425c97ea81fd7e9118b8b899c512d97c737"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.7.6.1"
|
||||||
|
},
|
||||||
|
"pycparser": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||||
|
],
|
||||||
|
"version": "==2.19"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
|
||||||
|
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
|
||||||
|
],
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"python-baseconv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1b98b11d0d1c00bf1165d62b0d183c8d2d496ae5baaa0991c0d4ffef079772d6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"python-magic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375",
|
||||||
|
"sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.15"
|
||||||
|
},
|
||||||
|
"python-slugify": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d3e034397236020498e677a35e5c05dcc6ba1624b608b9ef7e5fe3090ccbd5a8"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
||||||
|
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
||||||
|
],
|
||||||
|
"version": "==2018.9"
|
||||||
|
},
|
||||||
|
"pyup-django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f02242b4c7a8926bf9118054429dcaf84e5708a050548abcd3b2b9de4a7570b9",
|
||||||
|
"sha256:fe84cef39c41d5feb24e307d6c8a55454db50df4c6955fa6a890a42b6e58650e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.0"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||||
|
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||||
|
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||||
|
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||||
|
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||||
|
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||||
|
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||||
|
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||||
|
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||||
|
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||||
|
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.13"
|
||||||
|
},
|
||||||
|
"qrcode": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5",
|
||||||
|
"sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==6.1"
|
||||||
|
},
|
||||||
|
"rcssmin": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
|
||||||
|
],
|
||||||
|
"version": "==1.0.6"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2100750629beff143b6a200a2ea8e719fcf26420adabb81402895e144c5083cf",
|
||||||
|
"sha256:8e0bdd2de02e829b6225b25646f9fb9daffea99a252610d040409a6738541f0a"
|
||||||
|
],
|
||||||
|
"version": "==3.0.1"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||||
|
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||||
|
],
|
||||||
|
"version": "==2.21.0"
|
||||||
|
},
|
||||||
|
"rjsmin": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:dd9591aa73500b08b7db24367f8d32c6470021f39d5ab4e50c7c02e4401386f1"
|
||||||
|
],
|
||||||
|
"version": "==1.0.12"
|
||||||
|
},
|
||||||
|
"ronkyuu": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5aa77b39d301bc174ab99ba8a53954627771cb501651a12103c58f51b32e84bf",
|
||||||
|
"sha256:85b25fef7f5fb0c93afd5377ea35b5ff72b2458f926bafdf10f0c9a1e19cab10"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6"
|
||||||
|
},
|
||||||
|
"rq": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ef7de3fa26a4ce41dcd0561bdba12bb62fa5b9b6d21120122d8dbbe44bb6a77",
|
||||||
|
"sha256:c3c6bdf738652b8464e8b32c5e158a5e646ee553711d5a1e9c6c23ab17ef68b1"
|
||||||
|
],
|
||||||
|
"version": "==0.13.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"soupsieve": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:009d8865916766f7f452880d08ff94ed4c5445011a3deaac67543b82bdb0b9ee",
|
||||||
|
"sha256:97599c45a1ddfe9ab0a0cba889b7f214b3e310b703f176a0610c0b54e207cc04"
|
||||||
|
],
|
||||||
|
"version": "==1.7.1"
|
||||||
|
},
|
||||||
|
"sqlparse": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec",
|
||||||
|
"sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4"
|
||||||
|
],
|
||||||
|
"version": "==0.2.4"
|
||||||
|
},
|
||||||
|
"unidecode": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:092cdf7ad9d1052c50313426a625b717dab52f7ac58f859e09ea020953b1ad8f",
|
||||||
|
"sha256:8b85354be8fd0c0e10adbf0675f6dc2310e56fda43fa8fe049123b6c475e52fb"
|
||||||
|
],
|
||||||
|
"version": "==1.0.23"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||||
|
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||||
|
],
|
||||||
|
"version": "==1.24.1"
|
||||||
|
},
|
||||||
|
"webencodings": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||||
|
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||||
|
],
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"xrd": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:51d01f732b5b5b7983c5179ffaed864408d95a667b3a6630fe27aa7528274089"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"argh": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
|
||||||
|
"sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"
|
||||||
|
],
|
||||||
|
"version": "==0.26.2"
|
||||||
|
},
|
||||||
|
"atomicwrites": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||||
|
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||||
|
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||||
|
],
|
||||||
|
"version": "==18.2.0"
|
||||||
|
},
|
||||||
|
"docopt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
||||||
|
],
|
||||||
|
"version": "==0.6.2"
|
||||||
|
},
|
||||||
|
"jedi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd",
|
||||||
|
"sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"
|
||||||
|
],
|
||||||
|
"version": "==0.13.2"
|
||||||
|
},
|
||||||
|
"more-itertools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||||
|
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||||
|
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||||
|
],
|
||||||
|
"version": "==5.0.0"
|
||||||
|
},
|
||||||
|
"mypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e",
|
||||||
|
"sha256:cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.660"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
|
||||||
|
"sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
|
||||||
|
],
|
||||||
|
"version": "==0.4.1"
|
||||||
|
},
|
||||||
|
"parso": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
|
||||||
|
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
|
||||||
|
],
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"pathtools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"
|
||||||
|
],
|
||||||
|
"version": "==0.1.2"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||||
|
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||||
|
],
|
||||||
|
"version": "==0.8.1"
|
||||||
|
},
|
||||||
|
"prompt-toolkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34",
|
||||||
|
"sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9",
|
||||||
|
"sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39"
|
||||||
|
],
|
||||||
|
"version": "==2.0.7"
|
||||||
|
},
|
||||||
|
"ptpython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:51a74abe931f692360a32d650c2ba1ca329c08f3ed9b1de8abcd1164e0b0a6a7",
|
||||||
|
"sha256:938ee050e37d61c138dbbeb21383dfef8b9ed4ffb453a5f34041f42025bf5042",
|
||||||
|
"sha256:ebe9d68ea7532ec8ab306d4bdc7ec393701cd9bbd6eff0aa3067c821f99264d4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.4"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
|
||||||
|
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
|
||||||
|
],
|
||||||
|
"version": "==1.7.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
|
||||||
|
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
|
||||||
|
],
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2",
|
||||||
|
"sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be"
|
||||||
|
],
|
||||||
|
"version": "==4.1.1"
|
||||||
|
},
|
||||||
|
"pytest-django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a5d33be930e3172fa238643a380414dc369fe8fa4b3c3de25e59ed142950736",
|
||||||
|
"sha256:e88e471d3d0f9acfb6293bb03d0ee8a33ed978734e92ea6b5312163a6c9e87cc"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.4.5"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||||
|
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||||
|
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||||
|
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||||
|
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||||
|
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||||
|
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||||
|
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||||
|
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||||
|
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||||
|
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.13"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"typed-ast": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe",
|
||||||
|
"sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c",
|
||||||
|
"sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2",
|
||||||
|
"sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a",
|
||||||
|
"sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7",
|
||||||
|
"sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827",
|
||||||
|
"sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33",
|
||||||
|
"sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9",
|
||||||
|
"sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032",
|
||||||
|
"sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9",
|
||||||
|
"sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2",
|
||||||
|
"sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2",
|
||||||
|
"sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062",
|
||||||
|
"sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15",
|
||||||
|
"sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357",
|
||||||
|
"sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a",
|
||||||
|
"sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824",
|
||||||
|
"sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442",
|
||||||
|
"sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1",
|
||||||
|
"sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2",
|
||||||
|
"sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"
|
||||||
|
],
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"watchdog": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||||
|
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||||
|
],
|
||||||
|
"version": "==0.1.7"
|
||||||
|
},
|
||||||
|
"werkzeug": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
||||||
|
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.14.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'entries.apps.EntriesConfig'
|
|
@ -8,10 +8,12 @@ class SyndicationInline(admin.TabularInline):
|
||||||
|
|
||||||
|
|
||||||
class EntryAdmin(admin.ModelAdmin):
|
class EntryAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = 'created'
|
||||||
list_display = ("title", "id", "kind", "created")
|
list_display = ('title', 'id', 'kind', 'created')
|
||||||
list_filter = ("kind",)
|
list_filter = ('kind',)
|
||||||
inlines = (SyndicationInline,)
|
inlines = (
|
||||||
|
SyndicationInline,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Cat)
|
admin.site.register(Cat)
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class EntriesConfig(AppConfig):
|
class EntriesConfig(AppConfig):
|
||||||
name = "entries"
|
name = 'entries'
|
||||||
|
|
|
@ -11,24 +11,24 @@ from .models import Entry
|
||||||
def from_url(url: str) -> Entry:
|
def from_url(url: str) -> Entry:
|
||||||
domain = Site.objects.get_current().domain
|
domain = Site.objects.get_current().domain
|
||||||
if not url:
|
if not url:
|
||||||
raise error.bad_req("url parameter required")
|
raise error.bad_req('url parameter required')
|
||||||
if "//" not in url:
|
if '//' not in url:
|
||||||
url = "//" + url
|
url = '//' + url
|
||||||
parts = urlparse(url, scheme="https")
|
parts = urlparse(url, scheme='https')
|
||||||
if parts.scheme not in ("http", "https") or parts.netloc != domain:
|
if parts.scheme not in ('http', 'https') or parts.netloc != domain:
|
||||||
raise error.bad_req("url does not point to this site")
|
raise error.bad_req('url does not point to this site')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
match = resolve(parts.path)
|
match = resolve(parts.path)
|
||||||
except Resolver404:
|
except Resolver404:
|
||||||
raise error.bad_req("url does not point to a valid page on this site")
|
raise error.bad_req('url does not point to a valid page on this site')
|
||||||
|
|
||||||
if match.view_name != "entries:entry":
|
if match.view_name != 'entries:entry':
|
||||||
raise error.bad_req("url does not point to an entry on this site")
|
raise error.bad_req('url does not point to an entry on this site')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = Entry.objects.get(pk=match.kwargs["id"])
|
entry = Entry.objects.get(pk=match.kwargs['id'])
|
||||||
except Entry.DoesNotExist:
|
except Entry.DoesNotExist:
|
||||||
raise error.bad_req("url does not point to an existing entry")
|
raise error.bad_req('url does not point to an existing entry')
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends 'lemoncurry/layout.html' %}
|
{% extends 'lemoncurry/layout.html' %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="shortlink" href="{{ entry.short_url }}" />
|
<link rel="shortlink" href="{{ entry.short_url }}" />
|
||||||
|
<link rel="alternate" type="application/json+oembed" href="https://wirres.net/oembed/oembed/php?url={{ entry.absolute_url | urlencode }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<article class="h-entry media">
|
<article class="h-entry media">
|
||||||
{{i}}<aside class="info">
|
{{i}}<aside class="info">
|
||||||
{{i}}<a class="p-author h-card" href="{{ entry.author.url }}">
|
{{i}}<a class="p-author h-card" href="{{ entry.author.url }}">
|
||||||
{{i}}{% if entry.author.avatar %}<img class="u-photo img-fluid" src="{{ entry.author.avatar.url }}" alt="{{ entry.author.name }}" />{% endif %}
|
{{i}}<img class="u-photo img-fluid" src="{{ entry.author.avatar.url }}" alt="{{ entry.author.name }}" />
|
||||||
{{i}}<span class="p-name sr-only">{{ entry.author.name }}</span>
|
{{i}}<span class="p-name sr-only">{{ entry.author.name }}</span>
|
||||||
{{i}}</a>
|
{{i}}</a>
|
||||||
{{i}}<a class="u-uid u-url" href="{{ entry.url }}">
|
{{i}}<a class="u-uid u-url" href="{{ entry.url }}">
|
||||||
|
|
|
@ -7,19 +7,16 @@ from ronkyuu import webmention
|
||||||
@job
|
@job
|
||||||
def ping_hub(*urls):
|
def ping_hub(*urls):
|
||||||
for url in urls:
|
for url in urls:
|
||||||
requests.post(
|
requests.post(settings.PUSH_HUB, data={
|
||||||
settings.PUSH_HUB,
|
'hub.mode': 'publish',
|
||||||
data={
|
'hub.url': url,
|
||||||
"hub.mode": "publish",
|
})
|
||||||
"hub.url": url,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@job
|
@job
|
||||||
def send_mentions(source, targets=None):
|
def send_mentions(source, targets=None):
|
||||||
if targets is None:
|
if targets is None:
|
||||||
targets = webmention.findMentions(source)["refs"]
|
targets = webmention.findMentions(source)['refs']
|
||||||
for target in targets:
|
for target in targets:
|
||||||
status, endpoint = webmention.discoverEndpoint(target)
|
status, endpoint = webmention.discoverEndpoint(target)
|
||||||
if endpoint is not None and status == 200:
|
if endpoint is not None and status == 200:
|
||||||
|
|
|
@ -14,62 +14,62 @@ class Entry:
|
||||||
return self.index_page()
|
return self.index_page()
|
||||||
|
|
||||||
def index_page(self, page=0):
|
def index_page(self, page=0):
|
||||||
kwargs = {"kind": self}
|
kwargs = {'kind': self}
|
||||||
if page > 1:
|
if page > 1:
|
||||||
kwargs["page"] = page
|
kwargs['page'] = page
|
||||||
return reverse("entries:index", kwargs=kwargs)
|
return reverse('entries:index', kwargs=kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entry(self):
|
def entry(self):
|
||||||
return self.plural + "_entry"
|
return self.plural + '_entry'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def atom(self):
|
def atom(self):
|
||||||
return reverse("entries:atom_by_kind", kwargs={"kind": self})
|
return reverse('entries:atom_by_kind', kwargs={'kind': self})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rss(self):
|
def rss(self):
|
||||||
return reverse("entries:rss_by_kind", kwargs={"kind": self})
|
return reverse('entries:rss_by_kind', kwargs={'kind': self})
|
||||||
|
|
||||||
|
|
||||||
Note = Entry(
|
Note = Entry(
|
||||||
id="note",
|
id='note',
|
||||||
icon="fas fa-paper-plane",
|
icon='fas fa-paper-plane',
|
||||||
plural="notes",
|
plural='notes',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
Article = Entry(
|
Article = Entry(
|
||||||
id="article",
|
id='article',
|
||||||
icon="fas fa-file-alt",
|
icon='fas fa-file-alt',
|
||||||
plural="articles",
|
plural='articles',
|
||||||
slug=True,
|
slug=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
Photo = Entry(
|
Photo = Entry(
|
||||||
id="photo",
|
id='photo',
|
||||||
icon="fas fa-camera",
|
icon='fas fa-camera',
|
||||||
plural="photos",
|
plural='photos',
|
||||||
)
|
)
|
||||||
|
|
||||||
Reply = Entry(
|
Reply = Entry(
|
||||||
id="reply",
|
id='reply',
|
||||||
icon="fas fa-comment",
|
icon='fas fa-comment',
|
||||||
plural="replies",
|
plural='replies',
|
||||||
on_home=False,
|
on_home=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
Like = Entry(
|
Like = Entry(
|
||||||
id="like",
|
id='like',
|
||||||
icon="fas fa-heart",
|
icon='fas fa-heart',
|
||||||
plural="likes",
|
plural='likes',
|
||||||
on_home=False,
|
on_home=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
Repost = Entry(
|
Repost = Entry(
|
||||||
id="repost",
|
id='repost',
|
||||||
icon="fas fa-retweet",
|
icon='fas fa-retweet',
|
||||||
plural="reposts",
|
plural='reposts',
|
||||||
)
|
)
|
||||||
|
|
||||||
all = (Note, Article, Photo)
|
all = (Note, Article, Photo)
|
||||||
|
@ -79,7 +79,7 @@ from_plural = {k.plural: k for k in all}
|
||||||
|
|
||||||
|
|
||||||
class EntryKindConverter:
|
class EntryKindConverter:
|
||||||
regex = "|".join(k.plural for k in all)
|
regex = '|'.join(k.plural for k in all)
|
||||||
|
|
||||||
def to_python(self, plural):
|
def to_python(self, plural):
|
||||||
return from_plural[plural]
|
return from_plural[plural]
|
||||||
|
|
|
@ -8,6 +8,7 @@ import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -16,41 +17,20 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Entry",
|
name='Entry',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"id",
|
('kind', models.CharField(choices=[('note', 'Note'), ('article', 'Article')], default='note', max_length=30)),
|
||||||
models.AutoField(
|
('name', models.CharField(blank=True, max_length=100)),
|
||||||
auto_created=True,
|
('summary', models.TextField(blank=True)),
|
||||||
primary_key=True,
|
('content', models.TextField()),
|
||||||
serialize=False,
|
('published', models.DateTimeField()),
|
||||||
verbose_name="ID",
|
('updated', models.DateTimeField()),
|
||||||
),
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
),
|
|
||||||
(
|
|
||||||
"kind",
|
|
||||||
models.CharField(
|
|
||||||
choices=[("note", "Note"), ("article", "Article")],
|
|
||||||
default="note",
|
|
||||||
max_length=30,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(blank=True, max_length=100)),
|
|
||||||
("summary", models.TextField(blank=True)),
|
|
||||||
("content", models.TextField()),
|
|
||||||
("published", models.DateTimeField()),
|
|
||||||
("updated", models.DateTimeField()),
|
|
||||||
(
|
|
||||||
"author",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name_plural": "entries",
|
'verbose_name_plural': 'entries',
|
||||||
"ordering": ["-published"],
|
'ordering': ['-published'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,42 +7,23 @@ import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("users", "0005_auto_20171023_0158"),
|
('users', '0005_auto_20171023_0158'),
|
||||||
("entries", "0001_initial"),
|
('entries', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Syndication",
|
name='Syndication',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"id",
|
('url', models.CharField(max_length=255)),
|
||||||
models.AutoField(
|
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syndications', to='entries.Entry')),
|
||||||
auto_created=True,
|
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')),
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("url", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"entry",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="syndications",
|
|
||||||
to="entries.Entry",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"profile",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="users.Profile"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"ordering": ["profile"],
|
'ordering': ['profile'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,14 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0002_syndication"),
|
('entries', '0002_syndication'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="summary",
|
name='summary',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,28 +8,20 @@ import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0003_remove_entry_summary"),
|
('entries', '0003_remove_entry_summary'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="author",
|
name='author',
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL),
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="entries",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="kind",
|
name='kind',
|
||||||
field=models.CharField(
|
field=models.CharField(choices=[('note', 'note'), ('article', 'article')], db_index=True, default='note', max_length=30),
|
||||||
choices=[("note", "note"), ("article", "article")],
|
|
||||||
db_index=True,
|
|
||||||
default="note",
|
|
||||||
max_length=30,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,24 +6,20 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0004_auto_20171027_0846"),
|
('entries', '0004_auto_20171027_0846'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="photo",
|
name='photo',
|
||||||
field=models.ImageField(blank=True, upload_to=""),
|
field=models.ImageField(blank=True, upload_to=''),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="kind",
|
name='kind',
|
||||||
field=models.CharField(
|
field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo')], db_index=True, default='note', max_length=30),
|
||||||
choices=[("note", "note"), ("article", "article"), ("photo", "photo")],
|
|
||||||
db_index=True,
|
|
||||||
default="note",
|
|
||||||
max_length=30,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,41 +8,34 @@ import model_utils.fields
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0005_auto_20171027_1557"),
|
('entries', '0005_auto_20171027_1557'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name="entry",
|
name='entry',
|
||||||
options={"ordering": ["-created"], "verbose_name_plural": "entries"},
|
options={'ordering': ['-created'], 'verbose_name_plural': 'entries'},
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
old_name="published",
|
old_name='published',
|
||||||
new_name="created",
|
new_name='created',
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
old_name="updated",
|
old_name='updated',
|
||||||
new_name="modified",
|
new_name='modified',
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="created",
|
name='created',
|
||||||
field=model_utils.fields.AutoCreatedField(
|
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
|
||||||
default=django.utils.timezone.now,
|
|
||||||
editable=False,
|
|
||||||
verbose_name="created",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="modified",
|
name='modified',
|
||||||
field=model_utils.fields.AutoLastModifiedField(
|
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
|
||||||
default=django.utils.timezone.now,
|
|
||||||
editable=False,
|
|
||||||
verbose_name="modified",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,31 +6,20 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0006_auto_20171102_1200"),
|
('entries', '0006_auto_20171102_1200'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="cite",
|
name='cite',
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="kind",
|
name='kind',
|
||||||
field=models.CharField(
|
field=models.CharField(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo'), ('reply', 'reply'), ('like', 'like'), ('repost', 'repost')], db_index=True, default='note', max_length=30),
|
||||||
choices=[
|
|
||||||
("note", "note"),
|
|
||||||
("article", "article"),
|
|
||||||
("photo", "photo"),
|
|
||||||
("reply", "reply"),
|
|
||||||
("like", "like"),
|
|
||||||
("repost", "repost"),
|
|
||||||
],
|
|
||||||
db_index=True,
|
|
||||||
default="note",
|
|
||||||
max_length=30,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,24 +6,25 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0007_auto_20171113_0841"),
|
('entries', '0007_auto_20171113_0841'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
old_name="cite",
|
old_name='cite',
|
||||||
new_name="in_reply_to",
|
new_name='in_reply_to',
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="like_of",
|
name='like_of',
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="repost_of",
|
name='repost_of',
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,28 +6,21 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0008_auto_20171116_2116"),
|
('entries', '0008_auto_20171116_2116'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Tag",
|
name='Tag',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"id",
|
('name', models.CharField(max_length=255, unique=True)),
|
||||||
models.AutoField(
|
('slug', models.CharField(max_length=255, unique=True)),
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=255, unique=True)),
|
|
||||||
("slug", models.CharField(max_length=255, unique=True)),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"ordering": ("name",),
|
'ordering': ('name',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,14 +6,15 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0009_tag"),
|
('entries', '0009_tag'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
name="tags",
|
name='tags',
|
||||||
field=models.ManyToManyField(related_name="entries", to="entries.Tag"),
|
field=models.ManyToManyField(related_name='entries', to='entries.Tag'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,17 +9,17 @@ class Migration(migrations.Migration):
|
||||||
atomic = False
|
atomic = False
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0010_entry_tags"),
|
('entries', '0010_entry_tags'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameModel(
|
migrations.RenameModel(
|
||||||
old_name="Tag",
|
old_name='Tag',
|
||||||
new_name="Cat",
|
new_name='Cat',
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name="entry",
|
model_name='entry',
|
||||||
old_name="tags",
|
old_name='tags',
|
||||||
new_name="cats",
|
new_name='cats',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,25 +5,25 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("entries", "0011_auto_20171120_1108"),
|
('entries', '0011_auto_20171120_1108'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name="syndication",
|
name='syndication',
|
||||||
options={"ordering": ["domain"]},
|
options={'ordering': ['domain']},
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name="syndication",
|
model_name='syndication',
|
||||||
name="profile",
|
name='profile',
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="syndication",
|
model_name='syndication',
|
||||||
name="domain",
|
name='domain',
|
||||||
field=computed_property.fields.ComputedCharField(
|
field=computed_property.fields.ComputedCharField(
|
||||||
compute_from="calc_domain", default="", editable=False, max_length=255
|
compute_from='calc_domain', default='', editable=False, max_length=255),
|
||||||
),
|
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-03-12 04:24
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("entries", "0012_auto_20180628_2044"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="entry",
|
|
||||||
name="kind",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[("note", "note"), ("article", "article"), ("photo", "photo")],
|
|
||||||
db_index=True,
|
|
||||||
default="note",
|
|
||||||
max_length=30,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -17,7 +17,6 @@ from users.models import Site
|
||||||
|
|
||||||
from . import kinds
|
from . import kinds
|
||||||
from lemoncurry import requests, utils
|
from lemoncurry import requests, utils
|
||||||
|
|
||||||
ENTRY_KINDS = [(k.id, k.id) for k in kinds.all]
|
ENTRY_KINDS = [(k.id, k.id) for k in kinds.all]
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,33 +32,38 @@ class Cat(models.Model):
|
||||||
slug = models.CharField(max_length=255, unique=True)
|
slug = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "#" + self.name
|
return '#' + self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return reverse("entries:cat", args=(self.slug,))
|
return reverse('entries:cat', args=(self.slug,))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ('name',)
|
||||||
|
|
||||||
|
|
||||||
class EntryManager(models.Manager):
|
class EntryManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super(EntryManager, self).get_queryset()
|
qs = super(EntryManager, self).get_queryset()
|
||||||
return qs.select_related("author").prefetch_related("cats", "syndications")
|
return (qs
|
||||||
|
.select_related('author')
|
||||||
|
.prefetch_related('cats', 'syndications'))
|
||||||
|
|
||||||
|
|
||||||
class Entry(ModelMeta, TimeStampedModel):
|
class Entry(ModelMeta, TimeStampedModel):
|
||||||
objects = EntryManager()
|
objects = EntryManager()
|
||||||
kind = models.CharField(
|
kind = models.CharField(
|
||||||
max_length=30, choices=ENTRY_KINDS, db_index=True, default=ENTRY_KINDS[0][0]
|
max_length=30,
|
||||||
|
choices=ENTRY_KINDS,
|
||||||
|
db_index=True,
|
||||||
|
default=ENTRY_KINDS[0][0]
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=100, blank=True)
|
name = models.CharField(max_length=100, blank=True)
|
||||||
photo = models.ImageField(blank=True)
|
photo = models.ImageField(blank=True)
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
|
|
||||||
cats = models.ManyToManyField(Cat, related_name="entries")
|
cats = models.ManyToManyField(Cat, related_name='entries')
|
||||||
|
|
||||||
in_reply_to = models.CharField(max_length=255, blank=True)
|
in_reply_to = models.CharField(max_length=255, blank=True)
|
||||||
like_of = models.CharField(max_length=255, blank=True)
|
like_of = models.CharField(max_length=255, blank=True)
|
||||||
|
@ -67,7 +71,7 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
get_user_model(),
|
get_user_model(),
|
||||||
related_name="entries",
|
related_name='entries',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,7 +79,10 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
def reply_context(self):
|
def reply_context(self):
|
||||||
if not self.in_reply_to:
|
if not self.in_reply_to:
|
||||||
return None
|
return None
|
||||||
return interpret(requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to)
|
return interpret(
|
||||||
|
requests.mf2(self.in_reply_to).to_dict(),
|
||||||
|
self.in_reply_to
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def published(self):
|
def published(self):
|
||||||
|
@ -86,29 +93,35 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
return self.modified
|
return self.modified
|
||||||
|
|
||||||
_metadata = {
|
_metadata = {
|
||||||
"description": "excerpt",
|
'description': 'excerpt',
|
||||||
"image": "image_url",
|
'image': 'image_url',
|
||||||
"twitter_creator": "twitter_creator",
|
'twitter_creator': 'twitter_creator',
|
||||||
"og_profile_id": "og_profile_id",
|
'og_profile_id': 'og_profile_id',
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
if self.name:
|
if self.name:
|
||||||
return self.name
|
return self.name
|
||||||
return shorten(utils.to_plain(self.paragraphs[0]), width=100, placeholder="…")
|
return shorten(
|
||||||
|
utils.to_plain(self.paragraphs[0]),
|
||||||
|
width=100,
|
||||||
|
placeholder='…'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def excerpt(self):
|
def excerpt(self):
|
||||||
try:
|
try:
|
||||||
return utils.to_plain(self.paragraphs[0 if self.name else 1])
|
return utils.to_plain(self.paragraphs[0 if self.name else 1])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return " "
|
return ' '
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def paragraphs(self):
|
def paragraphs(self):
|
||||||
lines = self.content.splitlines()
|
lines = self.content.splitlines()
|
||||||
return ["\n".join(para) for k, para in groupby(lines, key=bool) if k]
|
return [
|
||||||
|
"\n".join(para) for k, para in groupby(lines, key=bool) if k
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def twitter_creator(self):
|
def twitter_creator(self):
|
||||||
|
@ -123,31 +136,31 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
return self.photo.url if self.photo else self.author.avatar_url
|
return self.photo.url if self.photo else self.author.avatar_url
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} {1}: {2}".format(self.kind, self.id, self.title)
|
return '{0} {1}: {2}'.format(self.kind, self.id, self.title)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.absolute_url
|
return self.absolute_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def absolute_url(self):
|
def absolute_url(self):
|
||||||
base = "https://" + DjangoSite.objects.get_current().domain
|
base = 'https://' + DjangoSite.objects.get_current().domain
|
||||||
return urljoin(base, self.url)
|
return urljoin(base, self.url)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def affected_urls(self):
|
def affected_urls(self):
|
||||||
base = "https://" + DjangoSite.objects.get_current().domain
|
base = 'https://' + DjangoSite.objects.get_current().domain
|
||||||
kind = kinds.from_id[self.kind]
|
kind = kinds.from_id[self.kind]
|
||||||
urls = {
|
urls = {
|
||||||
self.url,
|
self.url,
|
||||||
reverse("entries:index", kwargs={"kind": kind}),
|
reverse('entries:index', kwargs={'kind': kind}),
|
||||||
reverse("entries:atom_by_kind", kwargs={"kind": kind}),
|
reverse('entries:atom_by_kind', kwargs={'kind': kind}),
|
||||||
reverse("entries:rss_by_kind", kwargs={"kind": kind}),
|
reverse('entries:rss_by_kind', kwargs={'kind': kind}),
|
||||||
} | {cat.url for cat in self.cats.all()}
|
} | {cat.url for cat in self.cats.all()}
|
||||||
if kind.on_home:
|
if kind.on_home:
|
||||||
urls |= {
|
urls |= {
|
||||||
reverse("home:index"),
|
reverse('home:index'),
|
||||||
reverse("entries:atom"),
|
reverse('entries:atom'),
|
||||||
reverse("entries:rss"),
|
reverse('entries:rss')
|
||||||
}
|
}
|
||||||
return {urljoin(base, u) for u in urls}
|
return {urljoin(base, u) for u in urls}
|
||||||
|
|
||||||
|
@ -157,7 +170,7 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
args = [kind, self.id]
|
args = [kind, self.id]
|
||||||
if kind.slug:
|
if kind.slug:
|
||||||
args.append(self.slug)
|
args.append(self.slug)
|
||||||
return reverse("entries:entry", args=args)
|
return reverse('entries:entry', args=args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_url(self):
|
def short_url(self):
|
||||||
|
@ -169,48 +182,49 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json_ld(self):
|
def json_ld(self):
|
||||||
base = "https://" + DjangoSite.objects.get_current().domain
|
base = 'https://' + DjangoSite.objects.get_current().domain
|
||||||
url = urljoin(base, self.url)
|
url = urljoin(base, self.url)
|
||||||
|
|
||||||
posting = {
|
posting = {
|
||||||
"@context": "http://schema.org",
|
'@context': 'http://schema.org',
|
||||||
"@type": "BlogPosting",
|
'@type': 'BlogPosting',
|
||||||
"@id": url,
|
'@id': url,
|
||||||
"url": url,
|
'url': url,
|
||||||
"mainEntityOfPage": url,
|
'mainEntityOfPage': url,
|
||||||
"author": {
|
'author': {
|
||||||
"@type": "Person",
|
'@type': 'Person',
|
||||||
"url": urljoin(base, self.author.url),
|
'url': urljoin(base, self.author.url),
|
||||||
"name": self.author.name,
|
'name': self.author.name,
|
||||||
},
|
},
|
||||||
"headline": self.title,
|
'headline': self.title,
|
||||||
"description": self.excerpt,
|
'description': self.excerpt,
|
||||||
"datePublished": self.created.isoformat(),
|
'datePublished': self.created.isoformat(),
|
||||||
"dateModified": self.modified.isoformat(),
|
'dateModified': self.modified.isoformat(),
|
||||||
}
|
}
|
||||||
if self.photo:
|
if self.photo:
|
||||||
posting["image"] = (urljoin(base, self.photo.url),)
|
posting['image'] = (urljoin(base, self.photo.url), )
|
||||||
return posting
|
return posting
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "entries"
|
verbose_name_plural = 'entries'
|
||||||
ordering = ["-created"]
|
ordering = ['-created']
|
||||||
|
|
||||||
|
|
||||||
class Syndication(models.Model):
|
class Syndication(models.Model):
|
||||||
entry = models.ForeignKey(
|
entry = models.ForeignKey(
|
||||||
Entry, related_name="syndications", on_delete=models.CASCADE
|
Entry,
|
||||||
|
related_name='syndications',
|
||||||
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
url = models.CharField(max_length=255)
|
url = models.CharField(max_length=255)
|
||||||
|
|
||||||
domain = ComputedCharField(
|
domain = ComputedCharField(
|
||||||
compute_from="calc_domain",
|
compute_from='calc_domain', max_length=255,
|
||||||
max_length=255,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def calc_domain(self):
|
def calc_domain(self):
|
||||||
domain = urlparse(self.url).netloc
|
domain = urlparse(self.url).netloc
|
||||||
if domain.startswith("www."):
|
if domain.startswith('www.'):
|
||||||
domain = domain[4:]
|
domain = domain[4:]
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
|
@ -220,7 +234,7 @@ class Syndication(models.Model):
|
||||||
try:
|
try:
|
||||||
return Site.objects.get(domain=d)
|
return Site.objects.get(domain=d)
|
||||||
except Site.DoesNotExist:
|
except Site.DoesNotExist:
|
||||||
return Site(name=d, domain=d, icon="fas fa-newspaper")
|
return Site(name=d, domain=d, icon='fas fa-newspaper')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["domain"]
|
ordering = ['domain']
|
||||||
|
|
|
@ -1,32 +1,35 @@
|
||||||
from typing import Callable
|
from django.core.paginator import Paginator
|
||||||
|
|
||||||
from django.core.paginator import Page, Paginator
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from lemoncurry.middleware import ResponseException
|
from lemoncurry.middleware import ResponseException
|
||||||
|
|
||||||
|
|
||||||
def paginate(queryset, reverse: Callable[[int], str], page: int | None) -> Page:
|
def paginate(queryset, reverse, page):
|
||||||
def redirect_to_page(i: int):
|
class Page:
|
||||||
raise ResponseException(redirect(reverse(i)))
|
def __init__(self, i):
|
||||||
|
self.i = i
|
||||||
|
|
||||||
def reversible(p: Page) -> Page:
|
@property
|
||||||
p.reverse = reverse
|
def url(self):
|
||||||
return p
|
return reverse(self.i)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
return self.i == entries.number
|
||||||
|
|
||||||
|
# If the first page was requested, redirect to the clean version of the URL
|
||||||
|
# with no page suffix.
|
||||||
|
if page == 1:
|
||||||
|
raise ResponseException(redirect(Page(1).url))
|
||||||
|
|
||||||
paginator = Paginator(queryset, 10)
|
paginator = Paginator(queryset, 10)
|
||||||
|
entries = paginator.page(page or 1)
|
||||||
|
|
||||||
# If no page number was specified, return page one.
|
entries.pages = tuple(Page(i) for i in paginator.page_range)
|
||||||
if page is None:
|
|
||||||
return reversible(paginator.page(1))
|
|
||||||
|
|
||||||
# If the first page was explicitly requested, or the page number was negative, redirect to page one with no URL suffix.
|
if entries.has_previous():
|
||||||
if page <= 1:
|
entries.prev = Page(entries.previous_page_number())
|
||||||
redirect_to_page(1)
|
if entries.has_next():
|
||||||
|
entries.next = Page(entries.next_page_number())
|
||||||
|
|
||||||
# If the page requested is larger than the last page, then redirect to the last page.
|
return entries
|
||||||
if page > paginator.num_pages:
|
|
||||||
redirect_to_page(paginator.num_pages)
|
|
||||||
|
|
||||||
# Just return the current page! Hooray!
|
|
||||||
return reversible(paginator.page(page))
|
|
||||||
|
|
|
@ -3,27 +3,27 @@ import pytest
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_atom(client):
|
def test_atom(client):
|
||||||
res = client.get("/atom")
|
res = client.get('/atom')
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
assert res["Content-Type"] == "application/atom+xml; charset=utf-8"
|
assert res['Content-Type'] == 'application/atom+xml; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rss(client):
|
def test_rss(client):
|
||||||
res = client.get("/rss")
|
res = client.get('/rss')
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
assert res["Content-Type"] == "application/rss+xml; charset=utf-8"
|
assert res['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_atom_by_kind(client):
|
def test_atom_by_kind(client):
|
||||||
res = client.get("/notes/atom")
|
res = client.get('/notes/atom')
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
assert res["Content-Type"] == "application/atom+xml; charset=utf-8"
|
assert res['Content-Type'] == 'application/atom+xml; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rss_by_kind(client):
|
def test_rss_by_kind(client):
|
||||||
res = client.get("/notes/rss")
|
res = client.get('/notes/rss')
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
assert res["Content-Type"] == "application/rss+xml; charset=utf-8"
|
assert res['Content-Type'] == 'application/rss+xml; charset=utf-8'
|
||||||
|
|
|
@ -3,46 +3,47 @@ from . import kinds
|
||||||
from .views import feeds, lists, perma
|
from .views import feeds, lists, perma
|
||||||
from lemoncurry import breadcrumbs as crumbs
|
from lemoncurry import breadcrumbs as crumbs
|
||||||
|
|
||||||
register_converter(kinds.EntryKindConverter, "kind")
|
register_converter(kinds.EntryKindConverter, 'kind')
|
||||||
|
|
||||||
|
|
||||||
def to_pat(*args):
|
def to_pat(*args):
|
||||||
return "^{0}$".format("".join(args))
|
return '^{0}$'.format(''.join(args))
|
||||||
|
|
||||||
|
|
||||||
def prefix(route):
|
def prefix(route):
|
||||||
return app_name + ":" + route
|
return app_name + ':' + route
|
||||||
|
|
||||||
|
|
||||||
id = r"/(?P<id>\d+)"
|
id = r'/(?P<id>\d+)'
|
||||||
kind = r"(?P<kind>{0})".format("|".join(k.plural for k in kinds.all))
|
kind = r'(?P<kind>{0})'.format('|'.join(k.plural for k in kinds.all))
|
||||||
page = r"(?:/page/(?P<page>\d+))?"
|
page = r'(?:/page/(?P<page>\d+))?'
|
||||||
slug = r"/(?P<slug>[^/]+)"
|
slug = r'/(?P<slug>[^/]+)'
|
||||||
|
|
||||||
slug_opt = "(?:" + slug + ")?"
|
slug_opt = '(?:' + slug + ')?'
|
||||||
|
|
||||||
app_name = "entries"
|
app_name = 'entries'
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
path("atom", feeds.AtomHomeEntries(), name="atom"),
|
path('atom', feeds.AtomHomeEntries(), name='atom'),
|
||||||
path("rss", feeds.RssHomeEntries(), name="rss"),
|
path('rss', feeds.RssHomeEntries(), name='rss'),
|
||||||
path("cats/<slug:slug>", lists.by_cat, name="cat"),
|
path('cats/<slug:slug>', lists.by_cat, name='cat'),
|
||||||
path("cats/<slug:slug>/page/<int:page>", lists.by_cat, name="cat"),
|
path('cats/<slug:slug>/page/<int:page>', lists.by_cat, name='cat'),
|
||||||
path("<kind:kind>", lists.by_kind, name="index"),
|
path('<kind:kind>', lists.by_kind, name='index'),
|
||||||
path("<kind:kind>/page/<int:page>", lists.by_kind, name="index"),
|
path('<kind:kind>/page/<int:page>', lists.by_kind, name='index'),
|
||||||
path("<kind:kind>/atom", feeds.AtomByKind(), name="atom_by_kind"),
|
path('<kind:kind>/atom', feeds.AtomByKind(), name='atom_by_kind'),
|
||||||
path("<kind:kind>/rss", feeds.RssByKind(), name="rss_by_kind"),
|
path('<kind:kind>/rss', feeds.RssByKind(), name='rss_by_kind'),
|
||||||
path("<kind:kind>/<int:id>", perma.entry, name="entry"),
|
|
||||||
path("<kind:kind>/<int:id>/<slug:slug>", perma.entry, name="entry"),
|
path('<kind:kind>/<int:id>', perma.entry, name='entry'),
|
||||||
|
path('<kind:kind>/<int:id>/<slug:slug>', perma.entry, name='entry'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IndexCrumb(crumbs.Crumb):
|
class IndexCrumb(crumbs.Crumb):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(prefix("index"), parent="home:index")
|
super().__init__(prefix('index'), parent='home:index')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self):
|
def kind(self):
|
||||||
return self.match.kwargs["kind"]
|
return self.match.kwargs['kind']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def label(self):
|
def label(self):
|
||||||
|
@ -50,9 +51,9 @@ class IndexCrumb(crumbs.Crumb):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return reverse(prefix("index"), kwargs={"kind": self.kind})
|
return reverse(prefix('index'), kwargs={'kind': self.kind})
|
||||||
|
|
||||||
|
|
||||||
crumbs.add(prefix("cat"), parent="home:index")
|
crumbs.add(prefix('cat'), parent='home:index')
|
||||||
crumbs.add(IndexCrumb())
|
crumbs.add(IndexCrumb())
|
||||||
crumbs.add(prefix("entry"), parent=prefix("index"))
|
crumbs.add(prefix('entry'), parent=prefix('index'))
|
||||||
|
|
|
@ -11,8 +11,8 @@ from ..models import Entry
|
||||||
class Atom1FeedWithHub(Atom1Feed):
|
class Atom1FeedWithHub(Atom1Feed):
|
||||||
def add_root_elements(self, handler):
|
def add_root_elements(self, handler):
|
||||||
super().add_root_elements(handler)
|
super().add_root_elements(handler)
|
||||||
handler.startElement("link", {"rel": "hub", "href": settings.PUSH_HUB})
|
handler.startElement('link', {'rel': 'hub', 'href': settings.PUSH_HUB})
|
||||||
handler.endElement("link")
|
handler.endElement('link')
|
||||||
|
|
||||||
|
|
||||||
class EntriesFeed(Feed):
|
class EntriesFeed(Feed):
|
||||||
|
@ -79,7 +79,7 @@ class RssHomeEntries(EntriesFeed):
|
||||||
return Site.objects.get_current().name
|
return Site.objects.get_current().name
|
||||||
|
|
||||||
def link(self):
|
def link(self):
|
||||||
return reverse("home:index")
|
return reverse('home:index')
|
||||||
|
|
||||||
def description(self):
|
def description(self):
|
||||||
return "content from {0}".format(
|
return "content from {0}".format(
|
||||||
|
|
|
@ -5,32 +5,32 @@ from ..models import Entry, Cat
|
||||||
from ..pagination import paginate
|
from ..pagination import paginate
|
||||||
|
|
||||||
|
|
||||||
@render_to("entries/index.html")
|
@render_to('entries/index.html')
|
||||||
def by_kind(request, kind, page=None):
|
def by_kind(request, kind, page=None):
|
||||||
entries = Entry.objects.filter(kind=kind.id)
|
entries = Entry.objects.filter(kind=kind.id)
|
||||||
entries = paginate(queryset=entries, reverse=kind.index_page, page=page)
|
entries = paginate(queryset=entries, reverse=kind.index_page, page=page)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entries": entries,
|
'entries': entries,
|
||||||
"atom": kind.atom,
|
'atom': kind.atom,
|
||||||
"rss": kind.rss,
|
'rss': kind.rss,
|
||||||
"title": kind.plural,
|
'title': kind.plural,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@render_to("entries/index.html")
|
@render_to('entries/index.html')
|
||||||
def by_cat(request, slug, page=None):
|
def by_cat(request, slug, page=None):
|
||||||
def url(page):
|
def url(page):
|
||||||
kwargs = {"slug": slug}
|
kwargs = {'slug': slug}
|
||||||
if page > 1:
|
if page > 1:
|
||||||
kwargs["page"] = page
|
kwargs['page'] = page
|
||||||
return reverse("entries:cat", kwargs=kwargs)
|
return reverse('entries:cat', kwargs=kwargs)
|
||||||
|
|
||||||
cat = get_object_or_404(Cat, slug=slug)
|
cat = get_object_or_404(Cat, slug=slug)
|
||||||
entries = cat.entries.all()
|
entries = cat.entries.all()
|
||||||
entries = paginate(queryset=entries, reverse=url, page=page)
|
entries = paginate(queryset=entries, reverse=url, page=page)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entries": entries,
|
'entries': entries,
|
||||||
"title": "#" + cat.name,
|
'title': '#' + cat.name,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ from django.shortcuts import redirect, get_object_or_404
|
||||||
from ..models import Entry
|
from ..models import Entry
|
||||||
|
|
||||||
|
|
||||||
@render_to("entries/entry.html")
|
@render_to('entries/entry.html')
|
||||||
def entry(request, kind, id, slug=None):
|
def entry(request, kind, id, slug=None):
|
||||||
entry = get_object_or_404(Entry, pk=id)
|
entry = get_object_or_404(Entry, pk=id)
|
||||||
if request.path != entry.url:
|
if request.path != entry.url:
|
||||||
return redirect(entry.url, permanent=True)
|
return redirect(entry.url, permanent=True)
|
||||||
return {
|
return {
|
||||||
"entry": entry,
|
'entry': entry,
|
||||||
"title": entry.title,
|
'title': entry.title,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
proc_name = "lemoncurry"
|
proc_name = 'lemoncurry'
|
||||||
worker_class = "gevent"
|
worker_class = 'gevent'
|
||||||
workers = multiprocessing.cpu_count() * 2 + 1
|
workers = multiprocessing.cpu_count() * 2 + 1
|
||||||
|
|
|
@ -3,10 +3,10 @@ from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class HomeSitemap(sitemaps.Sitemap):
|
class HomeSitemap(sitemaps.Sitemap):
|
||||||
changefreq = "daily"
|
changefreq = 'daily'
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return ("home:index",)
|
return ('home:index',)
|
||||||
|
|
||||||
def location(self, item):
|
def location(self, item):
|
||||||
return reverse(item)
|
return reverse(item)
|
||||||
|
|
|
@ -2,9 +2,9 @@ from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = "home"
|
app_name = 'home'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path('', views.index, name='index'),
|
||||||
path("page/<int:page>", views.index, name="index"),
|
path('page/<int:page>', views.index, name='index'),
|
||||||
path("robots.txt", views.robots, name="robots.txt"),
|
path('robots.txt', views.robots, name='robots.txt'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,31 +8,34 @@ from urllib.parse import urljoin
|
||||||
from entries import kinds, pagination
|
from entries import kinds, pagination
|
||||||
from lemoncurry import breadcrumbs, utils
|
from lemoncurry import breadcrumbs, utils
|
||||||
|
|
||||||
breadcrumbs.add("home:index", "home")
|
breadcrumbs.add('home:index', 'home')
|
||||||
|
|
||||||
|
|
||||||
@render_to("home/index.html")
|
@render_to('home/index.html')
|
||||||
def index(request, page=None):
|
def index(request, page=None):
|
||||||
def url(page):
|
def url(page):
|
||||||
kwargs = {"page": page} if page != 1 else {}
|
kwargs = {'page': page} if page != 1 else {}
|
||||||
return reverse("home:index", kwargs=kwargs)
|
return reverse('home:index', kwargs=kwargs)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not hasattr(user, "entries"):
|
if not hasattr(user, 'entries'):
|
||||||
user = get_object_or_404(User, pk=1)
|
user = get_object_or_404(User, pk=1)
|
||||||
|
|
||||||
entries = user.entries.filter(kind__in=kinds.on_home)
|
entries = user.entries.filter(kind__in=kinds.on_home)
|
||||||
entries = pagination.paginate(queryset=entries, reverse=url, page=page)
|
entries = pagination.paginate(queryset=entries, reverse=url, page=page)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user": user,
|
'user': user,
|
||||||
"entries": entries,
|
'entries': entries,
|
||||||
"atom": reverse("entries:atom"),
|
'atom': reverse('entries:atom'),
|
||||||
"rss": reverse("entries:rss"),
|
'rss': reverse('entries:rss'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def robots(request):
|
def robots(request):
|
||||||
base = utils.origin(request)
|
base = utils.origin(request)
|
||||||
lines = ("User-agent: *", "Sitemap: {0}".format(urljoin(base, reverse("sitemap"))))
|
lines = (
|
||||||
return HttpResponse("\n".join(lines) + "\n", content_type="text/plain")
|
'User-agent: *',
|
||||||
|
'Sitemap: {0}'.format(urljoin(base, reverse('sitemap')))
|
||||||
|
)
|
||||||
|
return HttpResponse("\n".join(lines) + "\n", content_type='text/plain')
|
||||||
|
|
|
@ -62,19 +62,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="verified-success" hidden>
|
<div id="verified-success" hidden>
|
||||||
this client has been <strong>verified</strong> using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- they are who they claim to be!
|
this client has been <strong>verified</strong> using <code>{{ '<link rel="redirect_uri">' | escape }}</code> - they are who they claim to be!
|
||||||
</div>
|
</div>
|
||||||
<div id="verified-warning" hidden>
|
<div id="verified-warning" hidden>
|
||||||
this client could <strong>not</strong> be verified using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- check the redirect uri carefully yourself!
|
this client could <strong>not</strong> be verified using <code>{{ '<link rel="redirect_uri">' | escape }}</code> - check the redirect uri carefully yourself!
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block foot %}
|
{% block foot %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
tippy('[data-tippy-html]', {
|
tippy('[data-tippy-theme]', {
|
||||||
arrow: true,
|
arrow: true,
|
||||||
allowHTML: true,
|
|
||||||
maxWidth: 500,
|
|
||||||
content: function(element) {
|
content: function(element) {
|
||||||
return document.querySelector(element.getAttribute('data-tippy-html')).innerHTML;
|
return document.querySelector(element.getAttribute('data-tippy-html')).innerHTML;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,36 +7,25 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [] # type: List[Tuple[str, str]]
|
dependencies = [
|
||||||
|
] # type: List[Tuple[str, str]]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="IndieAuthCode",
|
name='IndieAuthCode',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True,
|
||||||
"id",
|
primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
models.AutoField(
|
('code', models.CharField(max_length=64, unique=True)),
|
||||||
auto_created=True,
|
('me', models.CharField(max_length=255)),
|
||||||
primary_key=True,
|
('client_id', models.CharField(max_length=255)),
|
||||||
serialize=False,
|
('redirect_uri', models.CharField(max_length=255)),
|
||||||
verbose_name="ID",
|
('response_type', models.CharField(choices=[
|
||||||
),
|
('id', 'id'), ('code', 'code')], default='id', max_length=4)),
|
||||||
),
|
('scope', models.CharField(blank=True, max_length=200)),
|
||||||
("code", models.CharField(max_length=64, unique=True)),
|
|
||||||
("me", models.CharField(max_length=255)),
|
|
||||||
("client_id", models.CharField(max_length=255)),
|
|
||||||
("redirect_uri", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"response_type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[("id", "id"), ("code", "code")],
|
|
||||||
default="id",
|
|
||||||
max_length=4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("scope", models.CharField(blank=True, max_length=200)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,12 +6,13 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("lemonauth", "0001_initial"),
|
('lemonauth', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name="IndieAuthCode",
|
name='IndieAuthCode',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,112 +9,43 @@ import randomslugfield.fields
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
("lemonauth", "0002_delete_indieauthcode"),
|
('lemonauth', '0002_delete_indieauthcode'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="IndieAuthCode",
|
name='IndieAuthCode',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
"created",
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
model_utils.fields.AutoCreatedField(
|
('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)),
|
||||||
default=django.utils.timezone.now,
|
('client_id', models.URLField()),
|
||||||
editable=False,
|
('scope', models.TextField(blank=True)),
|
||||||
verbose_name="created",
|
('redirect_uri', models.URLField()),
|
||||||
),
|
('response_type', model_utils.fields.StatusField(choices=[('id', 'id'), ('code', 'code')], default='id', max_length=100, no_check_for_status=True)),
|
||||||
),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
model_utils.fields.AutoLastModifiedField(
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
editable=False,
|
|
||||||
verbose_name="modified",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
randomslugfield.fields.RandomSlugField(
|
|
||||||
blank=True,
|
|
||||||
editable=False,
|
|
||||||
length=30,
|
|
||||||
max_length=30,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
unique=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("client_id", models.URLField()),
|
|
||||||
("scope", models.TextField(blank=True)),
|
|
||||||
("redirect_uri", models.URLField()),
|
|
||||||
(
|
|
||||||
"response_type",
|
|
||||||
model_utils.fields.StatusField(
|
|
||||||
choices=[("id", "id"), ("code", "code")],
|
|
||||||
default="id",
|
|
||||||
max_length=100,
|
|
||||||
no_check_for_status=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"abstract": False,
|
'abstract': False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Token",
|
name='Token',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||||
"created",
|
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||||
model_utils.fields.AutoCreatedField(
|
('id', randomslugfield.fields.RandomSlugField(blank=True, editable=False, length=30, max_length=30, primary_key=True, serialize=False, unique=True)),
|
||||||
default=django.utils.timezone.now,
|
('client_id', models.URLField()),
|
||||||
editable=False,
|
('scope', models.TextField(blank=True)),
|
||||||
verbose_name="created",
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
model_utils.fields.AutoLastModifiedField(
|
|
||||||
default=django.utils.timezone.now,
|
|
||||||
editable=False,
|
|
||||||
verbose_name="modified",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
randomslugfield.fields.RandomSlugField(
|
|
||||||
blank=True,
|
|
||||||
editable=False,
|
|
||||||
length=30,
|
|
||||||
max_length=30,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
unique=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("client_id", models.URLField()),
|
|
||||||
("scope", models.TextField(blank=True)),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"abstract": False,
|
'abstract': False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,7 +17,6 @@ class AuthSecret(TimeStampedModel):
|
||||||
authorisation codes and tokens in IndieAuth - the two contain many
|
authorisation codes and tokens in IndieAuth - the two contain many
|
||||||
identical fields, but just a few differences.
|
identical fields, but just a few differences.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = RandomSlugField(primary_key=True, length=30)
|
id = RandomSlugField(primary_key=True, length=30)
|
||||||
client_id = models.URLField()
|
client_id = models.URLField()
|
||||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||||
|
@ -28,7 +27,7 @@ class AuthSecret(TimeStampedModel):
|
||||||
return self.user.full_url
|
return self.user.full_url
|
||||||
|
|
||||||
def __contains__(self, scope):
|
def __contains__(self, scope):
|
||||||
return scope in self.scope.split(" ")
|
return scope in self.scope.split(' ')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -42,11 +41,10 @@ class IndieAuthCode(AuthSecret):
|
||||||
Codes are single-use, and if unused will be expired automatically after
|
Codes are single-use, and if unused will be expired automatically after
|
||||||
thirty seconds.
|
thirty seconds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
redirect_uri = models.URLField()
|
redirect_uri = models.URLField()
|
||||||
|
|
||||||
RESPONSE_TYPE = Choices("id", "code")
|
RESPONSE_TYPE = Choices('id', 'code')
|
||||||
response_type = StatusField(choices_name="RESPONSE_TYPE")
|
response_type = StatusField(choices_name='RESPONSE_TYPE')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expired(self):
|
def expired(self):
|
||||||
|
@ -58,5 +56,4 @@ class Token(AuthSecret):
|
||||||
A Token grants a client long-term authorisation - it will not expire unless
|
A Token grants a client long-term authorisation - it will not expire unless
|
||||||
explicitly revoked by the user.
|
explicitly revoked by the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
img
|
img
|
||||||
height 2em
|
height 2em
|
||||||
margin-right .5em
|
margin-right .5em
|
||||||
.tippy-box
|
.tippy-tooltip
|
||||||
&[data-theme~='success']
|
&.success-theme
|
||||||
color $base0B
|
color $base0B
|
||||||
&[data-theme~='warning']
|
&.warning-theme
|
||||||
color $base0A
|
color $base0A
|
||||||
.verified-success
|
.verified-success
|
||||||
color $base0B
|
color $base0B
|
||||||
|
|
|
@ -3,17 +3,17 @@ from .models import IndieAuthCode, Token
|
||||||
|
|
||||||
|
|
||||||
def auth(request) -> Token:
|
def auth(request) -> Token:
|
||||||
if "HTTP_AUTHORIZATION" in request.META:
|
if 'HTTP_AUTHORIZATION' in request.META:
|
||||||
auth = request.META.get("HTTP_AUTHORIZATION").split(" ")
|
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
|
||||||
if auth[0] != "Bearer":
|
if auth[0] != 'Bearer':
|
||||||
raise error.bad_req("auth type {0} not supported".format(auth[0]))
|
raise error.bad_req('auth type {0} not supported'.format(auth[0]))
|
||||||
if len(auth) != 2:
|
if len(auth) != 2:
|
||||||
raise error.bad_req("invalid Bearer auth format, must be Bearer <token>")
|
raise error.bad_req('invalid Bearer auth format, must be Bearer <token>')
|
||||||
token = auth[1]
|
token = auth[1]
|
||||||
elif "access_token" in request.POST:
|
elif 'access_token' in request.POST:
|
||||||
token = request.POST.get("access_token")
|
token = request.POST.get('access_token')
|
||||||
elif "access_token" in request.GET:
|
elif 'access_token' in request.GET:
|
||||||
token = request.GET.get("access_token")
|
token = request.GET.get('access_token')
|
||||||
else:
|
else:
|
||||||
raise error.unauthorized()
|
raise error.unauthorized()
|
||||||
|
|
||||||
|
@ -28,11 +28,11 @@ def auth(request) -> Token:
|
||||||
def gen_auth_code(req):
|
def gen_auth_code(req):
|
||||||
code = IndieAuthCode()
|
code = IndieAuthCode()
|
||||||
code.user = req.user
|
code.user = req.user
|
||||||
code.client_id = req.POST["client_id"]
|
code.client_id = req.POST['client_id']
|
||||||
code.redirect_uri = req.POST["redirect_uri"]
|
code.redirect_uri = req.POST['redirect_uri']
|
||||||
code.response_type = req.POST.get("response_type", "id")
|
code.response_type = req.POST.get('response_type', 'id')
|
||||||
if "scope" in req.POST:
|
if 'scope' in req.POST:
|
||||||
code.scope = " ".join(req.POST.getlist("scope"))
|
code.scope = ' '.join(req.POST.getlist('scope'))
|
||||||
code.save()
|
code.save()
|
||||||
return code.id
|
return code.id
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = "lemonauth"
|
app_name = 'lemonauth'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("login", views.login, name="login"),
|
path('login', views.login, name='login'),
|
||||||
path("logout", views.logout, name="logout"),
|
path('logout', views.logout, name='logout'),
|
||||||
path("indie", views.IndieView.as_view(), name="indie"),
|
path('indie', views.IndieView.as_view(), name='indie'),
|
||||||
path("indie/approve", views.indie_approve, name="indie_approve"),
|
path('indie/approve', views.indie_approve, name='indie_approve'),
|
||||||
path("token", views.TokenView.as_view(), name="token"),
|
path('token', views.TokenView.as_view(), name='token'),
|
||||||
path("tokens", views.TokensListView.as_view(), name="tokens"),
|
path('tokens', views.TokensListView.as_view(), name='tokens'),
|
||||||
path(
|
path('tokens/<path:client_id>', views.TokensRevokeView.as_view(), name='tokens_revoke'),
|
||||||
"tokens/<path:client_id>",
|
|
||||||
views.TokensRevokeView.as_view(),
|
|
||||||
name="tokens_revoke",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,114 +12,120 @@ from urllib.parse import urlencode, urljoin, urlunparse, urlparse
|
||||||
from .. import tokens
|
from .. import tokens
|
||||||
from ..models import IndieAuthCode
|
from ..models import IndieAuthCode
|
||||||
|
|
||||||
breadcrumbs.add("lemonauth:indie", parent="home:index")
|
breadcrumbs.add('lemonauth:indie', parent='home:index')
|
||||||
|
|
||||||
|
|
||||||
def canonical(url):
|
def canonical(url):
|
||||||
if "//" not in url:
|
if '//' not in url:
|
||||||
url = "//" + url
|
url = '//' + url
|
||||||
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
||||||
if not scheme or scheme == "http":
|
if not scheme or scheme == 'http':
|
||||||
scheme = "https"
|
scheme = 'https'
|
||||||
if not path:
|
if not path:
|
||||||
path = "/"
|
path = '/'
|
||||||
return urlunparse((scheme, netloc, path, params, query, fragment))
|
return urlunparse((scheme, netloc, path, params, query, fragment))
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class IndieView(TemplateView):
|
class IndieView(TemplateView):
|
||||||
template_name = "lemonauth/indie.html"
|
template_name = 'lemonauth/indie.html'
|
||||||
required_params = ("client_id", "redirect_uri")
|
required_params = ('client_id', 'redirect_uri')
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
@method_decorator(render_to(template_name))
|
@method_decorator(render_to(template_name))
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
params = request.GET.dict()
|
params = request.GET.dict()
|
||||||
params.setdefault("response_type", "id")
|
params.setdefault('response_type', 'id')
|
||||||
|
|
||||||
for param in self.required_params:
|
for param in self.required_params:
|
||||||
if param not in params:
|
if param not in params:
|
||||||
return utils.bad_req("parameter {0} is required".format(param))
|
return utils.bad_req(
|
||||||
|
'parameter {0} is required'.format(param)
|
||||||
me = request.user.full_url
|
|
||||||
if "me" in params:
|
|
||||||
param_me = canonical(params["me"])
|
|
||||||
if me != param_me:
|
|
||||||
return utils.forbid(
|
|
||||||
"you are logged in as {}, not as {}".format(me, param_me)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
redirect_uri = urljoin(params["client_id"], params["redirect_uri"])
|
me = request.user.full_url
|
||||||
|
if 'me' in params:
|
||||||
|
param_me = canonical(params['me'])
|
||||||
|
if me != param_me:
|
||||||
|
return utils.forbid(
|
||||||
|
'you are logged in as {}, not as {}'.format(me, param_me)
|
||||||
|
)
|
||||||
|
|
||||||
type = params["response_type"]
|
redirect_uri = urljoin(params['client_id'], params['redirect_uri'])
|
||||||
if type not in ("id", "code"):
|
|
||||||
return utils.bad_req("unknown response_type: {0}".format(type))
|
type = params['response_type']
|
||||||
|
if type not in ('id', 'code'):
|
||||||
|
return utils.bad_req(
|
||||||
|
'unknown response_type: {0}'.format(type)
|
||||||
|
)
|
||||||
|
|
||||||
scopes = ()
|
scopes = ()
|
||||||
if type == "code":
|
if type == 'code':
|
||||||
if "scope" not in params:
|
if 'scope' not in params:
|
||||||
return utils.bad_req("scopes required for code type")
|
return utils.bad_req(
|
||||||
scopes = params["scope"].split(" ")
|
'scopes required for code type'
|
||||||
|
)
|
||||||
|
scopes = params['scope'].split(' ')
|
||||||
|
|
||||||
client = requests.mf2(params["client_id"])
|
client = requests.mf2(params['client_id'])
|
||||||
rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ())
|
rels = (client.to_dict()['rel-urls']
|
||||||
verified = "redirect_uri" in rels
|
.get(redirect_uri, {})
|
||||||
|
.get('rels', ()))
|
||||||
|
verified = 'redirect_uri' in rels
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = client.to_dict(filter_by_type="h-x-app")[0]["properties"]
|
app = client.to_dict(filter_by_type='h-x-app')[0]['properties']
|
||||||
except IndexError:
|
except IndexError:
|
||||||
app = None
|
app = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"app": app,
|
'app': app,
|
||||||
"me": me,
|
'me': me,
|
||||||
"redirect_uri": redirect_uri,
|
'redirect_uri': redirect_uri,
|
||||||
"verified": verified,
|
'verified': verified,
|
||||||
"params": params,
|
'params': params,
|
||||||
"scopes": scopes,
|
'scopes': scopes,
|
||||||
"title": "indieauth from {client_id}".format(**params),
|
'title': 'indieauth from {client_id}'.format(**params),
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
post = request.POST.dict()
|
post = request.POST.dict()
|
||||||
try:
|
try:
|
||||||
code = IndieAuthCode.objects.get(pk=post.get("code"))
|
code = IndieAuthCode.objects.get(pk=post.get('code'))
|
||||||
except IndieAuthCode.DoesNotExist:
|
except IndieAuthCode.DoesNotExist:
|
||||||
# if anything at all goes wrong when decoding the auth code, bail
|
# if anything at all goes wrong when decoding the auth code, bail
|
||||||
# out immediately.
|
# out immediately.
|
||||||
return utils.forbid("invalid auth code")
|
return utils.forbid('invalid auth code')
|
||||||
code.delete()
|
code.delete()
|
||||||
if code.expired:
|
if code.expired:
|
||||||
return utils.forbid("invalid auth code")
|
return utils.forbid('invalid auth code')
|
||||||
|
|
||||||
if code.response_type != "id":
|
if code.response_type != 'id':
|
||||||
return utils.bad_req("this endpoint only supports response_type=id")
|
return utils.bad_req(
|
||||||
if code.client_id != post.get("client_id"):
|
'this endpoint only supports response_type=id'
|
||||||
return utils.forbid("client id did not match")
|
)
|
||||||
if code.redirect_uri != post.get("redirect_uri"):
|
if code.client_id != post.get('client_id'):
|
||||||
return utils.forbid("redirect uri did not match")
|
return utils.forbid('client id did not match')
|
||||||
|
if code.redirect_uri != post.get('redirect_uri'):
|
||||||
|
return utils.forbid('redirect uri did not match')
|
||||||
|
|
||||||
# If we got here, it's valid! Yay!
|
# If we got here, it's valid! Yay!
|
||||||
return utils.choose_type(
|
return utils.choose_type(request, {'me': code.me}, {
|
||||||
request,
|
'application/x-www-form-urlencoded': utils.form_encoded_response,
|
||||||
{"me": code.me},
|
'application/json': JsonResponse,
|
||||||
{
|
})
|
||||||
"application/x-www-form-urlencoded": utils.form_encoded_response,
|
|
||||||
"application/json": JsonResponse,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def approve(request):
|
def approve(request):
|
||||||
params = {
|
params = {
|
||||||
"me": urljoin(utils.origin(request), request.user.url),
|
'me': urljoin(utils.origin(request), request.user.url),
|
||||||
"code": tokens.gen_auth_code(request),
|
'code': tokens.gen_auth_code(request),
|
||||||
}
|
}
|
||||||
if "state" in request.POST:
|
if 'state' in request.POST:
|
||||||
params["state"] = request.POST["state"]
|
params['state'] = request.POST['state']
|
||||||
|
|
||||||
uri = request.POST["redirect_uri"]
|
uri = request.POST['redirect_uri']
|
||||||
sep = "&" if "?" in uri else "?"
|
sep = '&' if '?' in uri else '?'
|
||||||
return redirect(uri + sep + urlencode(params))
|
return redirect(uri + sep + urlencode(params))
|
||||||
|
|
|
@ -2,11 +2,11 @@ import django.contrib.auth.views
|
||||||
from otp_agents.forms import OTPAuthenticationForm
|
from otp_agents.forms import OTPAuthenticationForm
|
||||||
from lemoncurry import breadcrumbs
|
from lemoncurry import breadcrumbs
|
||||||
|
|
||||||
breadcrumbs.add(route="lemonauth:login", label="log in", parent="home:index")
|
breadcrumbs.add(route='lemonauth:login', label='log in', parent='home:index')
|
||||||
|
|
||||||
login = django.contrib.auth.views.LoginView.as_view(
|
login = django.contrib.auth.views.LoginView.as_view(
|
||||||
authentication_form=OTPAuthenticationForm,
|
authentication_form=OTPAuthenticationForm,
|
||||||
extra_context={"title": "log in"},
|
extra_context={'title': 'log in'},
|
||||||
template_name="lemonauth/login.html",
|
template_name='lemonauth/login.html',
|
||||||
redirect_authenticated_user=True,
|
redirect_authenticated_user=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,42 +7,41 @@ from ..models import IndieAuthCode
|
||||||
from lemoncurry import utils
|
from lemoncurry import utils
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class TokenView(View):
|
class TokenView(View):
|
||||||
def get(self, req):
|
def get(self, req):
|
||||||
token = tokens.auth(req)
|
token = tokens.auth(req)
|
||||||
res = {
|
res = {
|
||||||
"me": token.me,
|
'me': token.me,
|
||||||
"client_id": token.client_id,
|
'client_id': token.client_id,
|
||||||
"scope": token.scope,
|
'scope': token.scope,
|
||||||
}
|
}
|
||||||
return utils.choose_type(req, res)
|
return utils.choose_type(req, res)
|
||||||
|
|
||||||
def post(self, req):
|
def post(self, req):
|
||||||
post = req.POST
|
post = req.POST
|
||||||
try:
|
try:
|
||||||
code = IndieAuthCode.objects.get(pk=post.get("code"))
|
code = IndieAuthCode.objects.get(pk=post.get('code'))
|
||||||
except IndieAuthCode.DoesNotExist:
|
except IndieAuthCode.DoesNotExist:
|
||||||
return utils.forbid("invalid auth code")
|
return utils.forbid('invalid auth code')
|
||||||
code.delete()
|
code.delete()
|
||||||
if code.expired:
|
if code.expired:
|
||||||
return utils.forbid("invalid auth code")
|
return utils.forbid('invalid auth code')
|
||||||
|
|
||||||
if code.response_type != "code":
|
if code.response_type != 'code':
|
||||||
return utils.bad_req("this endpoint only supports response_type=code")
|
return utils.bad_req(
|
||||||
if "client_id" in post and code.client_id != post["client_id"]:
|
'this endpoint only supports response_type=code'
|
||||||
return utils.forbid("client id did not match")
|
)
|
||||||
if code.redirect_uri != post.get("redirect_uri"):
|
if 'client_id' in post and code.client_id != post['client_id']:
|
||||||
return utils.forbid("redirect uri did not match")
|
return utils.forbid('client id did not match')
|
||||||
|
if code.redirect_uri != post.get('redirect_uri'):
|
||||||
|
return utils.forbid('redirect uri did not match')
|
||||||
|
|
||||||
if "me" in post and code.me != post["me"]:
|
if 'me' in post and code.me != post['me']:
|
||||||
return utils.forbid("me did not match")
|
return utils.forbid('me did not match')
|
||||||
|
|
||||||
return utils.choose_type(
|
return utils.choose_type(req, {
|
||||||
req,
|
'access_token': tokens.gen_token(code),
|
||||||
{
|
'me': code.me,
|
||||||
"access_token": tokens.gen_token(code),
|
'scope': code.scope,
|
||||||
"me": code.me,
|
})
|
||||||
"scope": code.scope,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from typing import Dict, Optional, Set
|
from typing import Dict, Optional, Set
|
||||||
|
@ -21,15 +20,15 @@ class Client:
|
||||||
self.id = client_id
|
self.id = client_id
|
||||||
self.count = 0
|
self.count = 0
|
||||||
self.scopes = set()
|
self.scopes = set()
|
||||||
|
apps = mf2(self.id).to_dict(filter_by_type='h-x-app')
|
||||||
try:
|
try:
|
||||||
apps = mf2(self.id).to_dict(filter_by_type="h-x-app")
|
self.app = apps[0]['properties']
|
||||||
self.app = apps[0]["properties"]
|
except IndexError:
|
||||||
except (RequestException, IndexError):
|
|
||||||
self.app = None
|
self.app = None
|
||||||
|
|
||||||
|
|
||||||
class TokensListView(LoginRequiredMixin, TemplateView):
|
class TokensListView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = "lemonauth/tokens.html"
|
template_name = 'lemonauth/tokens.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -37,6 +36,6 @@ class TokensListView(LoginRequiredMixin, TemplateView):
|
||||||
for token in self.request.user.token_set.all():
|
for token in self.request.user.token_set.all():
|
||||||
client = clients[token.client_id]
|
client = clients[token.client_id]
|
||||||
client.count += 1
|
client.count += 1
|
||||||
client.scopes |= set(token.scope.split(" "))
|
client.scopes |= set(token.scope.split(' '))
|
||||||
context.update({"clients": clients, "title": "tokens"})
|
context.update({'clients': clients, 'title': 'tokens'})
|
||||||
return context
|
return context
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Crumb:
|
||||||
return self._label
|
return self._label
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if hasattr(other, "route"):
|
if hasattr(other, 'route'):
|
||||||
return self.route == other.route
|
return self.route == other.route
|
||||||
return self.route == other
|
return self.route == other
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@ from debug_toolbar.middleware import show_toolbar as core_show_toolbar
|
||||||
|
|
||||||
|
|
||||||
def show_toolbar(request):
|
def show_toolbar(request):
|
||||||
if request.path.endswith("/amp"):
|
if request.path.endswith('/amp'):
|
||||||
return False
|
return False
|
||||||
return core_show_toolbar(request)
|
return core_show_toolbar(request)
|
||||||
|
|
|
@ -22,22 +22,18 @@ def environment(**options):
|
||||||
lstrip_blocks=True,
|
lstrip_blocks=True,
|
||||||
**options
|
**options
|
||||||
)
|
)
|
||||||
env.filters.update(
|
env.filters.update({
|
||||||
{
|
'ago': ago,
|
||||||
"ago": ago,
|
'friendly_url': friendly_url,
|
||||||
"friendly_url": friendly_url,
|
'markdown': markdown,
|
||||||
"markdown": markdown,
|
})
|
||||||
}
|
env.globals.update({
|
||||||
)
|
'entry_kinds': entry_kinds,
|
||||||
env.globals.update(
|
'favicons': favicons,
|
||||||
{
|
'package': load_package_json(),
|
||||||
"entry_kinds": entry_kinds,
|
'settings': settings,
|
||||||
"favicons": favicons,
|
'static': staticfiles_storage.url,
|
||||||
"package": load_package_json(),
|
'theme_color': theme_color,
|
||||||
"settings": settings,
|
'url': reverse,
|
||||||
"static": staticfiles_storage.url,
|
})
|
||||||
"theme_color": theme_color,
|
|
||||||
"url": reverse,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return env
|
return env
|
||||||
|
|
|
@ -6,4 +6,4 @@ def ago(dt: datetime) -> str:
|
||||||
# We have to convert the datetime we get to local time first, because ago
|
# We have to convert the datetime we get to local time first, because ago
|
||||||
# just strips the timezone from a timezone-aware datetime.
|
# just strips the timezone from a timezone-aware datetime.
|
||||||
dt = dt.astimezone()
|
dt = dt.astimezone()
|
||||||
return human(dt, precision=1, past_tense="{}", abbreviate=True)
|
return human(dt, precision=1, past_tense='{}', abbreviate=True)
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
from bleach.sanitizer import Cleaner, ALLOWED_TAGS
|
from bleach.sanitizer import Cleaner, ALLOWED_TAGS
|
||||||
from bleach.linkifier import LinkifyFilter
|
from bleach.linkifier import LinkifyFilter
|
||||||
from jinja2 import pass_eval_context
|
from jinja2 import evalcontextfilter, Markup
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
TAGS = ["cite", "code", "details", "p", "pre", "img", "span", "summary"]
|
TAGS = ['cite', 'code', 'p', 'pre', 'img', 'span']
|
||||||
TAGS.extend(ALLOWED_TAGS)
|
TAGS.extend(ALLOWED_TAGS)
|
||||||
ATTRIBUTES = {
|
ATTRIBUTES = {
|
||||||
"a": ["href", "title", "class"],
|
'a': ('href', 'title', 'class'),
|
||||||
"details": ["open"],
|
'img': ('alt', 'src', 'title'),
|
||||||
"img": ["alt", "src", "title"],
|
'span': ('class',),
|
||||||
"span": ["class"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,))
|
cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,))
|
||||||
|
|
||||||
|
|
||||||
@pass_eval_context
|
@evalcontextfilter
|
||||||
def bleach(ctx, html):
|
def bleach(ctx, html):
|
||||||
res = cleaner.clean(html)
|
res = cleaner.clean(html)
|
||||||
if ctx.autoescape:
|
if ctx.autoescape:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html{% block html_attr %} dir="ltr" lang="en" data-bs-theme="dark"{% endblock %}>
|
<html{% block html_attr %} dir="ltr" lang="en"{% endblock %}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
@ -27,10 +27,10 @@
|
||||||
<link rel="{{ i.rel }}" type="{{ i.mime }}" sizes="{{ i.sizes }}" href="{{ i.url }}" />
|
<link rel="{{ i.rel }}" type="{{ i.mime }}" sizes="{{ i.sizes }}" href="{{ i.url }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
|
||||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/monokai.min.css"
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/monokai.min.css"
|
||||||
integrity="sha384-88Jvj9Q2LiBDwL7w3yciRTcH5q2zzvMFYIm4xX9/evqxJsxA33Xk9XYKcvUlPITo" crossorigin="anonymous">
|
integrity="sha384-bHqbpRh/XW+phptvH9nQvMKHwPH1ZbOxpIeAB2D2OIEL4Ni7aZzZgMFpsRra+v1g" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/openwebicons@1.6.0/css/openwebicons.min.css"
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/openwebicons@1.6.0/css/openwebicons.min.css"
|
||||||
integrity="sha384-NkRWM9o4Kfak7GwS+un+sProBBpj02vc/e1EoXvdCUSdRk0muOfkKJ5NtpueAuka" crossorigin="anonymous">
|
integrity="sha384-NkRWM9o4Kfak7GwS+un+sProBBpj02vc/e1EoXvdCUSdRk0muOfkKJ5NtpueAuka" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@3.4.1/dist/tippy.css"
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@3.4.1/dist/tippy.css"
|
||||||
|
@ -40,11 +40,12 @@
|
||||||
{% block styles %}{% endblock %}
|
{% block styles %}{% endblock %}
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|
||||||
<script type="text/javascript" defer src="https://kit.fontawesome.com/a3aade9b41.js" crossorigin="anonymous"></script>
|
<script type="text/javascript" defer src="https://use.fontawesome.com/releases/v5.6.3/js/all.js"
|
||||||
|
integrity="sha384-EIHISlAOj4zgYieurP0SdoiBYfGJKkgWedPHH4jCzpCXLmzVsw1ouK59MuUtP4a1" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body{% block body_attr %}{% endblock %}>
|
<body{% block body_attr %}{% endblock %}>
|
||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-md"><div class="container-fluid">
|
<nav class="navbar navbar-expand-md navbar-dark">
|
||||||
<a class="navbar-brand" rel="home" href="{{ url('home:index') }}">{{ request.site.name }}</a>
|
<a class="navbar-brand" rel="home" href="{{ url('home:index') }}">{{ request.site.name }}</a>
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
|
||||||
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
@ -96,7 +97,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endactiveurl %}
|
{% endactiveurl %}
|
||||||
|
|
||||||
</div></nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
@ -110,29 +111,29 @@
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
{% if entries.has_previous() %}
|
{% if entries.prev %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" rel="prev" href="{{ entries.reverse(entries.previous_page_number()) }}">
|
<a class="page-link" rel="prev" href="{{ entries.prev.url }}">
|
||||||
<i class="fas fa-step-backward" aria-hidden="true"></i> <span class="sr-only">previous page</span>
|
<i class="fas fa-step-backward" aria-hidden="true"></i> <span class="sr-only">previous page</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for i in entries.paginator.page_range %}
|
{% for page in entries.pages %}
|
||||||
{% if i == entries.number %}
|
{% if page.current %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<span class="page-link">{{ i }} <span class="sr-only">(current page)</span></span>
|
<span class="page-link">{{ page.i }} <span class="sr-only">(current page)</span></span>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{{ entries.reverse(i) }}">{{ i }}</a>
|
<a class="page-link" href="{{ page.url }}">{{ page.i }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if entries.has_next() %}
|
{% if entries.next %}
|
||||||
|
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" rel="next" href="{{ entries.reverse(entries.next_page_number()) }}">
|
<a class="page-link" rel="next" href="{{ entries.next.url }}">
|
||||||
<i class="fas fa-step-forward" aria-hidden="true"></i> <span class="sr-only">next page</span>
|
<i class="fas fa-step-forward" aria-hidden="true"></i> <span class="sr-only">next page</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -146,14 +147,14 @@
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
|
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
|
||||||
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js" crossorigin="anonymous"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp"></script>
|
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"></script>
|
||||||
<script src="https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"></script>
|
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"></script>
|
integrity="sha384-BlPof9RtjBqeJFskKv3sK3dh4Wk70iKlpIe92FeVN+6qxaGUOUu+mZNpALZ+K7ya"></script>
|
||||||
<script src="https://unpkg.com/tippy.js@6.3.7/dist/tippy-bundle.umd.js" crossorigin="anonymous"
|
<script src="https://unpkg.com/tippy.js@3.4.1/dist/tippy.standalone.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-dtMr4wkcxQWUqsJFgElu4AttgIhOsjr2vYIzP2mv0MZbD/uJ6OHxFdbgE3MOKabN"></script>
|
integrity="sha384-x7dGoSfOWUdyPccAel9dkWte6n8GxDWbByavEixRzW0O9xvPGzg3y0qzZBwGNUw9"></script>
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
hljs.initHighlightingOnLoad();
|
hljs.initHighlightingOnLoad();
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
from jinja2 import pass_eval_context
|
from jinja2 import evalcontextfilter
|
||||||
from markdown import Markdown
|
from markdown import Markdown
|
||||||
|
|
||||||
from .bleach import bleach
|
from .bleach import bleach
|
||||||
|
|
||||||
md = Markdown(
|
md = Markdown(extensions=(
|
||||||
extensions=(
|
'extra',
|
||||||
"extra",
|
'sane_lists',
|
||||||
"sane_lists",
|
'smarty',
|
||||||
"smarty",
|
'toc',
|
||||||
"toc",
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pass_eval_context
|
@evalcontextfilter
|
||||||
def markdown(ctx, source):
|
def markdown(ctx, source):
|
||||||
return bleach(ctx, md.reset().convert(source))
|
return bleach(ctx, md.reset().convert(source))
|
||||||
|
|
|
@ -8,9 +8,7 @@ class ResponseException(Exception):
|
||||||
|
|
||||||
|
|
||||||
class ResponseExceptionMiddleware(MiddlewareMixin):
|
class ResponseExceptionMiddleware(MiddlewareMixin):
|
||||||
def process_exception(
|
def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse:
|
||||||
self, request: HttpRequest, exception: Exception
|
|
||||||
) -> HttpResponse:
|
|
||||||
if isinstance(exception, ResponseException):
|
if isinstance(exception, ResponseException):
|
||||||
return exception.response
|
return exception.response
|
||||||
raise exception
|
raise exception
|
||||||
|
|
|
@ -11,7 +11,7 @@ from mf2py import Parser
|
||||||
class DjangoCache(BaseCache):
|
class DjangoCache(BaseCache):
|
||||||
@classmethod
|
@classmethod
|
||||||
def key(cls, url):
|
def key(cls, url):
|
||||||
return "req:" + sha256(url.encode("utf-8")).hexdigest()
|
return 'req:' + sha256(url.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
def get(self, url):
|
def get(self, url):
|
||||||
key = self.key(url)
|
key = self.key(url)
|
||||||
|
@ -45,4 +45,4 @@ def get(url):
|
||||||
|
|
||||||
def mf2(url):
|
def mf2(url):
|
||||||
r = get(url)
|
r = get(url)
|
||||||
return Parser(doc=r.text, url=url, html_parser="html5lib")
|
return Parser(doc=r.text, url=url, html_parser='html5lib')
|
||||||
|
|
|
@ -16,7 +16,7 @@ from typing import List
|
||||||
APPEND_SLASH = False
|
APPEND_SLASH = False
|
||||||
|
|
||||||
ADMINS = [
|
ADMINS = [
|
||||||
("dani", "dani@00dani.me"),
|
('dani', 'dani@00dani.me'),
|
||||||
]
|
]
|
||||||
|
|
||||||
BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
|
BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
|
||||||
|
@ -26,13 +26,13 @@ BASE_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww"
|
SECRET_KEY = '6riil57g@r^wprf7mdy((+bs&(6l*phcn9&fd$l0@t-kzj+xww'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS: List[str] = []
|
ALLOWED_HOSTS = [] # type: List[str]
|
||||||
INTERNAL_IPS = ["127.0.0.1", "::1"]
|
INTERNAL_IPS = ['127.0.0.1', '::1']
|
||||||
|
|
||||||
# Settings to tighten up security - these can safely be on in dev mode too,
|
# Settings to tighten up security - these can safely be on in dev mode too,
|
||||||
# since I dev using a local HTTPS server.
|
# since I dev using a local HTTPS server.
|
||||||
|
@ -50,7 +50,7 @@ CSRF_COOKIE_SECURE = True
|
||||||
# Miscellanous headers to protect against attacks.
|
# Miscellanous headers to protect against attacks.
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
X_FRAME_OPTIONS = "DENY"
|
X_FRAME_OPTIONS = 'DENY'
|
||||||
|
|
||||||
# This technically isn't needed, since nginx doesn't let the app be accessed
|
# This technically isn't needed, since nginx doesn't let the app be accessed
|
||||||
# over insecure HTTP anyway. Just for completeness!
|
# over insecure HTTP anyway. Just for completeness!
|
||||||
|
@ -58,104 +58,110 @@ SECURE_SSL_REDIRECT = True
|
||||||
|
|
||||||
# We run behind nginx, so we need nginx to tell us whether we're using HTTPS or
|
# We run behind nginx, so we need nginx to tell us whether we're using HTTPS or
|
||||||
# not.
|
# not.
|
||||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"lemoncurry",
|
'lemoncurry',
|
||||||
"pyup_django",
|
'pyup_django',
|
||||||
"django.contrib.admin",
|
|
||||||
"django.contrib.admindocs",
|
'django.contrib.admin',
|
||||||
"django.contrib.auth",
|
'django.contrib.admindocs',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.auth',
|
||||||
"django.contrib.humanize",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.sessions",
|
'django.contrib.humanize',
|
||||||
"django.contrib.sites",
|
'django.contrib.sessions',
|
||||||
"django.contrib.sitemaps",
|
'django.contrib.sites',
|
||||||
"django.contrib.messages",
|
'django.contrib.sitemaps',
|
||||||
"django.contrib.staticfiles",
|
'django.contrib.messages',
|
||||||
"annoying",
|
'django.contrib.staticfiles',
|
||||||
"compressor",
|
|
||||||
"computed_property",
|
'analytical',
|
||||||
"corsheaders",
|
'annoying',
|
||||||
"debug_toolbar",
|
'compressor',
|
||||||
"django_activeurl",
|
'computed_property',
|
||||||
"django_agent_trust",
|
'corsheaders',
|
||||||
"django_extensions",
|
'debug_toolbar',
|
||||||
"django_otp",
|
'django_activeurl',
|
||||||
"django_otp.plugins.otp_static",
|
'django_agent_trust',
|
||||||
"django_otp.plugins.otp_totp",
|
'django_extensions',
|
||||||
"django_rq",
|
'django_otp',
|
||||||
"meta",
|
'django_otp.plugins.otp_static',
|
||||||
"entries",
|
'django_otp.plugins.otp_totp',
|
||||||
"home",
|
'django_rq',
|
||||||
"lemonauth",
|
'favicon',
|
||||||
"lemonshort",
|
'meta',
|
||||||
"micropub",
|
|
||||||
"users",
|
'entries',
|
||||||
"webmention",
|
'home',
|
||||||
"wellknowns",
|
'lemonauth',
|
||||||
|
'lemonshort',
|
||||||
|
'micropub',
|
||||||
|
'users',
|
||||||
|
'webmention',
|
||||||
|
'wellknowns',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
"django.middleware.http.ConditionalGetMiddleware",
|
'django.middleware.http.ConditionalGetMiddleware',
|
||||||
"django.middleware.security.SecurityMiddleware",
|
'django.middleware.security.SecurityMiddleware',
|
||||||
"django.contrib.admindocs.middleware.XViewMiddleware",
|
'django.contrib.admindocs.middleware.XViewMiddleware',
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.middleware.common.CommonMiddleware',
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"django_otp.middleware.OTPMiddleware",
|
'django_otp.middleware.OTPMiddleware',
|
||||||
"django_agent_trust.middleware.AgentMiddleware",
|
'django_agent_trust.middleware.AgentMiddleware',
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
"django.contrib.sites.middleware.CurrentSiteMiddleware",
|
'django.contrib.sites.middleware.CurrentSiteMiddleware',
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
"lemoncurry.middleware.ResponseExceptionMiddleware",
|
'lemoncurry.middleware.ResponseExceptionMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "lemoncurry.urls"
|
ROOT_URLCONF = 'lemoncurry.urls'
|
||||||
|
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.jinja2.Jinja2",
|
'BACKEND': 'django.template.backends.jinja2.Jinja2',
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"environment": "lemoncurry.jinja2.environment",
|
'environment': 'lemoncurry.jinja2.environment',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.debug",
|
'django.template.context_processors.debug',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "lemoncurry.wsgi.application"
|
WSGI_APPLICATION = 'lemoncurry.wsgi.application'
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-CACHES
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
'default': {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
"LOCATION": "redis://127.0.0.1:6380/0",
|
'LOCATION': 'redis://127.0.0.1:6380/0',
|
||||||
"KEY_PREFIX": "lemoncurry",
|
'KEY_PREFIX': 'lemoncurry',
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer",
|
'PARSER_CLASS': 'redis.connection.HiredisParser',
|
||||||
|
'SERIALIZER': 'lemoncurry.msgpack.MSGPackModernSerializer',
|
||||||
},
|
},
|
||||||
"VERSION": 2,
|
'VERSION': 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,51 +169,50 @@ CACHES = {
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
'default': {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
"NAME": environ.get("POSTGRES_DB", "lemoncurry"),
|
'NAME': environ.get('POSTGRES_DB', 'lemoncurry'),
|
||||||
"USER": environ.get("POSTGRES_USER"),
|
'USER': environ.get('POSTGRES_USER'),
|
||||||
"PASSWORD": environ.get("POSTGRES_PASSWORD"),
|
'PASSWORD': environ.get('POSTGRES_PASSWORD'),
|
||||||
"HOST": environ.get("POSTGRES_HOST", "localhost"),
|
'HOST': environ.get('POSTGRES_HOST', 'localhost'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = "users.User"
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
# Password hashers
|
# Password hashers
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
"django.contrib.auth.hashers.BCryptPasswordHasher",
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
PW_VALIDATOR_MODULE = "django.contrib.auth.password_validation"
|
PW_VALIDATOR_MODULE = 'django.contrib.auth.password_validation'
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": PW_VALIDATOR_MODULE + ".UserAttributeSimilarityValidator"},
|
{'NAME': PW_VALIDATOR_MODULE + '.UserAttributeSimilarityValidator'},
|
||||||
{"NAME": PW_VALIDATOR_MODULE + ".MinimumLengthValidator"},
|
{'NAME': PW_VALIDATOR_MODULE + '.MinimumLengthValidator'},
|
||||||
{"NAME": PW_VALIDATOR_MODULE + ".CommonPasswordValidator"},
|
{'NAME': PW_VALIDATOR_MODULE + '.CommonPasswordValidator'},
|
||||||
{"NAME": PW_VALIDATOR_MODULE + ".NumericPasswordValidator"},
|
{'NAME': PW_VALIDATOR_MODULE + '.NumericPasswordValidator'},
|
||||||
]
|
]
|
||||||
|
|
||||||
LOGIN_URL = "lemonauth:login"
|
LOGIN_URL = 'lemonauth:login'
|
||||||
LOGIN_REDIRECT_URL = "home:index"
|
LOGIN_REDIRECT_URL = 'home:index'
|
||||||
LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL
|
LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-au"
|
LANGUAGE_CODE = 'en-au'
|
||||||
|
|
||||||
TIME_ZONE = "Australia/Sydney"
|
TIME_ZONE = 'Australia/Sydney'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -219,21 +224,21 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = path.join(BASE_DIR, "static")
|
STATIC_ROOT = path.join(BASE_DIR, 'static')
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
"compressor.finders.CompressorFinder",
|
'compressor.finders.CompressorFinder',
|
||||||
)
|
)
|
||||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
|
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||||
|
|
||||||
COMPRESS_PRECOMPILERS = (
|
COMPRESS_PRECOMPILERS = (
|
||||||
("text/stylus", "npx stylus -u ./lemoncurry/static/lemoncurry/css/theme"),
|
('text/stylus', 'npx stylus -u ./lemoncurry/static/lemoncurry/css/theme'),
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_URL = STATIC_URL + "media/"
|
MEDIA_URL = STATIC_URL + 'media/'
|
||||||
MEDIA_ROOT = path.join(STATIC_ROOT, "media")
|
MEDIA_ROOT = path.join(STATIC_ROOT, 'media')
|
||||||
|
|
||||||
# django-contrib-sites
|
# django-contrib-sites
|
||||||
# https://docs.djangoproject.com/en/dev/ref/contrib/sites/
|
# https://docs.djangoproject.com/en/dev/ref/contrib/sites/
|
||||||
|
@ -245,25 +250,28 @@ AGENT_COOKIE_SECURE = True
|
||||||
|
|
||||||
# django-cors-headers
|
# django-cors-headers
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
CORS_URLS_REGEX = r"^/(?!admin|auth/(?:login|logout|indie)).*$"
|
CORS_URLS_REGEX = r'^/(?!admin|auth/(?:login|logout|indie)).*$'
|
||||||
|
|
||||||
# lemonshort
|
# lemonshort
|
||||||
SHORT_BASE_URL = "/s/"
|
SHORT_BASE_URL = '/s/'
|
||||||
SHORTEN_MODELS = {
|
SHORTEN_MODELS = {
|
||||||
"e": "entries.entry",
|
'e': 'entries.entry',
|
||||||
}
|
}
|
||||||
|
|
||||||
# django-meta
|
# django-meta
|
||||||
# https://django-meta.readthedocs.io/en/latest/settings.html
|
# https://django-meta.readthedocs.io/en/latest/settings.html
|
||||||
META_SITE_PROTOCOL = "https"
|
META_SITE_PROTOCOL = 'https'
|
||||||
META_USE_SITES = True
|
META_USE_SITES = True
|
||||||
META_USE_OG_PROPERTIES = True
|
META_USE_OG_PROPERTIES = True
|
||||||
META_USE_TWITTER_PROPERTIES = True
|
META_USE_TWITTER_PROPERTIES = True
|
||||||
|
|
||||||
# django-push
|
# django-push
|
||||||
# https://django-push.readthedocs.io/en/latest/publisher.html
|
# https://django-push.readthedocs.io/en/latest/publisher.html
|
||||||
PUSH_HUB = "https://00dani.superfeedr.com/"
|
PUSH_HUB = 'https://00dani.superfeedr.com/'
|
||||||
|
|
||||||
# django-rq
|
# django-rq
|
||||||
# https://github.com/ui/django-rq
|
# https://github.com/ui/django-rq
|
||||||
RQ_QUEUES = {"default": {"USE_REDIS_CACHE": "default"}}
|
RQ_QUEUES = {'default': {'USE_REDIS_CACHE': 'default'}}
|
||||||
|
|
||||||
|
# django-super-favicon
|
||||||
|
FAVICON_STORAGE = 'django.core.files.storage.DefaultStorage'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ['*']
|
||||||
META_SITE_DOMAIN = "00dani.lo"
|
META_SITE_DOMAIN = '00dani.dev'
|
||||||
META_FB_APPID = "142105433189339"
|
META_FB_APPID = '142105433189339'
|
||||||
STATIC_URL = "https://static.00dani.lo/"
|
STATIC_URL = 'https://static.00dani.dev/'
|
||||||
MEDIA_URL = "https://media.00dani.lo/"
|
MEDIA_URL = STATIC_URL + 'media/'
|
||||||
|
|
|
@ -4,19 +4,19 @@ from os.path import join
|
||||||
from .base import *
|
from .base import *
|
||||||
from .base import BASE_DIR, DATABASES
|
from .base import BASE_DIR, DATABASES
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["00dani.me"]
|
ALLOWED_HOSTS = ['00dani.me']
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
SECRET_KEY = environ["DJANGO_SECRET_KEY"]
|
SECRET_KEY = environ['DJANGO_SECRET_KEY']
|
||||||
SERVER_EMAIL = "lemoncurry@00dani.me"
|
SERVER_EMAIL = 'lemoncurry@00dani.me'
|
||||||
|
|
||||||
# Authenticate as an app-specific Postgres user in production.
|
# Authenticate as an app-specific Postgres user in production.
|
||||||
DATABASES["default"]["USER"] = "lemoncurry"
|
DATABASES['default']['USER'] = 'lemoncurry'
|
||||||
|
|
||||||
SHORT_BASE_URL = "https://nya.as/"
|
SHORT_BASE_URL = 'https://nya.as/'
|
||||||
|
|
||||||
STATIC_ROOT = join(BASE_DIR, "..", "static")
|
STATIC_ROOT = join(BASE_DIR, '..', 'static')
|
||||||
MEDIA_ROOT = join(BASE_DIR, "..", "media")
|
MEDIA_ROOT = join(BASE_DIR, '..', 'media')
|
||||||
STATIC_URL = "https://cdn.00dani.me/"
|
STATIC_URL = 'https://cdn.00dani.me/'
|
||||||
MEDIA_URL = STATIC_URL + "m/"
|
MEDIA_URL = STATIC_URL + 'm/'
|
||||||
META_SITE_DOMAIN = "00dani.me"
|
META_SITE_DOMAIN = '00dani.me'
|
||||||
META_FB_APPID = "145311792869199"
|
META_FB_APPID = '145311792869199'
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ['*']
|
||||||
SECURE_SSL_REDIRECT = False
|
SECURE_SSL_REDIRECT = False
|
||||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
|
||||||
STATIC_ROOT = path.join(BASE_DIR, "media")
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ html
|
||||||
|
|
||||||
a
|
a
|
||||||
color $base0D
|
color $base0D
|
||||||
text-decoration none
|
|
||||||
&:hover
|
&:hover
|
||||||
color $base0C
|
color $base0C
|
||||||
|
|
||||||
|
@ -32,13 +31,13 @@ code, pre, .code, .pre
|
||||||
text-decoration none
|
text-decoration none
|
||||||
line-height 1
|
line-height 1
|
||||||
|
|
||||||
.tippy-box[data-theme~='dark']
|
for placement in top bottom left right
|
||||||
|
.tippy-popper[x-placement^={placement}] .tippy-tooltip.dark-theme .tippy-arrow
|
||||||
|
border-{placement}-color $base03
|
||||||
|
|
||||||
|
.tippy-tooltip.dark-theme
|
||||||
background-color $base03
|
background-color $base03
|
||||||
color $base04
|
color $base04
|
||||||
text-align center
|
|
||||||
for placement in top bottom left right
|
|
||||||
&[data-placement^={placement}] > .tippy-arrow::before
|
|
||||||
border-{placement}-color $base03
|
|
||||||
|
|
||||||
body
|
body
|
||||||
display flex
|
display flex
|
||||||
|
@ -104,12 +103,6 @@ ul.pagination
|
||||||
background-color $base02
|
background-color $base02
|
||||||
border 1px solid rgba(0,0,0,.125)
|
border 1px solid rgba(0,0,0,.125)
|
||||||
|
|
||||||
.media
|
|
||||||
display flex
|
|
||||||
> .media-body
|
|
||||||
flex-grow 1
|
|
||||||
margin-left 3px
|
|
||||||
|
|
||||||
.card
|
.card
|
||||||
background-color $base02
|
background-color $base02
|
||||||
|
|
||||||
|
|
|
@ -8,5 +8,5 @@ register = template.Library()
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
@register.filter(is_safe=True)
|
@register.filter(is_safe=True)
|
||||||
def absolute_url(url):
|
def absolute_url(url):
|
||||||
base = "https://" + Site.objects.get_current().domain
|
base = 'https://' + Site.objects.get_current().domain
|
||||||
return urljoin(base, url)
|
return urljoin(base, url)
|
||||||
|
|
|
@ -5,13 +5,12 @@ from django.utils.safestring import mark_safe
|
||||||
from bleach.sanitizer import Cleaner, ALLOWED_TAGS
|
from bleach.sanitizer import Cleaner, ALLOWED_TAGS
|
||||||
from bleach.linkifier import LinkifyFilter
|
from bleach.linkifier import LinkifyFilter
|
||||||
|
|
||||||
tags = ["cite", "code", "details", "p", "pre", "img", "span", "summary"]
|
tags = ['cite', 'code', 'p', 'pre', 'img', 'span']
|
||||||
tags.extend(ALLOWED_TAGS)
|
tags.extend(ALLOWED_TAGS)
|
||||||
attributes = {
|
attributes = {
|
||||||
"a": ["href", "title", "class"],
|
'a': ('href', 'title', 'class'),
|
||||||
"details": ["open"],
|
'img': ('alt', 'src', 'title'),
|
||||||
"img": ["alt", "src", "title"],
|
'span': ('class',),
|
||||||
"span": ["class"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
|
@ -11,5 +11,5 @@ register = template.Library()
|
||||||
@register.filter
|
@register.filter
|
||||||
def jsonify(value):
|
def jsonify(value):
|
||||||
if isinstance(value, QuerySet):
|
if isinstance(value, QuerySet):
|
||||||
return mark_safe(serialize("json", value))
|
return mark_safe(serialize('json', value))
|
||||||
return mark_safe(json.dumps(value, cls=DjangoJSONEncoder))
|
return mark_safe(json.dumps(value, cls=DjangoJSONEncoder))
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
|
@ -39,71 +40,67 @@ def site_name():
|
||||||
return Site.objects.get_current().name
|
return Site.objects.get_current().name
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("lemoncurry/tags/nav.html")
|
@register.inclusion_tag('lemoncurry/tags/nav.html')
|
||||||
def nav_left(request):
|
def nav_left(request):
|
||||||
items = (
|
items = (MenuItem(
|
||||||
MenuItem(label=k.plural, icon=k.icon, url=("entries:index", (k,)))
|
label=k.plural,
|
||||||
for k in kinds.all
|
icon=k.icon,
|
||||||
)
|
url=('entries:index', (k,))
|
||||||
return {"items": items, "request": request}
|
) for k in kinds.all)
|
||||||
|
return {'items': items, 'request': request}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("lemoncurry/tags/nav.html")
|
@register.inclusion_tag('lemoncurry/tags/nav.html')
|
||||||
def nav_right(request):
|
def nav_right(request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
items = (
|
items = (
|
||||||
MenuItem(label="admin", icon="fas fa-cog", url="admin:index"),
|
MenuItem(label='admin', icon='fas fa-cog', url='admin:index'),
|
||||||
MenuItem(
|
MenuItem(label='log out', icon='fas fa-sign-out-alt',
|
||||||
label="log out", icon="fas fa-sign-out-alt", url="lemonauth:logout"
|
url='lemonauth:logout'),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
items = (
|
items = (
|
||||||
MenuItem(label="log in", icon="fas fa-sign-in-alt", url="lemonauth:login"),
|
MenuItem(label='log in', icon='fas fa-sign-in-alt',
|
||||||
|
url='lemonauth:login'),
|
||||||
)
|
)
|
||||||
return {"items": items, "request": request}
|
return {'items': items, 'request': request}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("lemoncurry/tags/breadcrumbs.html", takes_context=True)
|
@register.inclusion_tag('lemoncurry/tags/breadcrumbs.html', takes_context=True)
|
||||||
def nav_crumbs(context, route):
|
def nav_crumbs(context, route):
|
||||||
crumbs = breadcrumbs.find(route)
|
crumbs = breadcrumbs.find(route)
|
||||||
current = crumbs.pop()
|
current = crumbs.pop()
|
||||||
|
|
||||||
item_list_element = [
|
item_list_element = [{
|
||||||
{
|
'@type': 'ListItem',
|
||||||
"@type": "ListItem",
|
'position': i + 1,
|
||||||
"position": i + 1,
|
'item': {
|
||||||
"item": {
|
'@id': context['origin'] + crumb.url,
|
||||||
"@id": context["origin"] + crumb.url,
|
'@type': 'WebPage',
|
||||||
"@type": "WebPage",
|
'name': crumb.label
|
||||||
"name": crumb.label,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for i, crumb in enumerate(crumbs)
|
} for i, crumb in enumerate(crumbs)]
|
||||||
]
|
item_list_element.append({
|
||||||
item_list_element.append(
|
'@type': 'ListItem',
|
||||||
{
|
'position': len(item_list_element) + 1,
|
||||||
"@type": "ListItem",
|
'item': {
|
||||||
"position": len(item_list_element) + 1,
|
'id': context['uri'],
|
||||||
"item": {
|
'@type': 'WebPage',
|
||||||
"id": context["uri"],
|
'name': current.label or context.get('title'),
|
||||||
"@type": "WebPage",
|
|
||||||
"name": current.label or context.get("title"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
breadcrumb_list = {
|
breadcrumb_list = {
|
||||||
"@context": "http://schema.org",
|
'@context': 'http://schema.org',
|
||||||
"@type": "BreadcrumbList",
|
'@type': 'BreadcrumbList',
|
||||||
"itemListElement": item_list_element,
|
'itemListElement': item_list_element
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"breadcrumb_list": breadcrumb_list,
|
'breadcrumb_list': breadcrumb_list,
|
||||||
"crumbs": crumbs,
|
'crumbs': crumbs,
|
||||||
"current": current,
|
'current': current,
|
||||||
"title": context.get("title"),
|
'title': context.get('title'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,12 @@ from django import template
|
||||||
from markdown import Markdown
|
from markdown import Markdown
|
||||||
from .bleach import bleach
|
from .bleach import bleach
|
||||||
|
|
||||||
md = Markdown(
|
md = Markdown(extensions=(
|
||||||
extensions=(
|
'extra',
|
||||||
"extra",
|
'sane_lists',
|
||||||
"sane_lists",
|
'smarty',
|
||||||
"smarty",
|
'toc',
|
||||||
"toc",
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
|
@ -6,43 +6,43 @@ from .. import breadcrumbs as b
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def nested_crumbs():
|
def nested_crumbs():
|
||||||
x = b.Crumb("nc.x", label="x")
|
x = b.Crumb('nc.x', label='x')
|
||||||
y = b.Crumb("nc.y", label="y", parent="nc.x")
|
y = b.Crumb('nc.y', label='y', parent='nc.x')
|
||||||
z = b.Crumb("nc.z", label="z", parent="nc.y")
|
z = b.Crumb('nc.z', label='z', parent='nc.y')
|
||||||
crumbs = (x, y, z)
|
crumbs = (x, y, z)
|
||||||
|
|
||||||
for crumb in crumbs:
|
for crumb in crumbs:
|
||||||
b.breadcrumbs[crumb.route] = crumb
|
b.breadcrumbs[crumb.route] = crumb
|
||||||
yield namedtuple("NestedCrumbs", "x y z")(*crumbs)
|
yield namedtuple('NestedCrumbs', 'x y z')(*crumbs)
|
||||||
for crumb in crumbs:
|
for crumb in crumbs:
|
||||||
del b.breadcrumbs[crumb.route]
|
del b.breadcrumbs[crumb.route]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def crumb_match(nested_crumbs):
|
def crumb_match(nested_crumbs):
|
||||||
return namedtuple("Match", "view_name")(nested_crumbs.z.route)
|
return namedtuple('Match', 'view_name')(nested_crumbs.z.route)
|
||||||
|
|
||||||
|
|
||||||
class TestAdd:
|
class TestAdd:
|
||||||
def test_inserts_a_breadcrumb_without_parent(self):
|
def test_inserts_a_breadcrumb_without_parent(self):
|
||||||
route = "tests.add.insert"
|
route = 'tests.add.insert'
|
||||||
assert route not in b.breadcrumbs
|
assert route not in b.breadcrumbs
|
||||||
b.add(route, "some label")
|
b.add(route, 'some label')
|
||||||
assert route in b.breadcrumbs
|
assert route in b.breadcrumbs
|
||||||
assert b.breadcrumbs[route] == route
|
assert b.breadcrumbs[route] == route
|
||||||
route = b.breadcrumbs[route]
|
route = b.breadcrumbs[route]
|
||||||
assert route.label == "some label"
|
assert route.label == 'some label'
|
||||||
assert route.parent is None
|
assert route.parent is None
|
||||||
|
|
||||||
def test_inserts_a_breadcrumb_with_parent(self):
|
def test_inserts_a_breadcrumb_with_parent(self):
|
||||||
route = "tests.add.with_parent"
|
route = 'tests.add.with_parent'
|
||||||
parent = "tests.add.insert"
|
parent = 'tests.add.insert'
|
||||||
assert route not in b.breadcrumbs
|
assert route not in b.breadcrumbs
|
||||||
b.add(route, "child label", parent)
|
b.add(route, 'child label', parent)
|
||||||
assert route in b.breadcrumbs
|
assert route in b.breadcrumbs
|
||||||
assert b.breadcrumbs[route] == route
|
assert b.breadcrumbs[route] == route
|
||||||
route = b.breadcrumbs[route]
|
route = b.breadcrumbs[route]
|
||||||
assert route.label == "child label"
|
assert route.label == 'child label'
|
||||||
assert route.parent == parent
|
assert route.parent == parent
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,22 +5,22 @@ from .. import utils
|
||||||
class TestOrigin:
|
class TestOrigin:
|
||||||
def test_simple_http(self):
|
def test_simple_http(self):
|
||||||
"""should return the correct origin for a vanilla HTTP site"""
|
"""should return the correct origin for a vanilla HTTP site"""
|
||||||
req = Mock(scheme="http", site=Mock(domain="lemoncurry.test"))
|
req = Mock(scheme='http', site=Mock(domain='lemoncurry.test'))
|
||||||
assert utils.origin(req) == "http://lemoncurry.test"
|
assert utils.origin(req) == 'http://lemoncurry.test'
|
||||||
|
|
||||||
def test_simple_https(self):
|
def test_simple_https(self):
|
||||||
"""should return the correct origin for a vanilla HTTPS site"""
|
"""should return the correct origin for a vanilla HTTPS site"""
|
||||||
req = Mock(scheme="https", site=Mock(domain="secure.lemoncurry.test"))
|
req = Mock(scheme='https', site=Mock(domain='secure.lemoncurry.test'))
|
||||||
assert utils.origin(req) == "https://secure.lemoncurry.test"
|
assert utils.origin(req) == 'https://secure.lemoncurry.test'
|
||||||
|
|
||||||
|
|
||||||
class TestUri:
|
class TestUri:
|
||||||
def test_siteroot(self):
|
def test_siteroot(self):
|
||||||
"""should return correct full URI for requests to the site root"""
|
"""should return correct full URI for requests to the site root"""
|
||||||
req = Mock(scheme="https", path="/", site=Mock(domain="l.test"))
|
req = Mock(scheme='https', path='/', site=Mock(domain='l.test'))
|
||||||
assert utils.uri(req) == "https://l.test/"
|
assert utils.uri(req) == 'https://l.test/'
|
||||||
|
|
||||||
def test_path(self):
|
def test_path(self):
|
||||||
"""should return correct full URI for requests with a path"""
|
"""should return correct full URI for requests with a path"""
|
||||||
req = Mock(scheme="https", path="/notes/23", site=Mock(domain="l.tst"))
|
req = Mock(scheme='https', path='/notes/23', site=Mock(domain='l.tst'))
|
||||||
assert utils.uri(req) == "https://l.tst/notes/23"
|
assert utils.uri(req) == 'https://l.tst/notes/23'
|
||||||
|
|
|
@ -4,14 +4,12 @@ from yaml import safe_load
|
||||||
|
|
||||||
path = join(
|
path = join(
|
||||||
settings.BASE_DIR,
|
settings.BASE_DIR,
|
||||||
"lemoncurry",
|
'lemoncurry', 'static',
|
||||||
"static",
|
'base16-materialtheme-scheme', 'material-darker.yaml',
|
||||||
"base16-materialtheme-scheme",
|
|
||||||
"material-darker.yaml",
|
|
||||||
)
|
)
|
||||||
with open(path, "r") as f:
|
with open(path, 'r') as f:
|
||||||
theme = safe_load(f)
|
theme = safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
def color(i):
|
def color(i):
|
||||||
return "#" + theme["base0" + format(i, "1X")]
|
return '#' + theme['base0' + format(i, '1X')]
|
||||||
|
|
|
@ -27,37 +27,33 @@ from entries.sitemaps import EntriesSitemap
|
||||||
from home.sitemaps import HomeSitemap
|
from home.sitemaps import HomeSitemap
|
||||||
|
|
||||||
sections = {
|
sections = {
|
||||||
"entries": EntriesSitemap,
|
'entries': EntriesSitemap,
|
||||||
"home": HomeSitemap,
|
'home': HomeSitemap,
|
||||||
}
|
}
|
||||||
maps = {"sitemaps": sections}
|
maps = {'sitemaps': sections}
|
||||||
|
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
path("", include("home.urls")),
|
path('', include('home.urls')),
|
||||||
path("", include("entries.urls")),
|
path('', include('entries.urls')),
|
||||||
path("", include("users.urls")),
|
path('', include('users.urls')),
|
||||||
path(".well-known/", include("wellknowns.urls")),
|
path('.well-known/', include('wellknowns.urls')),
|
||||||
path("admin/doc/", include("django.contrib.admindocs.urls")),
|
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
path("admin/", admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path("auth/", include("lemonauth.urls")),
|
path('auth/', include('lemonauth.urls')),
|
||||||
path(
|
path('favicon.ico', RedirectView.as_view(
|
||||||
"favicon.ico",
|
url=settings.MEDIA_URL + 'favicon/favicon.ico')),
|
||||||
RedirectView.as_view(url=settings.MEDIA_URL + "favicon/favicon.ico"),
|
path('micropub', include('micropub.urls')),
|
||||||
),
|
path('s/', include('lemonshort.urls')),
|
||||||
path("micropub", include("micropub.urls")),
|
path('webmention', include('webmention.urls')),
|
||||||
path("s/", include("lemonshort.urls")),
|
|
||||||
path("webmention", include("webmention.urls")),
|
path('django-rq/', include('django_rq.urls')),
|
||||||
path("django-rq/", include("django_rq.urls")),
|
path('sitemap.xml', sitemap.index, maps, name='sitemap'),
|
||||||
path("sitemap.xml", sitemap.index, maps, name="sitemap"),
|
path('sitemaps/<section>.xml', sitemap.sitemap, maps,
|
||||||
path(
|
name='django.contrib.sitemaps.views.sitemap'),
|
||||||
"sitemaps/<section>.xml",
|
|
||||||
sitemap.sitemap,
|
|
||||||
maps,
|
|
||||||
name="django.contrib.sitemaps.views.sitemap",
|
|
||||||
),
|
|
||||||
) # type: Tuple[URLPattern, ...]
|
) # type: Tuple[URLPattern, ...]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
urlpatterns += (
|
||||||
urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),)
|
path('__debug__/', include(debug_toolbar.urls)),
|
||||||
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class PackageJson:
|
||||||
|
|
||||||
def load(self) -> Dict[str, Any]:
|
def load(self) -> Dict[str, Any]:
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
with open(join(settings.BASE_DIR, "package.json")) as f:
|
with open(join(settings.BASE_DIR, 'package.json')) as f:
|
||||||
self.data = json.load(f)
|
self.data = json.load(f)
|
||||||
assert self.data is not None
|
assert self.data is not None
|
||||||
return self.data
|
return self.data
|
||||||
|
@ -30,10 +30,10 @@ PACKAGE = PackageJson()
|
||||||
|
|
||||||
|
|
||||||
def friendly_url(url):
|
def friendly_url(url):
|
||||||
if "//" not in url:
|
if '//' not in url:
|
||||||
url = "//" + url
|
url = '//' + url
|
||||||
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
||||||
if path == "/":
|
if path == '/':
|
||||||
return netloc
|
return netloc
|
||||||
return "{}\u200B{}".format(netloc, path)
|
return "{}\u200B{}".format(netloc, path)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ def load_package_json() -> Dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def origin(request):
|
def origin(request):
|
||||||
return "{0}://{1}".format(request.scheme, request.site.domain)
|
return '{0}://{1}'.format(request.scheme, request.site.domain)
|
||||||
|
|
||||||
|
|
||||||
def absolute_url(request, url):
|
def absolute_url(request, url):
|
||||||
|
@ -56,18 +56,19 @@ def uri(request):
|
||||||
|
|
||||||
def form_encoded_response(content):
|
def form_encoded_response(content):
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
urlencode(content), content_type="application/x-www-form-urlencoded"
|
urlencode(content),
|
||||||
|
content_type='application/x-www-form-urlencoded'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
REPS = {
|
REPS = {
|
||||||
"application/x-www-form-urlencoded": form_encoded_response,
|
'application/x-www-form-urlencoded': form_encoded_response,
|
||||||
"application/json": JsonResponse,
|
'application/json': JsonResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def choose_type(request, content, reps=REPS):
|
def choose_type(request, content, reps=REPS):
|
||||||
accept = request.META.get("HTTP_ACCEPT", "*/*")
|
accept = request.META.get('HTTP_ACCEPT', '*/*')
|
||||||
type = get_best_match(accept, reps.keys())
|
type = get_best_match(accept, reps.keys())
|
||||||
if type:
|
if type:
|
||||||
return reps[type](content)
|
return reps[type](content)
|
||||||
|
@ -75,11 +76,11 @@ def choose_type(request, content, reps=REPS):
|
||||||
|
|
||||||
|
|
||||||
def bad_req(message):
|
def bad_req(message):
|
||||||
return HttpResponseBadRequest(message, content_type="text/plain")
|
return HttpResponseBadRequest(message, content_type='text/plain')
|
||||||
|
|
||||||
|
|
||||||
def forbid(message):
|
def forbid(message):
|
||||||
return HttpResponseForbidden(message, content_type="text/plain")
|
return HttpResponseForbidden(message, content_type='text/plain')
|
||||||
|
|
||||||
|
|
||||||
def to_plain(md):
|
def to_plain(md):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'lemonshort.apps.LemonshortConfig'
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class LemonshortConfig(AppConfig):
|
class LemonshortConfig(AppConfig):
|
||||||
name = "lemonshort"
|
name = 'lemonshort'
|
||||||
|
|
|
@ -7,11 +7,9 @@ chars = ascii_uppercase + ascii_lowercase
|
||||||
conv = BaseConverter(chars)
|
conv = BaseConverter(chars)
|
||||||
|
|
||||||
|
|
||||||
class AbcIdConverter:
|
def abc_to_id(abc):
|
||||||
regex = "[a-zA-Z]+"
|
return int(conv.decode(abc))
|
||||||
|
|
||||||
def to_python(self, value: str) -> int:
|
|
||||||
return int(conv.decode(value))
|
|
||||||
|
|
||||||
def to_url(self, value: int) -> str:
|
def id_to_abc(id):
|
||||||
return conv.encode(value)
|
return conv.encode(id)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from typing import Any, Dict, Type
|
from typing import Any, Dict, Type
|
||||||
|
|
||||||
from .convert import AbcIdConverter
|
from .convert import id_to_abc
|
||||||
|
|
||||||
prefixes = {} # type: Dict[Type[Any], str]
|
prefixes = {} # type: Dict[Type[Any], str]
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ def short_url(entity):
|
||||||
if not prefixes:
|
if not prefixes:
|
||||||
for k, m in settings.SHORTEN_MODELS.items():
|
for k, m in settings.SHORTEN_MODELS.items():
|
||||||
prefixes[apps.get_model(m)] = k
|
prefixes[apps.get_model(m)] = k
|
||||||
base = "/"
|
base = '/'
|
||||||
if hasattr(settings, "SHORT_BASE_URL"):
|
if hasattr(settings, 'SHORT_BASE_URL'):
|
||||||
base = settings.SHORT_BASE_URL
|
base = settings.SHORT_BASE_URL
|
||||||
return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id)
|
return base + prefixes[type(entity)] + id_to_abc(entity.id)
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
from .. import convert
|
from .. import convert
|
||||||
|
|
||||||
|
|
||||||
def test_to_python():
|
def test_abc_to_id():
|
||||||
samples = {
|
samples = {
|
||||||
"A": 0,
|
'A': 0,
|
||||||
"B": 1,
|
'B': 1,
|
||||||
"Y": 24,
|
'Y': 24,
|
||||||
"a": 26,
|
'a': 26,
|
||||||
"b": 27,
|
'b': 27,
|
||||||
"y": 50,
|
'y': 50,
|
||||||
"BA": 52,
|
'BA': 52,
|
||||||
"BAB": 2705,
|
'BAB': 2705,
|
||||||
}
|
}
|
||||||
converter = convert.AbcIdConverter()
|
|
||||||
for abc, id in samples.items():
|
for abc, id in samples.items():
|
||||||
assert converter.to_python(abc) == id
|
assert convert.abc_to_id(abc) == id
|
||||||
|
|
||||||
|
|
||||||
def test_id_to_abc():
|
def test_id_to_abc():
|
||||||
samples = {
|
samples = {
|
||||||
1: "B",
|
1: 'B',
|
||||||
24: "Y",
|
24: 'Y',
|
||||||
26: "a",
|
26: 'a',
|
||||||
52: "BA",
|
52: 'BA',
|
||||||
78: "Ba",
|
78: 'Ba',
|
||||||
104: "CA",
|
104: 'CA',
|
||||||
130: "Ca",
|
130: 'Ca',
|
||||||
}
|
}
|
||||||
converter = convert.AbcIdConverter()
|
|
||||||
for id, abc in samples.items():
|
for id, abc in samples.items():
|
||||||
assert converter.to_url(id) == abc
|
assert convert.id_to_abc(id) == abc
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import path, register_converter
|
from django.urls import path
|
||||||
|
|
||||||
from .convert import AbcIdConverter
|
|
||||||
from .views import unshort
|
from .views import unshort
|
||||||
|
|
||||||
register_converter(AbcIdConverter, "abc_id")
|
app_name = 'lemonshort'
|
||||||
|
|
||||||
app_name = "lemonshort"
|
|
||||||
urlpatterns = tuple(
|
urlpatterns = tuple(
|
||||||
path("{0!s}<abc_id:tiny>".format(k), unshort, name=m, kwargs={"model": m})
|
path('{0!s}<tiny>'.format(k), unshort, name=m, kwargs={'model': m})
|
||||||
for k, m in settings.SHORTEN_MODELS.items()
|
for k, m in settings.SHORTEN_MODELS.items()
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
|
||||||
from .convert import AbcIdConverter
|
from .convert import abc_to_id
|
||||||
|
|
||||||
|
|
||||||
def unshort(request, model, tiny):
|
def unshort(request, model, tiny):
|
||||||
entity = get_object_or_404(apps.get_model(model), pk=tiny)
|
entity = get_object_or_404(apps.get_model(model), pk=abc_to_id(tiny))
|
||||||
return redirect(entity, permanent=True)
|
return redirect(entity, permanent=True)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'micropub.apps.MicropubConfig'
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class MicropubConfig(AppConfig):
|
class MicropubConfig(AppConfig):
|
||||||
name = "micropub"
|
name = 'micropub'
|
||||||
|
|
|
@ -4,35 +4,33 @@ from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def forbidden() -> ResponseException:
|
def forbidden() -> ResponseException:
|
||||||
return res("forbidden", 403)
|
return res('forbidden', 403)
|
||||||
|
|
||||||
|
|
||||||
def unauthorized() -> ResponseException:
|
def unauthorized() -> ResponseException:
|
||||||
return res("unauthorized", 401)
|
return res('unauthorized', 401)
|
||||||
|
|
||||||
|
|
||||||
def bad_req(msg: str) -> ResponseException:
|
def bad_req(msg: str) -> ResponseException:
|
||||||
return res("invalid_request", msg=msg)
|
return res('invalid_request', msg=msg)
|
||||||
|
|
||||||
|
|
||||||
def bad_type(type: str) -> ResponseException:
|
def bad_type(type: str) -> ResponseException:
|
||||||
msg = "unsupported request type {0}".format(type)
|
msg = 'unsupported request type {0}'.format(type)
|
||||||
return res("invalid_request", 415, msg)
|
return res('invalid_request', 415, msg)
|
||||||
|
|
||||||
|
|
||||||
def bad_scope(scope: str) -> ResponseException:
|
def bad_scope(scope: str) -> ResponseException:
|
||||||
return res("insufficient_scope", 401, scope=scope)
|
return res('insufficient_scope', 401, scope=scope)
|
||||||
|
|
||||||
|
|
||||||
def res(
|
def res(error: str,
|
||||||
error: str,
|
status: Optional[int]=400,
|
||||||
status: Optional[int] = 400,
|
msg: Optional[str]=None,
|
||||||
msg: Optional[str] = None,
|
scope: Optional[str]=None):
|
||||||
scope: Optional[str] = None,
|
content = {'error': error}
|
||||||
):
|
|
||||||
content = {"error": error}
|
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
content["error_description"] = msg
|
content['error_description'] = msg
|
||||||
if scope:
|
if scope:
|
||||||
content["scope"] = scope
|
content['scope'] = scope
|
||||||
return ResponseException(JsonResponse(content, status=status))
|
return ResponseException(JsonResponse(content, status=status))
|
||||||
|
|
|
@ -2,8 +2,8 @@ from django.urls import path
|
||||||
from .views import micropub
|
from .views import micropub
|
||||||
from .views.media import media
|
from .views.media import media
|
||||||
|
|
||||||
app_name = "micropub"
|
app_name = 'micropub'
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
path("", micropub, name="micropub"),
|
path('', micropub, name='micropub'),
|
||||||
path("/media", media, name="media"),
|
path('/media', media, name='media'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,22 +10,22 @@ from .delete import delete
|
||||||
from .query import query
|
from .query import query
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
"create": create,
|
'create': create,
|
||||||
"delete": delete,
|
'delete': delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@require_http_methods(["GET", "HEAD", "POST"])
|
@require_http_methods(['GET', 'HEAD', 'POST'])
|
||||||
def micropub(request):
|
def micropub(request):
|
||||||
request.token = tokens.auth(request)
|
request.token = tokens.auth(request)
|
||||||
if request.method in ("GET", "HEAD"):
|
if request.method in ('GET', 'HEAD'):
|
||||||
return query(request)
|
return query(request)
|
||||||
|
|
||||||
action = request.POST.get("action", "create")
|
action = request.POST.get('action', 'create')
|
||||||
if request.content_type == "application/json":
|
if request.content_type == 'application/json':
|
||||||
request.json = json.load(request)
|
request.json = json.load(request)
|
||||||
action = request.json.get("action", "create")
|
action = request.json.get('action', 'create')
|
||||||
if action not in actions:
|
if action not in actions:
|
||||||
raise error.bad_req("unknown action: {}".format(action))
|
raise error.bad_req('unknown action: {}'.format(action))
|
||||||
return actions[action](request)
|
return actions[action](request)
|
||||||
|
|
|
@ -14,62 +14,63 @@ def form_to_mf2(request):
|
||||||
properties = {}
|
properties = {}
|
||||||
post = request.POST
|
post = request.POST
|
||||||
for key in post.keys():
|
for key in post.keys():
|
||||||
if key.endswith("[]"):
|
if key.endswith('[]'):
|
||||||
key = key[:-2]
|
key = key[:-2]
|
||||||
if key == "access_token":
|
if key == 'access_token':
|
||||||
continue
|
continue
|
||||||
properties[key] = post.getlist(key) + post.getlist(key + "[]")
|
properties[key] = post.getlist(key) + post.getlist(key + '[]')
|
||||||
|
|
||||||
type = []
|
type = []
|
||||||
if "h" in properties:
|
if 'h' in properties:
|
||||||
type = ["h-" + p for p in properties["h"]]
|
type = ['h-' + p for p in properties['h']]
|
||||||
del properties["h"]
|
del properties['h']
|
||||||
return {"type": type, "properties": properties}
|
return {'type': type, 'properties': properties}
|
||||||
|
|
||||||
|
|
||||||
def create(request):
|
def create(request):
|
||||||
normalise = {
|
normalise = {
|
||||||
"application/json": lambda r: r.json,
|
'application/json': lambda r: r.json,
|
||||||
"application/x-www-form-urlencoded": form_to_mf2,
|
'application/x-www-form-urlencoded': form_to_mf2,
|
||||||
}
|
}
|
||||||
if "create" not in request.token:
|
if 'create' not in request.token:
|
||||||
raise error.bad_scope("create")
|
raise error.bad_scope('create')
|
||||||
if request.content_type not in normalise:
|
if request.content_type not in normalise:
|
||||||
raise error.unsupported_type(request.content_type)
|
raise error.unsupported_type(request.content_type)
|
||||||
body = normalise[request.content_type](request)
|
body = normalise[request.content_type](request)
|
||||||
if "type" not in body:
|
if 'type' not in body:
|
||||||
raise error.bad_req("mf2 object type required")
|
raise error.bad_req('mf2 object type required')
|
||||||
if body["type"] != ["h-entry"]:
|
if body['type'] != ['h-entry']:
|
||||||
raise error.bad_req("only h-entry supported")
|
raise error.bad_req('only h-entry supported')
|
||||||
|
|
||||||
entry = Entry(author=request.token.user)
|
entry = Entry(author=request.token.user)
|
||||||
props = body.get("properties", {})
|
props = body.get('properties', {})
|
||||||
kind = Note
|
kind = Note
|
||||||
if "name" in props:
|
if 'name' in props:
|
||||||
entry.name = "\n".join(props["name"])
|
entry.name = '\n'.join(props['name'])
|
||||||
kind = Article
|
kind = Article
|
||||||
if "content" in props:
|
if 'content' in props:
|
||||||
entry.content = "\n".join(
|
entry.content = '\n'.join(
|
||||||
c if isinstance(c, str) else c["html"] for c in props["content"]
|
c if isinstance(c, str) else c['html']
|
||||||
|
for c in props['content']
|
||||||
)
|
)
|
||||||
if "in-reply-to" in props:
|
if 'in-reply-to' in props:
|
||||||
entry.in_reply_to = props["in-reply-to"]
|
entry.in_reply_to = props['in-reply-to']
|
||||||
kind = Reply
|
kind = Reply
|
||||||
if "like-of" in props:
|
if 'like-of' in props:
|
||||||
entry.like_of = props["like-of"]
|
entry.like_of = props['like-of']
|
||||||
kind = Like
|
kind = Like
|
||||||
if "repost-of" in props:
|
if 'repost-of' in props:
|
||||||
entry.repost_of = props["repost-of"]
|
entry.repost_of = props['repost-of']
|
||||||
kind = Repost
|
kind = Repost
|
||||||
|
|
||||||
cats = [Cat.objects.from_name(c) for c in props.get("category", [])]
|
cats = [Cat.objects.from_name(c) for c in props.get('category', [])]
|
||||||
|
|
||||||
entry.kind = kind.id
|
entry.kind = kind.id
|
||||||
entry.save()
|
entry.save()
|
||||||
entry.cats.set(cats)
|
entry.cats.set(cats)
|
||||||
entry.save()
|
entry.save()
|
||||||
|
|
||||||
for url in props.get("syndication", []):
|
for url in props.get('syndication', []):
|
||||||
entry.syndications.create(url=url)
|
entry.syndications.create(url=url)
|
||||||
|
|
||||||
base = utils.origin(request)
|
base = utils.origin(request)
|
||||||
|
@ -79,6 +80,6 @@ def create(request):
|
||||||
send_mentions.delay(perma)
|
send_mentions.delay(perma)
|
||||||
|
|
||||||
res = HttpResponse(status=201)
|
res = HttpResponse(status=201)
|
||||||
res["Location"] = perma
|
res['Location'] = perma
|
||||||
res["Link"] = '<{}>; rel="shortlink"'.format(short)
|
res['Link'] = '<{}>; rel="shortlink"'.format(short)
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -6,25 +6,24 @@ from entries.jobs import ping_hub, send_mentions
|
||||||
|
|
||||||
from .. import error
|
from .. import error
|
||||||
|
|
||||||
|
|
||||||
def delete(request):
|
def delete(request):
|
||||||
normalise = {
|
normalise = {
|
||||||
"application/json": lambda r: r.json.get("url"),
|
'application/json': lambda r: r.json.get('url'),
|
||||||
"application/x-www-form-urlencoded": lambda r: r.POST.get("url"),
|
'application/x-www-form-urlencoded': lambda r: r.POST.get('url'),
|
||||||
}
|
}
|
||||||
if "delete" not in request.token:
|
if 'delete' not in request.token:
|
||||||
raise error.bad_scope("delete")
|
raise error.bad_scope('delete')
|
||||||
if request.content_type not in normalise:
|
if request.content_type not in normalise:
|
||||||
raise error.unsupported_type(request.content_type)
|
raise error.unsupported_type(request.content_type)
|
||||||
url = normalise[request.content_type](request)
|
url = normalise[request.content_type](request)
|
||||||
entry = from_url(url)
|
entry = from_url(url)
|
||||||
|
|
||||||
if entry.author != request.token.user:
|
if entry.author != request.token.user:
|
||||||
raise error.forbid("entry belongs to another user")
|
raise error.forbid('entry belongs to another user')
|
||||||
|
|
||||||
perma = entry.absolute_url
|
perma = entry.absolute_url
|
||||||
pings = entry.affected_urls
|
pings = entry.affected_urls
|
||||||
mentions = webmention.findMentions(perma)["refs"]
|
mentions = webmention.findMentions(perma)['refs']
|
||||||
|
|
||||||
entry.delete()
|
entry.delete()
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ from lemoncurry.utils import absolute_url
|
||||||
from .. import error
|
from .. import error
|
||||||
|
|
||||||
ACCEPTED_MEDIA_TYPES = (
|
ACCEPTED_MEDIA_TYPES = (
|
||||||
"image/gif",
|
'image/gif',
|
||||||
"image/jpeg",
|
'image/jpeg',
|
||||||
"image/png",
|
'image/png',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,13 +21,15 @@ ACCEPTED_MEDIA_TYPES = (
|
||||||
@require_POST
|
@require_POST
|
||||||
def media(request):
|
def media(request):
|
||||||
token = tokens.auth(request)
|
token = tokens.auth(request)
|
||||||
if "file" not in request.FILES:
|
if 'file' not in request.FILES:
|
||||||
raise error.bad_req(
|
raise error.bad_req(
|
||||||
"a file named 'file' must be provided to the media endpoint"
|
"a file named 'file' must be provided to the media endpoint"
|
||||||
)
|
)
|
||||||
file = request.FILES["file"]
|
file = request.FILES['file']
|
||||||
if file.content_type not in ACCEPTED_MEDIA_TYPES:
|
if file.content_type not in ACCEPTED_MEDIA_TYPES:
|
||||||
raise error.bad_req("unacceptable file type {0}".format(file.content_type))
|
raise error.bad_req(
|
||||||
|
'unacceptable file type {0}'.format(file.content_type)
|
||||||
|
)
|
||||||
|
|
||||||
mime = None
|
mime = None
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
|
@ -38,15 +40,14 @@ def media(request):
|
||||||
|
|
||||||
if mime != file.content_type:
|
if mime != file.content_type:
|
||||||
raise error.bad_req(
|
raise error.bad_req(
|
||||||
"detected file type {0} did not match specified file type {1}".format(
|
'detected file type {0} did not match specified file type {1}'
|
||||||
mime, file.content_type
|
.format(mime, file.content_type)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
path = "mp/{0[0]}/{2}.{1}".format(*mime.split("/"), sha.hexdigest())
|
path = 'mp/{0[0]}/{2}.{1}'.format(*mime.split('/'), sha.hexdigest())
|
||||||
path = store.save(path, file)
|
path = store.save(path, file)
|
||||||
url = absolute_url(request, store.url(path))
|
url = absolute_url(request, store.url(path))
|
||||||
|
|
||||||
res = HttpResponse(status=201)
|
res = HttpResponse(status=201)
|
||||||
res["Location"] = url
|
res['Location'] = url
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -7,47 +7,48 @@ from lemoncurry.utils import absolute_url
|
||||||
from .. import error
|
from .. import error
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def config(request):
|
def config(request):
|
||||||
config = syndicate_to(request)
|
config = syndicate_to(request)
|
||||||
config["media-endpoint"] = absolute_url(request, reverse("micropub:media"))
|
config['media-endpoint'] = absolute_url(request, reverse('micropub:media'))
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def source(request):
|
def source(request):
|
||||||
if "url" not in request.GET:
|
if 'url' not in request.GET:
|
||||||
raise error.bad_req("must specify url parameter for source query")
|
raise error.bad_req('must specify url parameter for source query')
|
||||||
entry = from_url(request.GET["url"])
|
entry = from_url(request.GET['url'])
|
||||||
props = {}
|
props = {}
|
||||||
|
|
||||||
keys = set(request.GET.getlist("properties") + request.GET.getlist("properties[]"))
|
keys = set(request.GET.getlist('properties') + request.GET.getlist('properties[]'))
|
||||||
if not keys or "content" in keys:
|
if not keys or 'content' in keys:
|
||||||
props["content"] = [entry.content]
|
props['content'] = [entry.content]
|
||||||
if (not keys or "category" in keys) and entry.cats.exists():
|
if (not keys or 'category' in keys) and entry.cats.exists():
|
||||||
props["category"] = [cat.name for cat in entry.cats.all()]
|
props['category'] = [cat.name for cat in entry.cats.all()]
|
||||||
if (not keys or "name" in keys) and entry.name:
|
if (not keys or 'name' in keys) and entry.name:
|
||||||
props["name"] = [entry.name]
|
props['name'] = [entry.name]
|
||||||
if (not keys or "syndication" in keys) and entry.syndications.exists():
|
if (not keys or 'syndication' in keys) and entry.syndications.exists():
|
||||||
props["syndication"] = [synd.url for synd in entry.syndications.all()]
|
props['syndication'] = [synd.url for synd in entry.syndications.all()]
|
||||||
|
|
||||||
return {"type": ["h-entry"], "properties": props}
|
return {'type': ['h-entry'], 'properties': props}
|
||||||
|
|
||||||
|
|
||||||
def syndicate_to(request):
|
def syndicate_to(request):
|
||||||
return {"syndicate-to": []}
|
return {'syndicate-to': []}
|
||||||
|
|
||||||
|
|
||||||
queries = {
|
queries = {
|
||||||
"config": config,
|
'config': config,
|
||||||
"source": source,
|
'source': source,
|
||||||
"syndicate-to": syndicate_to,
|
'syndicate-to': syndicate_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def query(request):
|
def query(request):
|
||||||
if "q" not in request.GET:
|
if 'q' not in request.GET:
|
||||||
raise error.bad_req("must specify q parameter")
|
raise error.bad_req('must specify q parameter')
|
||||||
q = request.GET["q"]
|
q = request.GET['q']
|
||||||
if q not in queries:
|
if q not in queries:
|
||||||
raise error.bad_req("unsupported query {0}".format(q))
|
raise error.bad_req('unsupported query {0}'.format(q))
|
||||||
res = queries[q](request)
|
res = queries[q](request)
|
||||||
return JsonResponse(res)
|
return JsonResponse(res)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemoncurry",
|
"name": "lemoncurry",
|
||||||
"version": "1.12.4",
|
"version": "1.10.2",
|
||||||
"repository": "https://git.00dani.me/00dani/lemoncurry",
|
"repository": "https://git.00dani.me/00dani/lemoncurry",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "lemoncurry"
|
|
||||||
version = "1.12.5"
|
|
||||||
description = "Indieweb-compatible personal website"
|
|
||||||
|
|
||||||
authors = [
|
|
||||||
{name = "Danielle McLean", email = "dani@00dani.me"},
|
|
||||||
]
|
|
||||||
license = {text = "MIT"}
|
|
||||||
requires-python = "<4.0,>=3.12"
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
"accept-types",
|
|
||||||
"ago",
|
|
||||||
"argon2-cffi",
|
|
||||||
"bleach",
|
|
||||||
"cachecontrol",
|
|
||||||
"django<4,>=3",
|
|
||||||
"django-activeurl",
|
|
||||||
"django-annoying",
|
|
||||||
"django-compressor",
|
|
||||||
"django-computed-property",
|
|
||||||
"django-cors-headers",
|
|
||||||
"django-debug-toolbar",
|
|
||||||
"django-extensions",
|
|
||||||
"django-meta",
|
|
||||||
"django-model-utils",
|
|
||||||
"django-otp",
|
|
||||||
"django-otp-agents",
|
|
||||||
"django-push",
|
|
||||||
"django-randomslugfield",
|
|
||||||
"django-redis",
|
|
||||||
"django-rq",
|
|
||||||
"docutils",
|
|
||||||
"gevent",
|
|
||||||
"gunicorn[gevent]",
|
|
||||||
"hiredis",
|
|
||||||
"jinja2",
|
|
||||||
"markdown",
|
|
||||||
"mf2py",
|
|
||||||
"mf2util",
|
|
||||||
"msgpack",
|
|
||||||
"pillow",
|
|
||||||
"psycopg2-binary",
|
|
||||||
"python-baseconv",
|
|
||||||
"python-magic",
|
|
||||||
"python-slugify",
|
|
||||||
"pyup-django",
|
|
||||||
"pyyaml",
|
|
||||||
"qrcode",
|
|
||||||
"ronkyuu",
|
|
||||||
"xrd",
|
|
||||||
"greenlet",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.pdm.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
"mypy",
|
|
||||||
"ptpython",
|
|
||||||
"pytest-django",
|
|
||||||
"types-bleach",
|
|
||||||
"types-markdown",
|
|
||||||
"types-python-slugify",
|
|
||||||
"types-pyyaml",
|
|
||||||
"types-requests",
|
|
||||||
"watchdog",
|
|
||||||
"werkzeug",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.pdm.build]
|
|
||||||
includes = []
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
select = ["E4", "E7", "E9", "F", "I"]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["pdm-backend"]
|
|
||||||
build-backend = "pdm.backend"
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'users.apps.UsersConfig'
|
|
@ -1,14 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from .models import PgpKey, Profile, Site, User
|
from .models import Key, Profile, Site, User
|
||||||
|
|
||||||
|
|
||||||
class SiteAdmin(admin.ModelAdmin):
|
class SiteAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "icon", "domain", "url_template")
|
list_display = ('name', 'icon', 'domain', 'url_template')
|
||||||
|
|
||||||
|
|
||||||
class PgpKeyInline(admin.TabularInline):
|
class KeyInline(admin.TabularInline):
|
||||||
model = PgpKey
|
model = Key
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,11 +19,10 @@ class ProfileInline(admin.TabularInline):
|
||||||
|
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
fieldsets = BaseUserAdmin.fieldsets + (
|
fieldsets = BaseUserAdmin.fieldsets + (
|
||||||
("Profile", {"fields": ("avatar", "xmpp", "note")}),
|
('Profile', {'fields': ('avatar', 'xmpp', 'note')}),
|
||||||
("Nostr", {"fields": ("nostr_key", "nostr_relays")}),
|
|
||||||
)
|
)
|
||||||
inlines = (
|
inlines = (
|
||||||
PgpKeyInline,
|
KeyInline,
|
||||||
ProfileInline,
|
ProfileInline,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class UsersConfig(AppConfig):
|
class UsersConfig(AppConfig):
|
||||||
name = "users"
|
name = 'users'
|
||||||
verbose_name = "Users and Profiles"
|
verbose_name = 'Users and Profiles'
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue