Compare commits
116 commits
Author | SHA1 | Date | |
---|---|---|---|
5348dc9f82 | |||
e36ad27d49 | |||
d21d4bda83 | |||
8d8aa4749b | |||
3baf75e59e | |||
880b899e81 | |||
6061d6f600 | |||
a680a6501c | |||
625b5d963a | |||
9d11cc7576 | |||
c49e17db90 | |||
7696ff45db | |||
731f177d18 | |||
0061111ad8 | |||
6b53c00d7c | |||
1490a95735 | |||
c398b0d3f4 | |||
95cca433bc | |||
4f081c8d34 | |||
8386f77d72 | |||
03956637be | |||
60bdaa27a0 | |||
a6fa7ebb3a | |||
d0bd6c1231 | |||
960e64963f | |||
0b1a548ee4 | |||
04bd6dd35d | |||
2e7d12b3e6 | |||
cd990e4e2f | |||
fe187da491 | |||
636b470001 | |||
e5cf94d488 | |||
c5458c2d06 | |||
7af8636687 | |||
5ac46dad63 | |||
d4c814c79a | |||
db0d6e28a3 | |||
2f8d62649e | |||
683adc1b46 | |||
cfeb206154 | |||
c5c0f4258b | |||
73addc2f75 | |||
0ca50252dd | |||
8d79be07da | |||
37d5a7a20d | |||
76496e7169 | |||
7fcc3c8788 | |||
4436db7d83 | |||
d017c642eb | |||
7c5f311af9 | |||
73155f399b | |||
e540f7b784 | |||
0e8f816d0e | |||
1bf0d8478a | |||
594947852f | |||
b318ed5b06 | |||
012aed42b1 | |||
5c10bafb7d | |||
e660221265 | |||
e23ca7d215 | |||
95b02269bb | |||
ce07ba8cdc | |||
17e5c2c1b4 | |||
4fd2ff826a | |||
6efcc450a3 | |||
dc7442cfb6 | |||
9c708b8c89 | |||
40f0bd858b | |||
639e1ec9c6 | |||
a35072bbc3 | |||
da5ca5edea | |||
1e4df2d1b5 | |||
d68dda85ad | |||
065619772e | |||
7d17a92793 | |||
1d4be082cf | |||
2d643b48c6 | |||
bab7097fa3 | |||
fa8419976d | |||
427dcde672 | |||
580c61e924 | |||
6c9b6eb061 | |||
6d7b5db482 | |||
8a0c24a9b5 | |||
c8e0b9c5fb | |||
556329d5fa | |||
ac22c826cb | |||
0adc7a0d5e | |||
f7d7936499 | |||
c8faa30724 | |||
0d1d102f47 | |||
cf0264b5a6 | |||
6054accc54 | |||
778a9c870d | |||
dee64f130e | |||
bc8d7923b4 | |||
dec5ef153b | |||
5cf566251a | |||
7edc5d0165 | |||
35ced9a451 | |||
446029ce84 | |||
bb91d3c6b6 | |||
b32412f4fd | |||
ce0bf28725 | |||
77816b6c5d | |||
b145f4ada9 | |||
e4aa5c6e6e | |||
fa66fbbf1e | |||
bc433f235f | |||
2a38c8d21b | |||
4bc7fde36b | |||
5042f3bda7 | |||
fca5b3259d | |||
ee12c15d1c | |||
741c2eb234 | |||
9c843ee145 |
161 changed files with 3915 additions and 2763 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# Created by https://www.gitignore.io/api/django
|
# Created by https://www.gitignore.io/api/django
|
||||||
|
|
||||||
### Django ###
|
### Django ###
|
||||||
|
@ -15,7 +14,10 @@ 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
|
||||||
/.mypy_cache
|
/.mypy_cache
|
||||||
/.pytest_cache
|
/.pytest_cache
|
||||||
/static
|
/static
|
||||||
node_modules
|
/node_modules
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
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 = git://github.com/ntpeters/base16-materialtheme-scheme.git
|
url = https://github.com/ntpeters/base16-materialtheme-scheme
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v1.3.0
|
|
||||||
hooks:
|
|
||||||
- id: autopep8-wrapper
|
|
||||||
- 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]
|
|
|
@ -1,3 +0,0 @@
|
||||||
requirements:
|
|
||||||
- Pipfile
|
|
||||||
- Pipfile.lock
|
|
16
.travis.yml
16
.travis.yml
|
@ -1,16 +0,0 @@
|
||||||
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
|
|
|
@ -1,4 +0,0 @@
|
||||||
# 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 - 2018 Danielle McLean
|
Copyright (c) 2017 - 2024 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
|
||||||
|
|
57
Pipfile
57
Pipfile
|
@ -1,57 +0,0 @@
|
||||||
[[source]]
|
|
||||||
url = "https://pypi.python.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
name = "pypi"
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = '3.6'
|
|
||||||
|
|
||||||
[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 = "*"
|
|
||||||
python-jose = "*"
|
|
||||||
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 = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
ptpython = "*"
|
|
||||||
pytest-django = "*"
|
|
||||||
werkzeug = "*"
|
|
||||||
watchdog = "*"
|
|
||||||
mypy = "*"
|
|
964
Pipfile.lock
generated
964
Pipfile.lock
generated
|
@ -1,964 +0,0 @@
|
||||||
{
|
|
||||||
"_meta": {
|
|
||||||
"hash": {
|
|
||||||
"sha256": "176c601737f4eb5da6b8689846e05d8df8b5b9ef25706fba98ffac6296e3d1d2"
|
|
||||||
},
|
|
||||||
"pipfile-spec": 6,
|
|
||||||
"requires": {
|
|
||||||
"python_version": "3.6"
|
|
||||||
},
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"name": "pypi",
|
|
||||||
"url": "https://pypi.python.org/simple",
|
|
||||||
"verify_ssl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"default": {
|
|
||||||
"accept-types": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9ae86512bf3a3eaad6a2793617a34eb15b384593e6c28697bef9b15ac237017a"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.3.0"
|
|
||||||
},
|
|
||||||
"argon2-cffi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:05dd15949be3a7d9f65807fe58fad70526023a319747054bb89da209c4071a33",
|
|
||||||
"sha256:07480018d77f4c7447924e6c44c5ba1789a918413fe3efaa391a097958bbd9f6",
|
|
||||||
"sha256:10e702dbd98a2148d22de9524a605021bdc55d05304beb90ea801ba58c4a4f1e",
|
|
||||||
"sha256:131effd5eabbe08649bc672b5d602fd6e2772b03cfec2ddb2795f9d9babe3fba",
|
|
||||||
"sha256:3f3b48b4802e98bb9692d72108ecad2fecea969c254c17660b70ce5730bbe4a6",
|
|
||||||
"sha256:4c510232a96e991079a743a9310d3c9a014856cdbca644fccc496db2a1ff0e17",
|
|
||||||
"sha256:5f1099b0f5ee4a7148bbd323503983aa4387ab16769ff9b5c51d26f6b0f1719e",
|
|
||||||
"sha256:67452b1f10e873ececcea657c25d063e4bb4007e115227a53157369de5848992",
|
|
||||||
"sha256:77a3d50e6325df79499e1220b7c38adbd30588c2f6d7c2d764fddb2d3b02e650",
|
|
||||||
"sha256:7e4b75611b73f53012117ad21cdde7a17b32d1e99ff6799f22d827eb83a2a59b",
|
|
||||||
"sha256:7f4b6d7c38258e76c1db293a6cf55b7e31701927fc773c5108e57578c7f8e09a",
|
|
||||||
"sha256:82db759b8a495aaed51aec4762b0f44e5e7ad80256e8baf512ae70cdb3b28c50",
|
|
||||||
"sha256:92b3f8f93b19081d520d911f1ce5902693edeeab2181c08aa0bb4130adba51aa",
|
|
||||||
"sha256:93f631fa567dbf948f26874476c9e9afb51e0a835372bf1a319df0c5aa071bfb",
|
|
||||||
"sha256:9befaa6d9798d9771b8176174ba82160beaf1dcdbcc63cd2dc5212f723e5e2a3",
|
|
||||||
"sha256:a14e6d99787a2972d3802615911770fcba9c904401fb0dfb60bdeb250b4c5110",
|
|
||||||
"sha256:c60764fe7f62cc52a74f326e366c60f7aa33a1586c8d02107394a01ae9db6e91",
|
|
||||||
"sha256:cba2c8c539bed691513ae1bcd5a7da632d2aa2410d8b8ebdf56026eac7e2193f",
|
|
||||||
"sha256:d79c918cf8bf981cd23b43a1a547cd1eececb77f3607ba9fa7c0ec01bf1f05a5",
|
|
||||||
"sha256:dc3028ec541146924e3c45973b458a7acf390b9e9ee0b64a13ac0853109a69bc",
|
|
||||||
"sha256:eb3fcb55224a47b8d50830561977c64761eaad9e349af0b2241eab089af44a14",
|
|
||||||
"sha256:f732ca584e81491cc11e3d12e18cbd8c63e137b3f461f378426a6fdaaef47fb0",
|
|
||||||
"sha256:fcd5681388d1f18e4a7ee3ff7a9b68650bc04db044b5a0a832728cbce182806d"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==18.1.0"
|
|
||||||
},
|
|
||||||
"beautifulsoup4": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76",
|
|
||||||
"sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11",
|
|
||||||
"sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89"
|
|
||||||
],
|
|
||||||
"version": "==4.6.0"
|
|
||||||
},
|
|
||||||
"bleach": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:b8fa79e91f96c2c2cd9fd1f9eda906efb1b88b483048978ba62fef680e962b34",
|
|
||||||
"sha256:eb7386f632349d10d9ce9d4a838b134d4731571851149f9cc2c05a9a837a9a44"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.1.3"
|
|
||||||
},
|
|
||||||
"cachecontrol": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:cef77effdf51b43178f6a2d3b787e3734f98ade253fa3187f3bb7315aaa42ff7"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.12.5"
|
|
||||||
},
|
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
|
||||||
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
|
||||||
],
|
|
||||||
"version": "==2018.4.16"
|
|
||||||
},
|
|
||||||
"cffi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
|
|
||||||
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
|
|
||||||
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
|
|
||||||
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
|
|
||||||
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
|
|
||||||
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
|
|
||||||
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
|
|
||||||
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
|
|
||||||
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
|
|
||||||
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
|
|
||||||
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
|
|
||||||
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
|
|
||||||
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
|
|
||||||
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
|
|
||||||
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
|
|
||||||
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
|
|
||||||
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
|
|
||||||
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
|
|
||||||
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
|
|
||||||
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
|
|
||||||
"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:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
|
||||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
|
||||||
],
|
|
||||||
"version": "==6.7"
|
|
||||||
},
|
|
||||||
"django": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3eb25c99df1523446ec2dc1b00e25eb2ecbdf42c9d8b0b8b32a204a8db9011f8",
|
|
||||||
"sha256:69ff89fa3c3a8337015478a1a0744f52a9fef5d12c1efa01a01f99bcce9bf10c"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.0.6"
|
|
||||||
},
|
|
||||||
"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:44dd65e30a3f11519852d5f5e50556c0f88cabb5720a2fd3637621952048abef",
|
|
||||||
"sha256:cf7b4c0b368139a090da2b0b45741bdd28b54daa0cb2b83ef801021c8eb2c050"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.4.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:31aa6453a5c504ce196ba9ae3bacbe0557cadf7ae89e25431b90bf206febd3b3"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.2.1"
|
|
||||||
},
|
|
||||||
"django-cors-headers": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0e9532628b3aa8806442d4d0b15e56112e6cfbef3735e13401935c98b842a2b4",
|
|
||||||
"sha256:c7ec4816ec49416517b84f317499d1519db62125471922ab78d670474ed9b987"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.2.0"
|
|
||||||
},
|
|
||||||
"django-debug-toolbar": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:4af2a4e1e932dadbda197b18585962d4fc20172b4e5a479490bc659fe998864d",
|
|
||||||
"sha256:d9ea75659f76d8f1e3eb8f390b47fc5bad0908d949c34a8a3c4c87978eb40a0f"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.9.1"
|
|
||||||
},
|
|
||||||
"django-extensions": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3be3debf53c77ca795bdf713726c923aa3c3f895e1a42e2e31a68c1a562346a4",
|
|
||||||
"sha256:94bfac99eb262c5ac27e53eda96925e2e53fe0b331af7dde37012d07639a649c"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.0.7"
|
|
||||||
},
|
|
||||||
"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:0016baa0f11544aa3a709d1b4fca13d607397ae41025bdc4fdb8a7e80a39973c",
|
|
||||||
"sha256:fd9e787779c053ba77e47a6907539c01e8db41b09f99722793317ed9a4183b32"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.4.3"
|
|
||||||
},
|
|
||||||
"django-otp-agents": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:4ca8fae30418e0a813840cee5068d2fb96e3759787a5820d54921b90c7beaa7a",
|
|
||||||
"sha256:8d9f26d5a186b059251bd03e1ab509b5861a678e463c49de9b0766080b2c16a5"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.3.0"
|
|
||||||
},
|
|
||||||
"django-push": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9d73a27f147ea46f5e92d6ab36c19640b11214b43b378693c8961aaf8bea5b60",
|
|
||||||
"sha256:d5442fcb6d8254a7e837383ce766a72e8fb921f3bcfc2355440c2da8fbcf07b4"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.1"
|
|
||||||
},
|
|
||||||
"django-redis": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:15b47faef6aefaa3f47135a2aeb67372da300e4a4cf06809c66ab392686a2155",
|
|
||||||
"sha256:a90343c33a816073b735f0bed878eaeec4f83b75fcc0dce2432189b8ea130424"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==4.9.0"
|
|
||||||
},
|
|
||||||
"django-rq": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:413df6e5789775b287b4b187ea09ff27f5f1d8205e999650ba9e52c34d58d252",
|
|
||||||
"sha256:71a604d4bfc18029c2f64da86bfadb803143f5784b3a340e3767202ced93245a"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.1.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"
|
|
||||||
},
|
|
||||||
"ecdsa": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c",
|
|
||||||
"sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa"
|
|
||||||
],
|
|
||||||
"version": "==0.13"
|
|
||||||
},
|
|
||||||
"future": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
|
|
||||||
],
|
|
||||||
"version": "==0.16.0"
|
|
||||||
},
|
|
||||||
"gevent": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:01ee9787d0a2182c0d56026d3923f73e6879835b1a85d4f996d00d09f1ecab20",
|
|
||||||
"sha256:0bae07cfd8a5a027b8e8a94d74e875d10809f2cc05bde84f31fd1748e4d43657",
|
|
||||||
"sha256:42b667080f7ab662d9c13d2e4d03289ee807d7926af9917685c844e481141563",
|
|
||||||
"sha256:4b668ea8a3e9e348aec21cd43e6fab63c5b50f0c6d57fa8b4cfb6cf9c72953d1",
|
|
||||||
"sha256:5756aabe722e158eff7a34124163f4c4f3dff01d078d00365ca21b380381906d",
|
|
||||||
"sha256:59465c7bce7671834f58b44ef62cd8626f1557a0e7e3de44a3b596056f8adc73",
|
|
||||||
"sha256:6a28d9e375df29200a5e0503f899a45b902cc0e40f4e4de1471773d87c43607a",
|
|
||||||
"sha256:73e50dccb4a2787002867ea59f40f5e0a5080f841d003f7660794bde216187c9",
|
|
||||||
"sha256:795006da15d13227d811c09999d37acc92c43c27341a2c9ae96094135ad56908",
|
|
||||||
"sha256:8957d025e3c361b56b268f35e59777013827848e166d8c0219e47d3b80e2f1ac",
|
|
||||||
"sha256:8c461a5897e520dd5ec4de725dae030d8c0ac74d07c704aa1fb3b6453315b865",
|
|
||||||
"sha256:9bff994b9eb5fb2652af74dc8adb09914f3752db25381ccb2c75e2fa45e4f522",
|
|
||||||
"sha256:a9fa2de95f203982135aaa80979270df83a195a38a152103cd3723b185e407ff",
|
|
||||||
"sha256:ac0d572a48275495db9513d7bb5d41ccf4f820b7df4594e704fa5891de0d86c4",
|
|
||||||
"sha256:ba94d6b3998fbb2828fb9d585e409ec46d958bafd7e3f185a14146c3615231d7",
|
|
||||||
"sha256:bb3bd3aaec9cc51f6fcbb2e7ac2063a1a0160159fe0fee5e978ebceef4ed35d2",
|
|
||||||
"sha256:cb2f2810a4a1de40cec38d18d3255eb6f4b0778ed3b4dcce03b0b7d462f1f8d4",
|
|
||||||
"sha256:df7794dc0117215a236b7efe83850dbf6ff90c34c5d0b4da01843f89efabc3d4",
|
|
||||||
"sha256:e66bac19c88faad0884da2e7b95ef90053927e552e3e50046145014d54dd9d01",
|
|
||||||
"sha256:f04bbc9b64696775a2367b1d07c124d1ae1d1d70bdd4523db28f81de126d22e6",
|
|
||||||
"sha256:fd6fdfa71cf4a21d33c32df2b261b18aaf41128e6db29c1d27c5fa5e0a5459d5"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.3.3"
|
|
||||||
},
|
|
||||||
"greenlet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:09ef2636ea35782364c830f07127d6c7a70542b178268714a9a9ba16318e7e8b",
|
|
||||||
"sha256:0fef83d43bf87a5196c91e73cb9772f945a4caaff91242766c5916d1dd1381e4",
|
|
||||||
"sha256:1b7df09c6598f5cfb40f843ade14ed1eb40596e75cd79b6fa2efc750ba01bb01",
|
|
||||||
"sha256:1fff21a2da5f9e03ddc5bd99131a6b8edf3d7f9d6bc29ba21784323d17806ed7",
|
|
||||||
"sha256:42118bf608e0288e35304b449a2d87e2ba77d1e373e8aa221ccdea073de026fa",
|
|
||||||
"sha256:50643fd6d54fd919f9a0a577c5f7b71f5d21f0959ab48767bd4bb73ae0839500",
|
|
||||||
"sha256:58798b5d30054bb4f6cf0f712f08e6092df23a718b69000786634a265e8911a9",
|
|
||||||
"sha256:5b49b3049697aeae17ef7bf21267e69972d9e04917658b4e788986ea5cc518e8",
|
|
||||||
"sha256:75c413551a436b462d5929255b6dc9c0c3c2b25cbeaee5271a56c7fda8ca49c0",
|
|
||||||
"sha256:769b740aeebd584cd59232be84fdcaf6270b8adc356596cdea5b2152c82caaac",
|
|
||||||
"sha256:ad2383d39f13534f3ca5c48fe1fc0975676846dc39c2cece78c0f1f9891418e0",
|
|
||||||
"sha256:b417bb7ff680d43e7bd7a13e2e08956fa6acb11fd432f74c97b7664f8bdb6ec1",
|
|
||||||
"sha256:b6ef0cabaf5a6ecb5ac122e689d25ba12433a90c7b067b12e5f28bdb7fb78254",
|
|
||||||
"sha256:c2de19c88bdb0366c976cc125dca1002ec1b346989d59524178adfd395e62421",
|
|
||||||
"sha256:c7b04a6dc74087b1598de8d713198de4718fa30ec6cbb84959b26426c198e041",
|
|
||||||
"sha256:f8f2a0ae8de0b49c7b5b2daca4f150fdd9c1173e854df2cce3b04123244f9f45",
|
|
||||||
"sha256:fcfadaf4bf68a27e5dc2f42cbb2f4b4ceea9f05d1d0b8f7787e640bed2801634"
|
|
||||||
],
|
|
||||||
"markers": "platform_python_implementation == 'CPython'",
|
|
||||||
"version": "==0.4.13"
|
|
||||||
},
|
|
||||||
"gunicorn": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7ef2b828b335ed58e3b64ffa84caceb0a7dd7c5ca12f217241350dec36a1d5dc",
|
|
||||||
"sha256:bc59005979efb6d2dd7d5ba72d99f8a8422862ad17ff3a16e900684630dd2a10"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==19.8.1"
|
|
||||||
},
|
|
||||||
"hiredis": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ca958e13128e49674aa4a96f02746f5de5973f39b57297b84d59fd44d314d5b5"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.2.0"
|
|
||||||
},
|
|
||||||
"html5lib": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
|
|
||||||
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
|
|
||||||
],
|
|
||||||
"version": "==1.0.1"
|
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
|
||||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
|
||||||
],
|
|
||||||
"version": "==2.6"
|
|
||||||
},
|
|
||||||
"isodate": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
|
|
||||||
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
|
|
||||||
],
|
|
||||||
"version": "==0.6.0"
|
|
||||||
},
|
|
||||||
"jinja2": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
|
||||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.10"
|
|
||||||
},
|
|
||||||
"lxml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:01c45df6d90497c20aa2a07789a41941f9a1029faa30bf725fc7f6d515b1afe9",
|
|
||||||
"sha256:0c9fef4f8d444e337df96c54544aeb85b7215b2ed7483bb6c35de97ac99f1bcd",
|
|
||||||
"sha256:0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b",
|
|
||||||
"sha256:0e7996e9b46b4d8b4ac1c329a00e2d10edcd8380b95d2a676fccabf4c1dd0512",
|
|
||||||
"sha256:1858b1933d483ec5727549d3fe166eeb54229fbd6a9d3d7ea26d2c8a28048058",
|
|
||||||
"sha256:1b164bba1320b14905dcff77da10d5ce9c411ac4acc4fb4ed9a2a4d10fae38c9",
|
|
||||||
"sha256:1b46f37927fa6cd1f3fe34b54f1a23bd5bea1d905657289e08e1297069a1a597",
|
|
||||||
"sha256:231047b05907315ae9a9b6925751f9fd2c479cf7b100fff62485a25e382ca0d4",
|
|
||||||
"sha256:28f0c6652c1b130f1e576b60532f84b19379485eb8da6185c29bd8c9c9bc97bf",
|
|
||||||
"sha256:34d49d0f72dd82b9530322c48b70ac78cca0911275da741c3b1d2f3603c5f295",
|
|
||||||
"sha256:3682a17fbf72d56d7e46db2e80ca23850b79c28cfe75dcd9b82f58808f730909",
|
|
||||||
"sha256:3cf2830b9a6ad7f6e965fa53a768d4d2372a7856f20ffa6ce43d2fe9c0d34b19",
|
|
||||||
"sha256:5b653c9379ce29ce271fbe1010c5396670f018e78b643e21beefbb3dc6d291de",
|
|
||||||
"sha256:65a272821d5d8194358d6b46f3ca727fa56a6b63981606eac737c86d27309cdd",
|
|
||||||
"sha256:691f2cd97cf026c611df1ea5055755eec7f878f2d4f4330dc8686583de6fc5fd",
|
|
||||||
"sha256:6b6379495d3baacf7ed755ac68547c8dff6ce5d37bf370f0b7678888dc1283f9",
|
|
||||||
"sha256:75322a531504d4f383264391d89993a42e286da8821ddc5ac315e57305cb84f0",
|
|
||||||
"sha256:7f457cbda964257f443bac861d3a36732dcba8183149e7818ee2fb7c86901b94",
|
|
||||||
"sha256:7ff1fc76d8804e0f870c343a72007ff587090c218b0f92d8ee784ac2b6eaf5b9",
|
|
||||||
"sha256:8523fbde9c2216f3f2b950cb01ebe52e785eaa8a07ffeb456dd3576ca1b4fb9b",
|
|
||||||
"sha256:8f37627f16e026523fca326f1b5c9a43534862fede6c3e99c2ba6a776d75c1ab",
|
|
||||||
"sha256:a7182ea298cc3555ea56ffbb0748fe0d5e0d81451e2bc16d7f4645cd01b1ca70",
|
|
||||||
"sha256:abbd2fb4a5a04c11b5e04eb146659a0cf67bb237dd3d7ca3b9994d3a9f826e55",
|
|
||||||
"sha256:accc9f6b77bed0a6f267b4fae120f6008a951193d548cdbe9b61fc98a08b1cf8",
|
|
||||||
"sha256:bd88c8ce0d1504fdfd96a35911dd4f3edfb2e560d7cfdb5a3d09aa571ae5fbae",
|
|
||||||
"sha256:c557ad647facb3c0027a9d0af58853f905e85a0a2f04dcb73f8e665272fcdc3a",
|
|
||||||
"sha256:defabb7fbb99f9f7b3e0b24b286a46855caef4776495211b066e9e6592d12b04",
|
|
||||||
"sha256:e2629cdbcad82b83922a3488937632a4983ecc0fed3e5cfbf430d069382eeb9b"
|
|
||||||
],
|
|
||||||
"version": "==4.2.1"
|
|
||||||
},
|
|
||||||
"markdown": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f",
|
|
||||||
"sha256:a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.6.11"
|
|
||||||
},
|
|
||||||
"markupsafe": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
|
||||||
],
|
|
||||||
"version": "==1.0"
|
|
||||||
},
|
|
||||||
"mf2py": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9231bc1317dd5d01973b78a60e52ecd76dd84089c132540e799e4a62aeff3436",
|
|
||||||
"sha256:bdc3f65ab8a1319b763012dfa8def6ce33214f19e1c53fc2844ed9e7afaf1413"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"mf2util": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:efb8ea1a275f16396993a3fbe32331b74a8f6985d3f7f47503641cf522f1f614"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.5.0"
|
|
||||||
},
|
|
||||||
"msgpack": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc",
|
|
||||||
"sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b",
|
|
||||||
"sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3",
|
|
||||||
"sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0",
|
|
||||||
"sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee",
|
|
||||||
"sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42",
|
|
||||||
"sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c",
|
|
||||||
"sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f",
|
|
||||||
"sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172",
|
|
||||||
"sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4",
|
|
||||||
"sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4",
|
|
||||||
"sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf",
|
|
||||||
"sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00",
|
|
||||||
"sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0",
|
|
||||||
"sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.5.6"
|
|
||||||
},
|
|
||||||
"packaging": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
|
|
||||||
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
|
|
||||||
],
|
|
||||||
"version": "==17.1"
|
|
||||||
},
|
|
||||||
"pillow": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:00633bc2ec40313f4daf351855e506d296ec3c553f21b66720d0f1225ca84c6f",
|
|
||||||
"sha256:03514478db61b034fc5d38b9bf060f994e5916776e93f02e59732a8270069c61",
|
|
||||||
"sha256:040144ba422216aecf7577484865ade90e1a475f867301c48bf9fbd7579efd76",
|
|
||||||
"sha256:16246261ff22368e5e32ad74d5ef40403ab6895171a7fc6d34f6c17cfc0f1943",
|
|
||||||
"sha256:1cb38df69362af35c14d4a50123b63c7ff18ec9a6d4d5da629a6f19d05e16ba8",
|
|
||||||
"sha256:2400e122f7b21d9801798207e424cbe1f716cee7314cd0c8963fdb6fc564b5fb",
|
|
||||||
"sha256:2ee6364b270b56a49e8b8a51488e847ab130adc1220c171bed6818c0d4742455",
|
|
||||||
"sha256:3b4560c3891b05022c464b09121bd507c477505a4e19d703e1027a3a7c68d896",
|
|
||||||
"sha256:41374a6afb3f44794410dab54a0d7175e6209a5a02d407119c81083f1a4c1841",
|
|
||||||
"sha256:438a3faf5f702c8d0f80b9f9f9b8382cfa048ca6a0d64ef71b86b563b0ee0359",
|
|
||||||
"sha256:472a124c640bde4d5468f6991c9fa7e30b723d84ac4195a77c6ab6aea30f2b9c",
|
|
||||||
"sha256:4d32c8e3623a61d6e29ccd024066cd1ba556555abfb4cd714155020e00107e3f",
|
|
||||||
"sha256:4d8077fd649ac40a5c4165f2c22fa2a4ad18c668e271ecb2f9d849d1017a9313",
|
|
||||||
"sha256:62ec7ae98357fcd46002c110bb7cad15fce532776f0cbe7ca1d44c49b837d49d",
|
|
||||||
"sha256:6c7cab6a05351cf61e469937c49dbf3cdf5ffb3eeac71f8d22dc9be3507598d8",
|
|
||||||
"sha256:6eca36905444c4b91fe61f1b9933a47a30480738a1dd26501ff67d94fc2bc112",
|
|
||||||
"sha256:74e2ebfd19c16c28ad43b8a28ff73b904ed382ea4875188838541751986e8c9a",
|
|
||||||
"sha256:7673e7473a13107059377c96c563aa36f73184c29d2926882e0a0210b779a1e7",
|
|
||||||
"sha256:81762cf5fca9a82b53b7b2d0e6b420e0f3b06167b97678c81d00470daa622d58",
|
|
||||||
"sha256:8554bbeb4218d9cfb1917c69e6f2d2ad0be9b18a775d2162547edf992e1f5f1f",
|
|
||||||
"sha256:9b66e968da9c4393f5795285528bc862c7b97b91251f31a08004a3c626d18114",
|
|
||||||
"sha256:a00edb2dec0035e98ac3ec768086f0b06dfabb4ad308592ede364ef573692f55",
|
|
||||||
"sha256:b48401752496757e95304a46213c3155bc911ac884bed2e9b275ce1c1df3e293",
|
|
||||||
"sha256:b6cf18f9e653a8077522bb3aa753a776b117e3e0cc872c25811cfdf1459491c2",
|
|
||||||
"sha256:bb8adab1877e9213385cbb1adc297ed8337e01872c42a30cfaa66ff8c422779c",
|
|
||||||
"sha256:c8a4b39ba380b57a31a4b5449a9d257b1302d8bc4799767e645dcee25725efe1",
|
|
||||||
"sha256:cee9bc75bff455d317b6947081df0824a8f118de2786dc3d74a3503fd631f4ef",
|
|
||||||
"sha256:d0dc1313dff48af64517cbbd85e046d6b477fbe5e9d69712801f024dcb08c62b",
|
|
||||||
"sha256:d5bf527ed83617edd1855a5c923eeeaf68bcb9ac0ceb28e3f19b575b3a424984",
|
|
||||||
"sha256:df5863a21f91de5ecdf7d32a32f406dd9867ebb35d41033b8bd9607a21887599",
|
|
||||||
"sha256:e39142332541ed2884c257495504858b22c078a5d781059b07aba4c3a80d7551",
|
|
||||||
"sha256:e52e8f675ba0b2b417fa98579e7286a41a8e23871f17f4793772f5aa884fea79",
|
|
||||||
"sha256:e6dd55d5d94b9e36929325dd0c9ab85bfde84a5fc35947c334c32af1af668944",
|
|
||||||
"sha256:e87cc1acbebf263f308a8494272c2d42016aa33c32bf14d209c81e1f65e11868",
|
|
||||||
"sha256:ea0091cd4100519cedfeea2c659f52291f535ac6725e2368bcf59e874f270efa",
|
|
||||||
"sha256:eeb247f4f4d962942b3b555530b0c63b77473c7bfe475e51c6b75b7344b49ce3",
|
|
||||||
"sha256:f0d4433adce6075efd24fc0285135248b0b50f5a58129c7e552030e04fe45c7f",
|
|
||||||
"sha256:f1f3bd92f8e12dc22884935a73c9f94c4d9bd0d34410c456540713d6b7832b8c",
|
|
||||||
"sha256:f42a87cbf50e905f49f053c0b1fb86c911c730624022bf44c8857244fc4cdaca",
|
|
||||||
"sha256:f5f302db65e2e0ae96e26670818157640d3ca83a3054c290eff3631598dcf819",
|
|
||||||
"sha256:f7634d534662bbb08976db801ba27a112aee23e597eeaf09267b4575341e45bf",
|
|
||||||
"sha256:fdd374c02e8bb2d6468a85be50ea66e1c4ef9e809974c30d8576728473a6ed03",
|
|
||||||
"sha256:fe6931db24716a0845bd8c8915bd096b77c2a7043e6fc59ae9ca364fe816f08b"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==5.1.0"
|
|
||||||
},
|
|
||||||
"psycopg2-binary": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:02eb674e3d5810e19b4d5d00720b17130e182da1ba259dda608aaf33d787347d",
|
|
||||||
"sha256:3a14baeabcebd4662f12f4bff03e0574a2369a2e41baf829e6fb4a24c95cf88b",
|
|
||||||
"sha256:436a503eda41f6adb08f292f40a3784fce0a5f351b6ae7b19a911904db53af93",
|
|
||||||
"sha256:465ff1d427ed42c31e456dbbd9edab3552be18a0edaef7450c5b3e6fee745052",
|
|
||||||
"sha256:4a1a5ea2fa4b53191637b162873a82822d92a85a08beefe28296b8eb5cf2fea5",
|
|
||||||
"sha256:4a4f23a08fbccbe40ecdb5384d807bcb469ea71dd87e6be2e80b036b8e6d47df",
|
|
||||||
"sha256:77a2fc622a1f2d08a707673c9be5769d521f03d867d305f172bb417fa7882754",
|
|
||||||
"sha256:8014c06a9ed7b78ba81beff3ae71acd78c212390f8ed839e9ce22735880bd5b4",
|
|
||||||
"sha256:83af04029bcb4b56c852e5876fef71340dcb465fa44fc99f80bac72e10fb0b74",
|
|
||||||
"sha256:86c0d2587f56776f25d52cca8e275adf495c8e01933fbfc2ca23b124610ab761",
|
|
||||||
"sha256:9305d7cbc802aaefac5c75a3df725f2654797369f32b18d4d0adb382dfab6c09",
|
|
||||||
"sha256:9b5ddbed85ec73293695d7116589d956ef0dd3fcf7bf3b2a3bc1e8e54c1d543a",
|
|
||||||
"sha256:a3d2cc0cb0b988dbfd0d11f7fac34058b25a6ce533ed5b8e88d6cb315e77d54a",
|
|
||||||
"sha256:ab1db8f3e96570d9f7ebc45133ce2574804b2280499baade178e163d022107b5",
|
|
||||||
"sha256:b039f51bca1ddd70234cc3f84f94f42ad43861b931bdfb497f887c60c39a6565",
|
|
||||||
"sha256:b287ddf4cafcfb632974907d1e7862119e36bb758228bdb07dd247553e4cdfc0",
|
|
||||||
"sha256:b6b2b26590304d97ef2af28d153ee99ace6fe0806934f4618edfc87216c77f91",
|
|
||||||
"sha256:c4c6004d410c77bfa5389ae9485498ce32805447a67afbfe8db0d247a5c88fa1",
|
|
||||||
"sha256:c606bff0978ee4858d86d40f6b6ab0c4cac4474f627bd054683dc03a4fc1a366",
|
|
||||||
"sha256:c8220c521a408b41c4f14036004a621ed0d965941286b978cd2ea2623fabd755",
|
|
||||||
"sha256:cb07184a4bfad304831f0a88b1c13fbd8cf9fcdf1f11e71c477dd6d7b1b078a0",
|
|
||||||
"sha256:cf3911fba0c47fc1313b5783183cda301032b14637a0b7a336766ae46998c7ee",
|
|
||||||
"sha256:d0972f062c73956332e9681dfdb133168618f0abfecc96e89f0205ac89cd454b",
|
|
||||||
"sha256:d1dd3eb8edd354083f5d27b968c5a17854c41347ba5a480b520be85ec1a8495c",
|
|
||||||
"sha256:d51c7ed810fce1e50464088c37cc8da05534de8afb12a732500827ebcc480081",
|
|
||||||
"sha256:d8940b5104588d6313315e037f0f5ed68d2e5f62ccc1c429d3cff11d2ba6de3f",
|
|
||||||
"sha256:de4f88f823037a71ea5ef3c1041d96b8a68d73343133edda684fd42f575bd9d7"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.7.4"
|
|
||||||
},
|
|
||||||
"pyasn1": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2f57960dc7a2820ea5a1782b872d974b639aa3b448ac6628d1ecc5d0fe3986f2",
|
|
||||||
"sha256:3651774ca1c9726307560792877db747ba5e8a844ea1a41feb7670b319800ab3",
|
|
||||||
"sha256:602fda674355b4701acd7741b2be5ac188056594bf1eecf690816d944e52905e",
|
|
||||||
"sha256:8fb265066eac1d3bb5015c6988981b009ccefd294008ff7973ed5f64335b0f2d",
|
|
||||||
"sha256:9334cb427609d2b1e195bb1e251f99636f817d7e3e1dffa150cb3365188fb992",
|
|
||||||
"sha256:9a15cc13ff6bf5ed29ac936ca941400be050dff19630d6cd1df3fb978ef4c5ad",
|
|
||||||
"sha256:a66dcda18dbf6e4663bde70eb30af3fc4fe1acb2d14c4867a861681887a5f9a2",
|
|
||||||
"sha256:ba77f1e8d7d58abc42bfeddd217b545fdab4c1eeb50fd37c2219810ad56303bf",
|
|
||||||
"sha256:cdc8eb2eaafb56de66786afa6809cd9db2df1b3b595dcb25aa5b9dc61189d40a",
|
|
||||||
"sha256:d01fbba900c80b42af5c3fe1a999acf61e27bf0e452e0f1ef4619065e57622da",
|
|
||||||
"sha256:f281bf11fe204f05859225ec2e9da7a7c140b65deccd8a4eb0bc75d0bd6949e0",
|
|
||||||
"sha256:fb81622d8f3509f0026b0683fe90fea27be7284d3826a5f2edf97f69151ab0fc"
|
|
||||||
],
|
|
||||||
"version": "==0.4.3"
|
|
||||||
},
|
|
||||||
"pycparser": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226"
|
|
||||||
],
|
|
||||||
"version": "==2.18"
|
|
||||||
},
|
|
||||||
"pyparsing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
|
|
||||||
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
|
|
||||||
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
|
|
||||||
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
|
|
||||||
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
|
|
||||||
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
|
|
||||||
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
|
|
||||||
],
|
|
||||||
"version": "==2.2.0"
|
|
||||||
},
|
|
||||||
"python-baseconv": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1b98b11d0d1c00bf1165d62b0d183c8d2d496ae5baaa0991c0d4ffef079772d6"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.2.1"
|
|
||||||
},
|
|
||||||
"python-jose": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e06dd2e5e9125da79b519ff2652b8c666d64a5ea228fcd9862e0b29a534ccc53",
|
|
||||||
"sha256:e8255fb3cc524c04f4c790547a6215468f2a32d3a866424175523359e69f3aeb"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.0.0"
|
|
||||||
},
|
|
||||||
"python-magic": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375",
|
|
||||||
"sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.4.15"
|
|
||||||
},
|
|
||||||
"python-slugify": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5dbb360b882b2dabe0471a1a92f604504d83c2a73c71f2098d004ab62e695534"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.2.5"
|
|
||||||
},
|
|
||||||
"pytz": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
|
||||||
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
|
||||||
],
|
|
||||||
"version": "==2018.4"
|
|
||||||
},
|
|
||||||
"pyup-django": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:f02242b4c7a8926bf9118054429dcaf84e5708a050548abcd3b2b9de4a7570b9",
|
|
||||||
"sha256:fe84cef39c41d5feb24e307d6c8a55454db50df4c6955fa6a890a42b6e58650e"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.4.0"
|
|
||||||
},
|
|
||||||
"pyyaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
|
||||||
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
|
||||||
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
|
||||||
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
|
||||||
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
|
||||||
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
|
||||||
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
|
||||||
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
|
||||||
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
|
||||||
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
|
||||||
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
|
||||||
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
|
||||||
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
|
||||||
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.12"
|
|
||||||
},
|
|
||||||
"qrcode": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf",
|
|
||||||
"sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==6.0"
|
|
||||||
},
|
|
||||||
"rcssmin": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
|
|
||||||
],
|
|
||||||
"version": "==1.0.6"
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb",
|
|
||||||
"sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f"
|
|
||||||
],
|
|
||||||
"version": "==2.10.6"
|
|
||||||
},
|
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
|
|
||||||
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
|
|
||||||
],
|
|
||||||
"version": "==2.18.4"
|
|
||||||
},
|
|
||||||
"rjsmin": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:dd9591aa73500b08b7db24367f8d32c6470021f39d5ab4e50c7c02e4401386f1"
|
|
||||||
],
|
|
||||||
"version": "==1.0.12"
|
|
||||||
},
|
|
||||||
"ronkyuu": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5aa77b39d301bc174ab99ba8a53954627771cb501651a12103c58f51b32e84bf",
|
|
||||||
"sha256:85b25fef7f5fb0c93afd5377ea35b5ff72b2458f926bafdf10f0c9a1e19cab10"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.6"
|
|
||||||
},
|
|
||||||
"rq": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:4f494d8554d45748b8fb91454a18e2293f305230eaec45b740c94a280aeaa682",
|
|
||||||
"sha256:f997d28c5e69aef38e9162572f78c424a7deff4cd00efae125085d4c1fbcc15b"
|
|
||||||
],
|
|
||||||
"version": "==0.11.0"
|
|
||||||
},
|
|
||||||
"rsa": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5",
|
|
||||||
"sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd"
|
|
||||||
],
|
|
||||||
"version": "==3.4.2"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
|
||||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
|
||||||
],
|
|
||||||
"version": "==1.11.0"
|
|
||||||
},
|
|
||||||
"sqlparse": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec",
|
|
||||||
"sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4"
|
|
||||||
],
|
|
||||||
"version": "==0.2.4"
|
|
||||||
},
|
|
||||||
"unidecode": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:72f49d3729f3d8f5799f710b97c1451c5163102e76d64d20e170aedbbd923582",
|
|
||||||
"sha256:8c33dd588e0c9bc22a76eaa0c715a5434851f726131bd44a6c26471746efabf5"
|
|
||||||
],
|
|
||||||
"version": "==1.0.22"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
|
||||||
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
|
||||||
],
|
|
||||||
"version": "==1.22"
|
|
||||||
},
|
|
||||||
"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:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
|
|
||||||
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
|
|
||||||
],
|
|
||||||
"version": "==1.1.5"
|
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
|
|
||||||
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
|
|
||||||
],
|
|
||||||
"version": "==18.1.0"
|
|
||||||
},
|
|
||||||
"docopt": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
|
||||||
],
|
|
||||||
"version": "==0.6.2"
|
|
||||||
},
|
|
||||||
"jedi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1972f694c6bc66a2fac8718299e2ab73011d653a6d8059790c3476d2353b99ad",
|
|
||||||
"sha256:5861f6dc0c16e024cbb0044999f9cf8013b292c05f287df06d3d991a87a4eb89"
|
|
||||||
],
|
|
||||||
"version": "==0.12.0"
|
|
||||||
},
|
|
||||||
"more-itertools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
|
|
||||||
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
|
|
||||||
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
|
|
||||||
],
|
|
||||||
"version": "==4.2.0"
|
|
||||||
},
|
|
||||||
"mypy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1b899802a89b67bb68f30d788bba49b61b1f28779436f06b75c03495f9d6ea5c",
|
|
||||||
"sha256:f472645347430282d62d1f97d12ccb8741f19f1572b7cf30b58280e4e0818739"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.610"
|
|
||||||
},
|
|
||||||
"parso": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:cdef26e8adc10d589f3ec4eb444bd0a29f3f1eb6d72a4292ab8afcb9d68976a6",
|
|
||||||
"sha256:f0604a40b96e062b0fd99cf134cc2d5cdf66939d0902f8267d938b0d5b26707f"
|
|
||||||
],
|
|
||||||
"version": "==0.2.1"
|
|
||||||
},
|
|
||||||
"pathtools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"
|
|
||||||
],
|
|
||||||
"version": "==0.1.2"
|
|
||||||
},
|
|
||||||
"pluggy": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
|
||||||
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
|
||||||
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
|
||||||
],
|
|
||||||
"version": "==0.6.0"
|
|
||||||
},
|
|
||||||
"prompt-toolkit": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381",
|
|
||||||
"sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4",
|
|
||||||
"sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917"
|
|
||||||
],
|
|
||||||
"version": "==1.0.15"
|
|
||||||
},
|
|
||||||
"ptpython": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:55d7cfad50a096f5922c4fdf8cea068a7ec9257418064b437c617ff2f120f81a",
|
|
||||||
"sha256:816da300f620fb88ba97c7962062c8c178d6693b1db19c184660156b1af91bdc",
|
|
||||||
"sha256:a78b27a85c5dbe9d89376e7f3aa70a9d8fa15cb45ee5f73a3cc3963b9b528ac1"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.41"
|
|
||||||
},
|
|
||||||
"py": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
|
||||||
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
|
||||||
],
|
|
||||||
"version": "==1.5.3"
|
|
||||||
},
|
|
||||||
"pygments": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
|
||||||
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
|
||||||
],
|
|
||||||
"version": "==2.2.0"
|
|
||||||
},
|
|
||||||
"pytest": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
|
|
||||||
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
|
|
||||||
],
|
|
||||||
"version": "==3.6.1"
|
|
||||||
},
|
|
||||||
"pytest-django": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:534505e0261cc566279032d9d887f844235342806fd63a6925689670fa1b29d7",
|
|
||||||
"sha256:7501942093db2250a32a4e36826edfc542347bb9b26c78ed0649cdcfd49e5789"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.2.1"
|
|
||||||
},
|
|
||||||
"pyyaml": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
|
||||||
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
|
||||||
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
|
||||||
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
|
||||||
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
|
||||||
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
|
||||||
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
|
||||||
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
|
||||||
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
|
||||||
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
|
||||||
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
|
||||||
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
|
||||||
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
|
||||||
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==3.12"
|
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
|
||||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
|
||||||
],
|
|
||||||
"version": "==1.11.0"
|
|
||||||
},
|
|
||||||
"typed-ast": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
|
|
||||||
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
|
|
||||||
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
|
|
||||||
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
|
|
||||||
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
|
|
||||||
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
|
|
||||||
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
|
|
||||||
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
|
|
||||||
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
|
|
||||||
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
|
|
||||||
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
|
|
||||||
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
|
|
||||||
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
|
|
||||||
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
|
|
||||||
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
|
|
||||||
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
|
|
||||||
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
|
|
||||||
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"watchdog": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.8.3"
|
|
||||||
},
|
|
||||||
"wcwidth": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
|
||||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
|
||||||
],
|
|
||||||
"version": "==0.1.7"
|
|
||||||
},
|
|
||||||
"werkzeug": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
|
||||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.14.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
default_app_config = 'entries.apps.EntriesConfig'
|
|
|
@ -8,12 +8,10 @@ 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 = (
|
inlines = (SyndicationInline,)
|
||||||
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"
|
||||||
|
|
34
entries/from_url.py
Normal file
34
entries/from_url.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import resolve, Resolver404
|
||||||
|
from micropub import error
|
||||||
|
|
||||||
|
from .models import Entry
|
||||||
|
|
||||||
|
|
||||||
|
def from_url(url: str) -> Entry:
|
||||||
|
domain = Site.objects.get_current().domain
|
||||||
|
if not url:
|
||||||
|
raise error.bad_req("url parameter required")
|
||||||
|
if "//" not in url:
|
||||||
|
url = "//" + url
|
||||||
|
parts = urlparse(url, scheme="https")
|
||||||
|
if parts.scheme not in ("http", "https") or parts.netloc != domain:
|
||||||
|
raise error.bad_req("url does not point to this site")
|
||||||
|
|
||||||
|
try:
|
||||||
|
match = resolve(parts.path)
|
||||||
|
except Resolver404:
|
||||||
|
raise error.bad_req("url does not point to a valid page on this site")
|
||||||
|
|
||||||
|
if match.view_name != "entries:entry":
|
||||||
|
raise error.bad_req("url does not point to an entry on this site")
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = Entry.objects.get(pk=match.kwargs["id"])
|
||||||
|
except Entry.DoesNotExist:
|
||||||
|
raise error.bad_req("url does not point to an existing entry")
|
||||||
|
|
||||||
|
return entry
|
15
entries/jinja2/entries/entry.html
Normal file
15
entries/jinja2/entries/entry.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'lemoncurry/layout.html' %}
|
||||||
|
{% block head %}
|
||||||
|
<link rel="shortlink" href="{{ entry.short_url }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" type="text/stylus" href="{{ static('entries/css/h-entry.styl') }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% import 'entries/h-entry.html' as h %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="entry">
|
||||||
|
{{ h.hEntry(entry, indent_width=8) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
55
entries/jinja2/entries/h-entry.html
Normal file
55
entries/jinja2/entries/h-entry.html
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{% macro hEntry(entry, indent_width) -%}
|
||||||
|
{%- set i = ' ' * indent_width -%}
|
||||||
|
<article class="h-entry media">
|
||||||
|
{{i}}<aside class="info">
|
||||||
|
{{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}}<span class="p-name sr-only">{{ entry.author.name }}</span>
|
||||||
|
{{i}}</a>
|
||||||
|
{{i}}<a class="u-uid u-url" href="{{ entry.url }}">
|
||||||
|
{{i}}<time class="dt-published media" datetime="{{ entry.published.isoformat() }}" title="{{ entry.published.isoformat() }}">
|
||||||
|
{{i}}<i class="fas fa-fw fa-calendar" aria-hidden="true"></i>
|
||||||
|
{{i}}<div class="media-body">{{ entry.published | ago }}</div>
|
||||||
|
{{i}}</time>
|
||||||
|
{{i}}</a>
|
||||||
|
{{i}}<time class="dt-updated media" datetime="{{ entry.updated.isoformat() }}" title="{{ entry.updated.isoformat() }}"{% if (entry.updated | ago) == (entry.published | ago) %} hidden{% endif %}>
|
||||||
|
{{i}}<i class="fas fa-fw fa-pencil-alt" aria-hidden="true"></i>
|
||||||
|
{{i}}<div class="media-body">{{ entry.updated | ago }}</div>
|
||||||
|
{{i}}</time>
|
||||||
|
{{i}}<a class="u-url media" href="{{ entry.short_url }}">
|
||||||
|
{{i}}<i class="fas fa-fw fa-link" aria-hidden="true"></i>
|
||||||
|
{{i}}<div class="media-body">{{ entry.short_url | friendly_url }}</div>
|
||||||
|
{{i}}</a>
|
||||||
|
{{i}}</aside>
|
||||||
|
|
||||||
|
{{i}}<div class="card media-body">
|
||||||
|
{% if entry.photo %}
|
||||||
|
{{i}}<img class="card-img-top u-photo" src="{{ entry.photo.url }}" />
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{{i}}<div class="card-body">
|
||||||
|
{% if entry.name %}
|
||||||
|
{{i}}<h4 class="card-title p-name">{{ entry.name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{{i}}<div class="e-content">
|
||||||
|
{{i}}{{ entry.content | markdown }}
|
||||||
|
{{i}}</div>
|
||||||
|
|
||||||
|
{% for c in entry.cats.all() %}
|
||||||
|
{{i}}<a class="p-category card-link" href="{{ c.url }}">
|
||||||
|
{{i}}<i class="fas fa-paw" aria-hidden="true"></i>
|
||||||
|
{{i}}{{ c.name }}
|
||||||
|
{{i}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% for s in entry.syndications.all() %}
|
||||||
|
{{i}}<a class="u-syndication card-link" href="{{ s.url }}">
|
||||||
|
{{i}}<i class="{{ s.site.icon }}" aria-hidden="true"></i>
|
||||||
|
{{i}}{{ s.site.domain }}
|
||||||
|
{{i}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{{i}}</div>
|
||||||
|
{{i}}</div>
|
||||||
|
|
||||||
|
{{i}}<script class="p-json-ld" type="application/ld+json">{{ entry.json_ld | tojson }}</script>
|
||||||
|
{{i}}</article>
|
||||||
|
{%- endmacro %}
|
20
entries/jinja2/entries/index.html
Normal file
20
entries/jinja2/entries/index.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends 'lemoncurry/layout.html' %}
|
||||||
|
|
||||||
|
{% block html_attr %}
|
||||||
|
class="h-feed"{{ super() }}
|
||||||
|
{%- endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" type="text/stylus" href="{{ static('entries/css/h-entry.styl') }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% import 'entries/h-entry.html' as h %}
|
||||||
|
{% block main %}
|
||||||
|
<ol class="list-unstyled entries">
|
||||||
|
{% for entry in entries %}
|
||||||
|
<li>
|
||||||
|
{{ h.hEntry(entry, indent_width=10) }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endblock %}
|
|
@ -7,16 +7,20 @@ from ronkyuu import webmention
|
||||||
@job
|
@job
|
||||||
def ping_hub(*urls):
|
def ping_hub(*urls):
|
||||||
for url in urls:
|
for url in urls:
|
||||||
requests.post(settings.PUSH_HUB, data={
|
requests.post(
|
||||||
'hub.mode': 'publish',
|
settings.PUSH_HUB,
|
||||||
'hub.url': url,
|
data={
|
||||||
})
|
"hub.mode": "publish",
|
||||||
|
"hub.url": url,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@job
|
@job
|
||||||
def send_mentions(source):
|
def send_mentions(source, targets=None):
|
||||||
result = webmention.findMentions(source)
|
if targets is None:
|
||||||
for target in result['refs']:
|
targets = webmention.findMentions(source)["refs"]
|
||||||
|
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:
|
||||||
webmention.sendWebmention(source, target, endpoint)
|
webmention.sendWebmention(source, target, endpoint)
|
||||||
|
|
|
@ -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,7 +8,6 @@ import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -17,20 +16,41 @@ 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')),
|
(
|
||||||
('kind', models.CharField(choices=[('note', 'Note'), ('article', 'Article')], default='note', max_length=30)),
|
"id",
|
||||||
('name', models.CharField(blank=True, max_length=100)),
|
models.AutoField(
|
||||||
('summary', models.TextField(blank=True)),
|
auto_created=True,
|
||||||
('content', models.TextField()),
|
primary_key=True,
|
||||||
('published', models.DateTimeField()),
|
serialize=False,
|
||||||
('updated', models.DateTimeField()),
|
verbose_name="ID",
|
||||||
('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,23 +7,42 @@ 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')),
|
(
|
||||||
('url', models.CharField(max_length=255)),
|
"id",
|
||||||
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syndications', to='entries.Entry')),
|
models.AutoField(
|
||||||
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')),
|
auto_created=True,
|
||||||
|
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,14 +6,13 @@ 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,20 +8,28 @@ 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(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
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(choices=[('note', 'note'), ('article', 'article')], db_index=True, default='note', max_length=30),
|
field=models.CharField(
|
||||||
|
choices=[("note", "note"), ("article", "article")],
|
||||||
|
db_index=True,
|
||||||
|
default="note",
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,20 +6,24 @@ 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(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo')], db_index=True, default='note', max_length=30),
|
field=models.CharField(
|
||||||
|
choices=[("note", "note"), ("article", "article"), ("photo", "photo")],
|
||||||
|
db_index=True,
|
||||||
|
default="note",
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,34 +8,41 @@ 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(default=django.utils.timezone.now, editable=False, verbose_name='created'),
|
field=model_utils.fields.AutoCreatedField(
|
||||||
|
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(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
|
field=model_utils.fields.AutoLastModifiedField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="modified",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,20 +6,31 @@ 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(choices=[('note', 'note'), ('article', 'article'), ('photo', 'photo'), ('reply', 'reply'), ('like', 'like'), ('repost', 'repost')], db_index=True, default='note', max_length=30),
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("note", "note"),
|
||||||
|
("article", "article"),
|
||||||
|
("photo", "photo"),
|
||||||
|
("reply", "reply"),
|
||||||
|
("like", "like"),
|
||||||
|
("repost", "repost"),
|
||||||
|
],
|
||||||
|
db_index=True,
|
||||||
|
default="note",
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,25 +6,24 @@ 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,21 +6,28 @@ 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')),
|
(
|
||||||
('name', models.CharField(max_length=255, unique=True)),
|
"id",
|
||||||
('slug', models.CharField(max_length=255, unique=True)),
|
models.AutoField(
|
||||||
|
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,15 +6,14 @@ 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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
29
entries/migrations/0012_auto_20180628_2044.py
Normal file
29
entries/migrations/0012_auto_20180628_2044.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 2.0.6 on 2018-06-28 10:44
|
||||||
|
|
||||||
|
import computed_property.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("entries", "0011_auto_20171120_1108"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="syndication",
|
||||||
|
options={"ordering": ["domain"]},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="syndication",
|
||||||
|
name="profile",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="syndication",
|
||||||
|
name="domain",
|
||||||
|
field=computed_property.fields.ComputedCharField(
|
||||||
|
compute_from="calc_domain", default="", editable=False, max_length=255
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
22
entries/migrations/0013_alter_entry_kind.py
Normal file
22
entries/migrations/0013_alter_entry_kind.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,20 +1,23 @@
|
||||||
|
from computed_property import ComputedCharField
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site as DjangoSite
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from mf2util import interpret
|
from mf2util import interpret
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from textwrap import shorten
|
from textwrap import shorten
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
from lemonshort.short_url import short_url
|
from lemonshort.short_url import short_url
|
||||||
from meta.models import ModelMeta
|
from meta.models import ModelMeta
|
||||||
from model_utils.models import TimeStampedModel
|
from model_utils.models import TimeStampedModel
|
||||||
from users.models import Profile
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,38 +33,33 @@ 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
|
return qs.select_related("author").prefetch_related("cats", "syndications")
|
||||||
.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,
|
max_length=30, choices=ENTRY_KINDS, db_index=True, default=ENTRY_KINDS[0][0]
|
||||||
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)
|
||||||
|
@ -69,7 +67,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -77,10 +75,7 @@ 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(
|
return interpret(requests.mf2(self.in_reply_to).to_dict(), self.in_reply_to)
|
||||||
requests.mf2(self.in_reply_to).to_dict(),
|
|
||||||
self.in_reply_to
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def published(self):
|
def published(self):
|
||||||
|
@ -91,35 +86,29 @@ 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(
|
return shorten(utils.to_plain(self.paragraphs[0]), width=100, placeholder="…")
|
||||||
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 [
|
return ["\n".join(para) for k, para in groupby(lines, key=bool) if k]
|
||||||
"\n".join(para) for k, para in groupby(lines, key=bool) if k
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def twitter_creator(self):
|
def twitter_creator(self):
|
||||||
|
@ -134,23 +123,41 @@ 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://' + Site.objects.get_current().domain
|
base = "https://" + DjangoSite.objects.get_current().domain
|
||||||
return urljoin(base, self.url)
|
return urljoin(base, self.url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def affected_urls(self):
|
||||||
|
base = "https://" + DjangoSite.objects.get_current().domain
|
||||||
|
kind = kinds.from_id[self.kind]
|
||||||
|
urls = {
|
||||||
|
self.url,
|
||||||
|
reverse("entries:index", kwargs={"kind": kind}),
|
||||||
|
reverse("entries:atom_by_kind", kwargs={"kind": kind}),
|
||||||
|
reverse("entries:rss_by_kind", kwargs={"kind": kind}),
|
||||||
|
} | {cat.url for cat in self.cats.all()}
|
||||||
|
if kind.on_home:
|
||||||
|
urls |= {
|
||||||
|
reverse("home:index"),
|
||||||
|
reverse("entries:atom"),
|
||||||
|
reverse("entries:rss"),
|
||||||
|
}
|
||||||
|
return {urljoin(base, u) for u in urls}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
kind = kinds.from_id[self.kind]
|
kind = kinds.from_id[self.kind]
|
||||||
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):
|
||||||
|
@ -162,49 +169,58 @@ class Entry(ModelMeta, TimeStampedModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json_ld(self):
|
def json_ld(self):
|
||||||
base = 'https://' + Site.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 SyndicationManager(models.Manager):
|
|
||||||
def get_queryset(self):
|
|
||||||
qs = super(SyndicationManager, self).get_queryset()
|
|
||||||
return qs.select_related('profile__site')
|
|
||||||
|
|
||||||
|
|
||||||
class Syndication(models.Model):
|
class Syndication(models.Model):
|
||||||
objects = SyndicationManager()
|
|
||||||
entry = models.ForeignKey(
|
entry = models.ForeignKey(
|
||||||
Entry,
|
Entry, related_name="syndications", on_delete=models.CASCADE
|
||||||
related_name='syndications',
|
|
||||||
on_delete=models.CASCADE
|
|
||||||
)
|
)
|
||||||
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
|
|
||||||
url = models.CharField(max_length=255)
|
url = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
domain = ComputedCharField(
|
||||||
|
compute_from="calc_domain",
|
||||||
|
max_length=255,
|
||||||
|
)
|
||||||
|
|
||||||
|
def calc_domain(self):
|
||||||
|
domain = urlparse(self.url).netloc
|
||||||
|
if domain.startswith("www."):
|
||||||
|
domain = domain[4:]
|
||||||
|
return domain
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def site(self):
|
||||||
|
d = self.domain
|
||||||
|
try:
|
||||||
|
return Site.objects.get(domain=d)
|
||||||
|
except Site.DoesNotExist:
|
||||||
|
return Site(name=d, domain=d, icon="fas fa-newspaper")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['profile']
|
ordering = ["domain"]
|
||||||
|
|
|
@ -1,33 +1,32 @@
|
||||||
from django.core.paginator import Paginator
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.core.paginator import Page, Paginator
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
from lemoncurry.middleware import ResponseException
|
||||||
|
|
||||||
def paginate(queryset, reverse, page):
|
|
||||||
class Page:
|
|
||||||
def __init__(self, i):
|
|
||||||
self.i = i
|
|
||||||
|
|
||||||
@property
|
def paginate(queryset, reverse: Callable[[int], str], page: int | None) -> Page:
|
||||||
def url(self):
|
def redirect_to_page(i: int):
|
||||||
return reverse(self.i)
|
raise ResponseException(redirect(reverse(i)))
|
||||||
|
|
||||||
@property
|
def reversible(p: Page) -> Page:
|
||||||
def current(self):
|
p.reverse = reverse
|
||||||
return self.i == entries.number
|
return p
|
||||||
|
|
||||||
# If the first page was requested, redirect to the clean version of the URL
|
|
||||||
# with no page suffix.
|
|
||||||
if page == 1:
|
|
||||||
return redirect(Page(1).url)
|
|
||||||
|
|
||||||
paginator = Paginator(queryset, 10)
|
paginator = Paginator(queryset, 10)
|
||||||
entries = paginator.page(page or 1)
|
|
||||||
|
|
||||||
entries.pages = tuple(Page(i) for i in paginator.page_range)
|
# If no page number was specified, return page one.
|
||||||
|
if page is None:
|
||||||
|
return reversible(paginator.page(1))
|
||||||
|
|
||||||
if entries.has_previous():
|
# If the first page was explicitly requested, or the page number was negative, redirect to page one with no URL suffix.
|
||||||
entries.prev = Page(entries.previous_page_number())
|
if page <= 1:
|
||||||
if entries.has_next():
|
redirect_to_page(1)
|
||||||
entries.next = Page(entries.next_page_number())
|
|
||||||
|
|
||||||
return entries
|
# If the page requested is larger than the last page, then redirect to the last page.
|
||||||
|
if page > paginator.num_pages:
|
||||||
|
redirect_to_page(paginator.num_pages)
|
||||||
|
|
||||||
|
# Just return the current page! Hooray!
|
||||||
|
return reversible(paginator.page(page))
|
||||||
|
|
|
@ -10,12 +10,40 @@ ol.entries, div.entry
|
||||||
&:last-child
|
&:last-child
|
||||||
margin-bottom 0
|
margin-bottom 0
|
||||||
|
|
||||||
.card.h-entry
|
.h-entry.media
|
||||||
|
> aside.info
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
align-items flex-start
|
||||||
|
font-size 0.8rem
|
||||||
|
margin-right 0.4rem
|
||||||
|
flex-basis 7rem
|
||||||
|
max-width 10%
|
||||||
|
|
||||||
|
a.p-author
|
||||||
|
align-self center
|
||||||
|
text-align center
|
||||||
|
img.u-photo
|
||||||
|
border-radius .25rem
|
||||||
|
max-height 3em
|
||||||
|
> *
|
||||||
|
margin-bottom .25rem
|
||||||
|
.media
|
||||||
|
align-items baseline
|
||||||
|
max-width 10rem
|
||||||
|
> :first-child
|
||||||
|
margin-right 2px
|
||||||
|
display none
|
||||||
|
@media (min-width $sm)
|
||||||
|
display inline-block
|
||||||
|
> .card
|
||||||
|
flex 1
|
||||||
.e-content
|
.e-content
|
||||||
ul
|
ul
|
||||||
list-style-type disc
|
list-style-type disc
|
||||||
ul, ol
|
ul, ol
|
||||||
margin-bottom 1rem
|
margin-bottom 1rem
|
||||||
|
padding-left 1.1rem
|
||||||
ul
|
ul
|
||||||
list-style-type circle
|
list-style-type circle
|
||||||
ul, ol
|
ul, ol
|
||||||
|
@ -26,14 +54,8 @@ ol.entries, div.entry
|
||||||
max-width 100%
|
max-width 100%
|
||||||
> :last-child
|
> :last-child
|
||||||
margin-bottom 0
|
margin-bottom 0
|
||||||
|
.card-link
|
||||||
.card-footer
|
|
||||||
text-align center
|
|
||||||
> *
|
|
||||||
display inline-block
|
display inline-block
|
||||||
margin-right 1rem
|
font-size 0.8rem
|
||||||
&:last-child
|
margin-left 0
|
||||||
margin-right 0
|
margin-right 1.25rem
|
||||||
.h-card > img
|
|
||||||
height 1em
|
|
||||||
vertical-align baseline
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends 'lemoncurry/layout.html' %}
|
|
||||||
{% load absolute_url static %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="shortlink" href="{{ entry.short_url }}" />
|
|
||||||
<link rel="alternate" type="application/json+oembed" href="https://wirres.net/oembed/oembed.php?url={{ uri | absolute_url | urlencode }}" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block styles %}
|
|
||||||
<link rel="stylesheet" type="text/stylus" href="{% static 'entries/css/h-entry.styl' %}" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<div class="entry">
|
|
||||||
{% include 'entries/h-entry.html' %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -1,67 +0,0 @@
|
||||||
{% load bleach friendly_url humanize jsonify markdown %}<article class="card h-entry">
|
|
||||||
{% if entry.photo %}<img class="card-img-top u-photo" src="{{ entry.photo.url }}" />{% endif %}
|
|
||||||
|
|
||||||
{% if entry.in_reply_to %}{% with reply=entry.reply_context %}
|
|
||||||
<article class="card-header media u-in-reply-to h-cite">
|
|
||||||
<a class="align-self-center p-author h-card" href="{{ reply.author.url }}">
|
|
||||||
<img class="mr-3 rounded" width="100" src="{{ reply.author.photo }}"
|
|
||||||
alt="{{ reply.author.name }}" title="{{ reply.author.name }}" />
|
|
||||||
</a>
|
|
||||||
<div class="media-body">
|
|
||||||
{% if reply.name %}<h4 class="p-name">{{ reply.name }}</h4>{% endif %}
|
|
||||||
<div class="e-content{% if not reply.name %} p-name{% endif %}">{{ reply.content | bleach }}</div>
|
|
||||||
</div>
|
|
||||||
</article>{% endwith %}{% endif %}
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
{% if entry.name %}<h4 class="card-title p-name">{{ entry.name }}</h4>{% endif %}
|
|
||||||
<div class="e-content{% if not entry.name %} p-name{% endif %}">{{ entry.content | markdown }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer">
|
|
||||||
<a class="p-author h-card" href="{{ entry.author.url }}">
|
|
||||||
<img class="u-photo" src="{{ entry.author.avatar.url }}" />
|
|
||||||
{{ entry.author.first_name }} {{ entry.author.last_name }}
|
|
||||||
</a>
|
|
||||||
<a class="u-uid u-url" href="{{ entry.url }}">
|
|
||||||
<time class="dt-published" datetime="{{ entry.published.isoformat }}">
|
|
||||||
<i class="fas fa-calendar"></i>
|
|
||||||
{{ entry.published | naturaltime }}
|
|
||||||
</time>
|
|
||||||
</a>
|
|
||||||
{% if entry.updated != entry.published %}
|
|
||||||
<time class="dt-updated" datetime="{{ entry.updated.isoformat }}">
|
|
||||||
<i class="fas fa-pencil-alt"></i>
|
|
||||||
{{ entry.updated | naturaltime }}
|
|
||||||
</time>
|
|
||||||
{% endif %}
|
|
||||||
<a class="u-url" href="{{ entry.short_url }}">
|
|
||||||
<i class="fas fa-link"></i>
|
|
||||||
{{ entry.short_url | friendly_url }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if entry.cats.exists %}
|
|
||||||
<div class="card-footer">
|
|
||||||
{% for c in entry.cats.all %}
|
|
||||||
<a class="p-category" href="{{ c.url }}">
|
|
||||||
<i class="fas fa-paw"></i>
|
|
||||||
{{ c.name }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.syndications.exists %}
|
|
||||||
<div class="card-footer">
|
|
||||||
{% for s in entry.syndications.all %}
|
|
||||||
<a class="u-syndication" href="{{ s.url }}">
|
|
||||||
<i class="{{ s.profile.site.icon }}" aria-hidden="true"></i>
|
|
||||||
{{ s.profile }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script class="p-json-ld" type="application/ld+json">{{ entry.json_ld | jsonify }}</script>
|
|
||||||
</article>
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends 'lemoncurry/layout.html' %}
|
|
||||||
{% load static %}
|
|
||||||
{% block html_class %}h-feed{% endblock %}
|
|
||||||
|
|
||||||
{% block styles %}
|
|
||||||
<link rel="stylesheet" type="text/stylus" href="{% static 'entries/css/h-entry.styl' %}" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<ol class="list-unstyled entries">
|
|
||||||
{% for entry in entries %}
|
|
||||||
<li>
|
|
||||||
{% include 'entries/h-entry.html' %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
{% endblock %}
|
|
|
@ -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,47 +3,46 @@ 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>', perma.entry, name='entry'),
|
path("<kind:kind>/<int:id>/<slug:slug>", 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):
|
||||||
|
@ -51,9 +50,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,36 +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)
|
||||||
if hasattr(entries, 'content'):
|
|
||||||
return entries
|
|
||||||
|
|
||||||
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)
|
||||||
if hasattr(entries, 'content'):
|
|
||||||
return entries
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'entries': entries,
|
"entries": entries,
|
||||||
'title': '#' + cat.name,
|
"title": "#" + cat.name,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +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,
|
||||||
'meta': entry.as_meta(request)
|
|
||||||
}
|
}
|
||||||
|
|
5
gunicorn.py
Normal file
5
gunicorn.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
proc_name = "lemoncurry"
|
||||||
|
worker_class = "gevent"
|
||||||
|
workers = multiprocessing.cpu_count() * 2 + 1
|
107
home/jinja2/home/index.html
Normal file
107
home/jinja2/home/index.html
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
{% extends 'lemoncurry/layout.html' %}
|
||||||
|
|
||||||
|
{% block html_attr %}
|
||||||
|
class="h-feed"{{ super() }}
|
||||||
|
{%- endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" type="text/stylus" href="{{ static('home/css/index.styl') }}" />
|
||||||
|
<link rel="stylesheet" type="text/stylus" href="{{ static('entries/css/h-entry.styl') }}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{% for key in user.keys.all() %}
|
||||||
|
<link rel="pgpkey" href="{{ key.file.url }}" />
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<aside class="author">
|
||||||
|
<article class="h-card p-author card">
|
||||||
|
<a class="u-uid u-url" href="{{ user.full_url }}">
|
||||||
|
{% if user.avatar %}
|
||||||
|
<img class="u-photo card-img-top" src="{{ user.avatar.url }}" alt="{{ user.name }}" />
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="p-name card-title">
|
||||||
|
<span class="p-given-name">{{ user.first_name }}</span> <span class="p-family-name">{{ user.last_name }}</span>
|
||||||
|
</h4>
|
||||||
|
{% if user.note %}
|
||||||
|
<div class="p-note">
|
||||||
|
{{ user.note | markdown }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<ul class="profiles">
|
||||||
|
<li>
|
||||||
|
<a class="u-email" rel="me" href="mailto:{{ user.email }}">
|
||||||
|
<i class="fas fa-envelope" aria-hidden="true"></i>
|
||||||
|
{{ user.email }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% if user.xmpp %}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="u-impp" rel="me" href="xmpp:{{ user.xmpp }}">
|
||||||
|
<i class="openwebicons-xmpp" aria-hidden="true"></i>
|
||||||
|
{{ user.xmpp }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if user.keys.exists() %}
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<ul class="profiles">
|
||||||
|
{% for key in user.keys.all() %}
|
||||||
|
<a class="u-key" href="{{ key.file.url }}">
|
||||||
|
<i class="fas fa-key" aria-hidden="true"></i>
|
||||||
|
{{ key.pretty_print() }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.profiles.exists() %}
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<ul class="profiles">
|
||||||
|
{% for profile in user.profiles.all() %}
|
||||||
|
<a class="u-url" rel="me" href="{{ profile.url }}" title="{{ profile }}">
|
||||||
|
<i class="{{ profile.site.icon }}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{ profile }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script class="p-json-ld" type="application/ld+json">{{ user.json_ld | tojson }}</script>
|
||||||
|
</article>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{% import 'entries/h-entry.html' as h %}
|
||||||
|
<ol class="list-unstyled entries">
|
||||||
|
{% for entry in entries %}
|
||||||
|
<li>
|
||||||
|
{{ h.hEntry(entry, indent_width=10) }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block foot %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
tippy('.profiles [title]', {
|
||||||
|
arrow: true,
|
||||||
|
content: function(element) {
|
||||||
|
return element.getAttribute('title');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -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)
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
$sm = 576px
|
|
||||||
$md = 768px
|
|
||||||
$lg = 992px
|
|
||||||
$xl = 1200px
|
|
||||||
|
|
||||||
main
|
main
|
||||||
flex-direction column
|
flex-direction column
|
||||||
align-items center
|
align-items center
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
{% extends 'lemoncurry/layout.html' %}
|
|
||||||
{% load jsonify markdown static %}
|
|
||||||
{% block html_class %}h-feed{% endblock %}
|
|
||||||
{% block styles %}
|
|
||||||
<link rel="stylesheet" type="text/stylus" href="{% static 'home/css/index.styl' %}" />
|
|
||||||
<link rel="stylesheet" type="text/stylus" href="{% static 'entries/css/h-entry.styl' %}" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}{% for key in user.keys.all %}<link rel="pgpkey" href="{{ key.file.url }}" />{% endfor %}{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<aside class="author">
|
|
||||||
<article class="h-card card p-author">
|
|
||||||
<a class="u-uid u-url" href="{{ uri }}">
|
|
||||||
{% if user.avatar %}<img class="card-img-top u-photo" src="{{ user.avatar.url }}" alt="{{ user.first_name }} {{ user.last_name }}" />{% endif %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<h4 class="card-title p-name">
|
|
||||||
<span class="p-given-name">{{ user.first_name }}</span> <span class="p-family-name">{{ user.last_name }}</span>
|
|
||||||
</h4>
|
|
||||||
{% if user.note %}<div class="p-note">{{ user.note | markdown }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer">
|
|
||||||
<ul class="profiles">
|
|
||||||
<li><a class="u-email" rel="me" href="mailto:{{ user.email }}">
|
|
||||||
<i class="fas fa-envelope"></i> {{ user.email }}
|
|
||||||
</a></li>
|
|
||||||
{% if user.xmpp %}<li><a class="u-impp" rel="me" href="xmpp:{{ user.xmpp }}">
|
|
||||||
<i class="openwebicons-xmpp" aria-hidden="true"></i> {{ user.xmpp }}
|
|
||||||
</a></li>{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if user.keys.exists %}<div class="card-footer">
|
|
||||||
<ul class="profiles">
|
|
||||||
{% for key in user.keys.all %}<li>
|
|
||||||
<a class="u-key" href="{{ key.file.url }}">
|
|
||||||
<i class="fas fa-key"></i> {{ key.pretty_print }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>{% endif %}
|
|
||||||
|
|
||||||
{% if user.profiles.exists %}<div class="card-footer">
|
|
||||||
<ul class="profiles">
|
|
||||||
{% for profile in user.profiles.all %}<li>
|
|
||||||
<a class="u-url" rel="me" href="{{ profile.url }}" title="{{ profile }}"><i class="{{ profile.site.icon }}" aria-hidden="true"></i><span class="sr-only">{{ profile }}</span></a>
|
|
||||||
</li>{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>{% endif %}
|
|
||||||
|
|
||||||
<script class="p-json-ld" type="application/ld+json">{{ user.json_ld | jsonify }}</script>
|
|
||||||
</article>
|
|
||||||
</aside>
|
|
||||||
<ol class="list-unstyled entries">
|
|
||||||
{% for entry in entries %}
|
|
||||||
<li>
|
|
||||||
{% include 'entries/h-entry.html' %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
{% endblock %}
|
|
||||||
{% block foot %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
tippy('.profiles [title]', {arrow: true});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -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,39 +8,31 @@ 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)
|
||||||
|
|
||||||
# If we got a valid HTTP response, just return it without rendering.
|
|
||||||
if hasattr(entries, 'content'):
|
|
||||||
return entries
|
|
||||||
|
|
||||||
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"),
|
||||||
'meta': user.as_meta(request),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def robots(request):
|
def robots(request):
|
||||||
base = utils.origin(request)
|
base = utils.origin(request)
|
||||||
lines = (
|
lines = ("User-agent: *", "Sitemap: {0}".format(urljoin(base, reverse("sitemap"))))
|
||||||
'User-agent: *',
|
return HttpResponse("\n".join(lines) + "\n", content_type="text/plain")
|
||||||
'Sitemap: {0}'.format(urljoin(base, reverse('sitemap')))
|
|
||||||
)
|
|
||||||
return HttpResponse("\n".join(lines) + "\n", content_type='text/plain')
|
|
||||||
|
|
|
@ -9,8 +9,10 @@
|
||||||
<form class="card" method="post" action="{{ url('lemonauth:indie_approve') }}">
|
<form class="card" method="post" action="{{ url('lemonauth:indie_approve') }}">
|
||||||
<h4 class="card-header h-x-app">
|
<h4 class="card-header h-x-app">
|
||||||
{% if app %}
|
{% if app %}
|
||||||
<img class="u-logo p-name" src="{{ app.logo[0] }}" alt="{{ app.name[0] }}">
|
{% if app.logo is defined %}
|
||||||
sign in to {{ app.name[0] }} (<a class="u-url code" href="{{ params.client_id }}">{{ params.client_id }}</a>)?
|
<img class="u-logo" src="{{ app.logo[0] }}" alt="{{ app.name[0] }}" />
|
||||||
|
{% endif %}
|
||||||
|
sign in to <span class="p-name">{{ app.name[0] }}</span> (<a class="u-url code" href="{{ params.client_id }}">{{ params.client_id }}</a>)?
|
||||||
{% else %}
|
{% else %}
|
||||||
sign in to <a class="u-url p-name code" href="{{ params.client_id }}">{{ params.client_id }}</a>?
|
sign in to <a class="u-url p-name code" href="{{ params.client_id }}">{{ params.client_id }}</a>?
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -60,15 +62,22 @@
|
||||||
</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> - they are who they claim to be!
|
this client has been <strong>verified</strong> using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- 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> - check the redirect uri carefully yourself!
|
this client could <strong>not</strong> be verified using <code>{{ '<link rel="redirect_uri">' | escape }}</code> <br/>- 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-theme]', {arrow: true});
|
tippy('[data-tippy-html]', {
|
||||||
|
arrow: true,
|
||||||
|
allowHTML: true,
|
||||||
|
maxWidth: 500,
|
||||||
|
content: function(element) {
|
||||||
|
return document.querySelector(element.getAttribute('data-tippy-html')).innerHTML;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
68
lemonauth/jinja2/lemonauth/tokens.html
Normal file
68
lemonauth/jinja2/lemonauth/tokens.html
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{% extends 'lemoncurry/layout.html' %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="card-columns">
|
||||||
|
{% for _, c in clients | dictsort %}
|
||||||
|
<div class="h-x-app card">
|
||||||
|
{% if c.app.logo %}
|
||||||
|
<img class="u-photo card-img-top" src="{{ c.app.logo[0] }}" alt="{{ c.app.name[0] }}" />
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<span class="p-name">{{ c.app.name[0] if c.app else (c.id | friendly_url) }}</span>
|
||||||
|
<span class="badge badge-light">
|
||||||
|
<span class="p-count">{{ c.count }}</span>
|
||||||
|
{{ 'tokens' if c.count > 1 else 'token' }}
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<h6 class="card-subtitle mb-2">
|
||||||
|
<a class="u-url" href="{{ c.id }}">{{ c.id }}</a>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<p class="card-text">this client has access to the scopes:</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for scope in c.scopes %}
|
||||||
|
<li class="p-scope list-group-item">{{ scope }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form class="card-footer text-right" action="{{ url('lemonauth:tokens_revoke', kwargs={'client_id': c.id}) }}">
|
||||||
|
{{ csrf_input }}
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-ban" aria-hidden="true"></i>
|
||||||
|
revoke access
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block foot %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$('form').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $f = $(this);
|
||||||
|
if ($f.data('deleting')) return;
|
||||||
|
$f.data('deleting', true);
|
||||||
|
$f.find('button').prop({disabled: true})
|
||||||
|
.find('[data-fa-i2svg]').removeClass('fa-ban').addClass('fa-circle-notch fa-spin');
|
||||||
|
$.ajax({
|
||||||
|
headers: {'X-CSRFToken': $f.find('[name="csrfmiddlewaretoken"]').val()},
|
||||||
|
method: 'DELETE',
|
||||||
|
url: $f.attr('action'),
|
||||||
|
}).then(function() {
|
||||||
|
var $app = $f.parent('.h-x-app');
|
||||||
|
return $app.hide(function() {
|
||||||
|
$app.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -7,25 +7,36 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [] # type: List[Tuple[str, str]]
|
||||||
] # type: List[Tuple[str, str]]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='IndieAuthCode',
|
name="IndieAuthCode",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True,
|
(
|
||||||
primary_key=True, serialize=False, verbose_name='ID')),
|
"id",
|
||||||
('code', models.CharField(max_length=64, unique=True)),
|
models.AutoField(
|
||||||
('me', models.CharField(max_length=255)),
|
auto_created=True,
|
||||||
('client_id', models.CharField(max_length=255)),
|
primary_key=True,
|
||||||
('redirect_uri', models.CharField(max_length=255)),
|
serialize=False,
|
||||||
('response_type', models.CharField(choices=[
|
verbose_name="ID",
|
||||||
('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,13 +6,12 @@ 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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
120
lemonauth/migrations/0003_indieauthcode_token.py
Normal file
120
lemonauth/migrations/0003_indieauthcode_token.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
# Generated by Django 2.0.6 on 2018-06-12 04:51
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
import randomslugfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("lemonauth", "0002_delete_indieauthcode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="IndieAuthCode",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="created",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Token",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
model_utils.fields.AutoCreatedField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="created",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
62
lemonauth/models.py
Normal file
62
lemonauth/models.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from randomslugfield import RandomSlugField
|
||||||
|
from model_utils import Choices
|
||||||
|
from model_utils.fields import StatusField
|
||||||
|
from model_utils.models import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class AuthSecret(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
An AuthSecret is a model with an unguessable primary key, suitable for
|
||||||
|
sharing with external sites for secure authentication.
|
||||||
|
|
||||||
|
AuthSecret is primarily used to factor out the many similarities between
|
||||||
|
authorisation codes and tokens in IndieAuth - the two contain many
|
||||||
|
identical fields, but just a few differences.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = RandomSlugField(primary_key=True, length=30)
|
||||||
|
client_id = models.URLField()
|
||||||
|
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||||
|
scope = models.TextField(blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def me(self):
|
||||||
|
return self.user.full_url
|
||||||
|
|
||||||
|
def __contains__(self, scope):
|
||||||
|
return scope in self.scope.split(" ")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class IndieAuthCode(AuthSecret):
|
||||||
|
"""
|
||||||
|
An IndieAuthCode is an authorisation code that a client must provide to us
|
||||||
|
to complete the IndieAuth process.
|
||||||
|
|
||||||
|
Codes are single-use, and if unused will be expired automatically after
|
||||||
|
thirty seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
redirect_uri = models.URLField()
|
||||||
|
|
||||||
|
RESPONSE_TYPE = Choices("id", "code")
|
||||||
|
response_type = StatusField(choices_name="RESPONSE_TYPE")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return self.created + timedelta(seconds=30) < now()
|
||||||
|
|
||||||
|
|
||||||
|
class Token(AuthSecret):
|
||||||
|
"""
|
||||||
|
A Token grants a client long-term authorisation - it will not expire unless
|
||||||
|
explicitly revoked by the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
|
@ -2,10 +2,10 @@
|
||||||
img
|
img
|
||||||
height 2em
|
height 2em
|
||||||
margin-right .5em
|
margin-right .5em
|
||||||
.tippy-tooltip
|
.tippy-box
|
||||||
&.success-theme
|
&[data-theme~='success']
|
||||||
color $base0B
|
color $base0B
|
||||||
&.warning-theme
|
&[data-theme~='warning']
|
||||||
color $base0A
|
color $base0A
|
||||||
.verified-success
|
.verified-success
|
||||||
color $base0B
|
color $base0B
|
||||||
|
|
|
@ -1,78 +1,46 @@
|
||||||
from jose import jwt
|
from micropub import error
|
||||||
|
from .models import IndieAuthCode, Token
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from micropub.views import error
|
|
||||||
|
|
||||||
|
|
||||||
def auth(request):
|
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":
|
||||||
return 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:
|
||||||
return error.bad_req(
|
raise error.bad_req("invalid Bearer auth format, must be Bearer <token>")
|
||||||
'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:
|
||||||
return error.unauthorized()
|
raise error.unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = decode(token)
|
token = Token.objects.get(pk=token)
|
||||||
except Exception as e:
|
except Token.DoesNotExist:
|
||||||
return error.forbidden()
|
raise error.forbidden()
|
||||||
|
|
||||||
return MicropubToken(token)
|
return token
|
||||||
|
|
||||||
|
|
||||||
class MicropubToken:
|
|
||||||
def __init__(self, tok):
|
|
||||||
self.user = get_user_model().objects.get(pk=tok['uid'])
|
|
||||||
self.client = tok['cid']
|
|
||||||
self.scope = tok['sco']
|
|
||||||
|
|
||||||
self.me = self.user.full_url
|
|
||||||
self.scopes = self.scope.split(' ')
|
|
||||||
|
|
||||||
def __contains__(self, scope):
|
|
||||||
return scope in self.scopes
|
|
||||||
|
|
||||||
|
|
||||||
def encode(payload):
|
|
||||||
return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
|
|
||||||
|
|
||||||
|
|
||||||
def decode(token):
|
|
||||||
return jwt.decode(token, settings.SECRET_KEY, algorithms=('HS256',))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_auth_code(req):
|
def gen_auth_code(req):
|
||||||
code = {
|
code = IndieAuthCode()
|
||||||
'uid': req.user.id,
|
code.user = req.user
|
||||||
'cid': req.POST['client_id'],
|
code.client_id = req.POST["client_id"]
|
||||||
'uri': req.POST['redirect_uri'],
|
code.redirect_uri = req.POST["redirect_uri"]
|
||||||
'typ': req.POST.get('response_type', 'id'),
|
code.response_type = req.POST.get("response_type", "id")
|
||||||
'iat': datetime.utcnow(),
|
if "scope" in req.POST:
|
||||||
'exp': datetime.utcnow() + timedelta(seconds=30),
|
code.scope = " ".join(req.POST.getlist("scope"))
|
||||||
}
|
code.save()
|
||||||
if 'scope' in req.POST:
|
return code.id
|
||||||
code['sco'] = ' '.join(req.POST.getlist('scope'))
|
|
||||||
|
|
||||||
return encode(code)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_token(code):
|
def gen_token(code):
|
||||||
tok = {
|
tok = Token()
|
||||||
'uid': code['uid'],
|
tok.user = code.user
|
||||||
'cid': code['cid'],
|
tok.client_id = code.client_id
|
||||||
'sco': code['sco'],
|
tok.scope = code.scope
|
||||||
'iat': datetime.utcnow(),
|
tok.save()
|
||||||
}
|
return tok.id
|
||||||
return encode(tok)
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
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/<path:client_id>",
|
||||||
|
views.TokensRevokeView.as_view(),
|
||||||
|
name="tokens_revoke",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,3 +2,4 @@ from .login import login
|
||||||
from .logout import logout
|
from .logout import logout
|
||||||
from .indie import IndieView, approve as indie_approve
|
from .indie import IndieView, approve as indie_approve
|
||||||
from .token import TokenView
|
from .token import TokenView
|
||||||
|
from .tokens import TokensListView, TokensRevokeView
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from annoying.decorators import render_to
|
from annoying.decorators import render_to
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -11,119 +10,116 @@ from lemoncurry import breadcrumbs, requests, utils
|
||||||
from urllib.parse import urlencode, urljoin, urlunparse, urlparse
|
from urllib.parse import urlencode, urljoin, urlunparse, urlparse
|
||||||
|
|
||||||
from .. import tokens
|
from .. import tokens
|
||||||
|
from ..models import IndieAuthCode
|
||||||
|
|
||||||
breadcrumbs.add('lemonauth:indie', parent='home:index')
|
breadcrumbs.add("lemonauth:indie", parent="home:index")
|
||||||
|
|
||||||
|
|
||||||
def canonical(url):
|
def canonical(url):
|
||||||
(scheme, loc, path, params, q, fragment) = urlparse(url)
|
if "//" not in url:
|
||||||
|
url = "//" + url
|
||||||
|
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
||||||
|
if not scheme or scheme == "http":
|
||||||
|
scheme = "https"
|
||||||
if not path:
|
if not path:
|
||||||
path = '/'
|
path = "/"
|
||||||
if not loc:
|
return urlunparse((scheme, netloc, path, params, query, fragment))
|
||||||
loc, path = path, ''
|
|
||||||
if not scheme:
|
|
||||||
scheme = 'https'
|
|
||||||
return urlunparse((scheme, loc, path, params, q, 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 = ('me', '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(
|
return utils.bad_req("parameter {0} is required".format(param))
|
||||||
'parameter {0} is required'.format(param)
|
|
||||||
)
|
|
||||||
|
|
||||||
me = canonical(params['me'])
|
me = request.user.full_url
|
||||||
user = urljoin(utils.origin(request), request.user.url)
|
if "me" in params:
|
||||||
if user != me:
|
param_me = canonical(params["me"])
|
||||||
|
if me != param_me:
|
||||||
return utils.forbid(
|
return utils.forbid(
|
||||||
'you are logged in but not as {0}'.format(me)
|
"you are logged in as {}, not as {}".format(me, param_me)
|
||||||
)
|
)
|
||||||
|
|
||||||
redirect_uri = urljoin(params['client_id'], params['redirect_uri'])
|
redirect_uri = urljoin(params["client_id"], params["redirect_uri"])
|
||||||
|
|
||||||
type = params['response_type']
|
type = params["response_type"]
|
||||||
if type not in ('id', 'code'):
|
if type not in ("id", "code"):
|
||||||
return utils.bad_req(
|
return utils.bad_req("unknown response_type: {0}".format(type))
|
||||||
'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(
|
return utils.bad_req("scopes required for code type")
|
||||||
'scopes required for code type'
|
scopes = params["scope"].split(" ")
|
||||||
)
|
|
||||||
scopes = params['scope'].split(' ')
|
|
||||||
|
|
||||||
client = requests.mf2(params['client_id'])
|
client = requests.mf2(params["client_id"])
|
||||||
rels = (client.to_dict()['rel-urls']
|
rels = client.to_dict()["rel-urls"].get(redirect_uri, {}).get("rels", ())
|
||||||
.get(redirect_uri, {})
|
verified = "redirect_uri" in rels
|
||||||
.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 = tokens.decode(post.get('code'))
|
code = IndieAuthCode.objects.get(pk=post.get("code"))
|
||||||
except Exception:
|
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()
|
||||||
|
if code.expired:
|
||||||
|
return utils.forbid("invalid auth code")
|
||||||
|
|
||||||
if code['typ'] != 'id':
|
if code.response_type != "id":
|
||||||
return utils.bad_req(
|
return utils.bad_req("this endpoint only supports response_type=id")
|
||||||
'this endpoint only supports response_type=id'
|
if code.client_id != post.get("client_id"):
|
||||||
)
|
return utils.forbid("client id did not match")
|
||||||
if code['cid'] != post.get('client_id'):
|
if code.redirect_uri != post.get("redirect_uri"):
|
||||||
return utils.forbid('client id did not match')
|
return utils.forbid("redirect uri did not match")
|
||||||
if code['uri'] != post.get('redirect_uri'):
|
|
||||||
return utils.forbid('redirect uri did not match')
|
|
||||||
|
|
||||||
user = get_user_model().objects.get(pk=code['uid'])
|
|
||||||
me = urljoin(utils.origin(request), user.url)
|
|
||||||
# If we got here, it's valid! Yay!
|
# If we got here, it's valid! Yay!
|
||||||
return utils.choose_type(request, {'me': me}, {
|
return utils.choose_type(
|
||||||
'application/x-www-form-urlencoded': utils.form_encoded_response,
|
request,
|
||||||
'application/json': JsonResponse,
|
{"me": code.me},
|
||||||
})
|
{
|
||||||
|
"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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,49 +1,48 @@
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
from .. import tokens
|
from .. import tokens
|
||||||
|
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)
|
||||||
if hasattr(token, 'content'):
|
|
||||||
return token
|
|
||||||
res = {
|
res = {
|
||||||
'me': token.me,
|
"me": token.me,
|
||||||
'client_id': token.client,
|
"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 = tokens.decode(post.get('code'))
|
code = IndieAuthCode.objects.get(pk=post.get("code"))
|
||||||
except Exception:
|
except IndieAuthCode.DoesNotExist:
|
||||||
return utils.forbid('invalid auth code')
|
return utils.forbid("invalid auth code")
|
||||||
|
code.delete()
|
||||||
|
if code.expired:
|
||||||
|
return utils.forbid("invalid auth code")
|
||||||
|
|
||||||
if code['typ'] != 'code':
|
if code.response_type != "code":
|
||||||
return utils.bad_req(
|
return utils.bad_req("this endpoint only supports response_type=code")
|
||||||
'this endpoint only supports response_type=code'
|
if "client_id" in post and code.client_id != post["client_id"]:
|
||||||
|
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"]:
|
||||||
|
return utils.forbid("me did not match")
|
||||||
|
|
||||||
|
return utils.choose_type(
|
||||||
|
req,
|
||||||
|
{
|
||||||
|
"access_token": tokens.gen_token(code),
|
||||||
|
"me": code.me,
|
||||||
|
"scope": code.scope,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if code['cid'] != post.get('client_id'):
|
|
||||||
return utils.forbid('client id did not match')
|
|
||||||
if code['uri'] != post.get('redirect_uri'):
|
|
||||||
return utils.forbid('redirect uri did not match')
|
|
||||||
|
|
||||||
user = get_user_model().objects.get(pk=code['uid'])
|
|
||||||
me = urljoin(utils.origin(req), user.url)
|
|
||||||
if me != post.get('me'):
|
|
||||||
return utils.forbid('me did not match')
|
|
||||||
|
|
||||||
return utils.choose_type(req, {
|
|
||||||
'access_token': tokens.gen_token(code),
|
|
||||||
'me': me,
|
|
||||||
'scope': code['sco'],
|
|
||||||
})
|
|
||||||
|
|
2
lemonauth/views/tokens/__init__.py
Normal file
2
lemonauth/views/tokens/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .list import TokensListView
|
||||||
|
from .revoke import TokensRevokeView
|
42
lemonauth/views/tokens/list.py
Normal file
42
lemonauth/views/tokens/list.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from typing import Dict, Optional, Set
|
||||||
|
from lemoncurry.requests import mf2
|
||||||
|
|
||||||
|
|
||||||
|
class ClientsDict(dict):
|
||||||
|
def __missing__(self, client_id):
|
||||||
|
self[client_id] = Client(client_id)
|
||||||
|
return self[client_id]
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
id: str
|
||||||
|
count: int
|
||||||
|
scopes: Set[str]
|
||||||
|
app: Optional[Dict[str, str]]
|
||||||
|
|
||||||
|
def __init__(self, client_id):
|
||||||
|
self.id = client_id
|
||||||
|
self.count = 0
|
||||||
|
self.scopes = set()
|
||||||
|
try:
|
||||||
|
apps = mf2(self.id).to_dict(filter_by_type="h-x-app")
|
||||||
|
self.app = apps[0]["properties"]
|
||||||
|
except (RequestException, IndexError):
|
||||||
|
self.app = None
|
||||||
|
|
||||||
|
|
||||||
|
class TokensListView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "lemonauth/tokens.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
clients = ClientsDict()
|
||||||
|
for token in self.request.user.token_set.all():
|
||||||
|
client = clients[token.client_id]
|
||||||
|
client.count += 1
|
||||||
|
client.scopes |= set(token.scope.split(" "))
|
||||||
|
context.update({"clients": clients, "title": "tokens"})
|
||||||
|
return context
|
9
lemonauth/views/tokens/revoke.py
Normal file
9
lemonauth/views/tokens/revoke.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
|
||||||
|
class TokensRevokeView(LoginRequiredMixin, View):
|
||||||
|
def delete(self, request, client_id: str):
|
||||||
|
request.user.token_set.filter(client_id=client_id).delete()
|
||||||
|
return HttpResponse(status=204)
|
BIN
lemoncurry.paw
BIN
lemoncurry.paw
Binary file not shown.
|
@ -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)
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
||||||
from django.urls import reverse
|
|
||||||
from jinja2 import Environment
|
|
||||||
|
|
||||||
from compressor.contrib.jinja2ext import CompressorExtension
|
|
||||||
from django_activeurl.ext.django_jinja import ActiveUrl
|
|
||||||
|
|
||||||
from entries.kinds import all as entry_kinds
|
|
||||||
from .utils import load_package_json
|
|
||||||
|
|
||||||
|
|
||||||
def environment(**options):
|
|
||||||
env = Environment(
|
|
||||||
extensions=[ActiveUrl, CompressorExtension],
|
|
||||||
trim_blocks=True,
|
|
||||||
lstrip_blocks=True,
|
|
||||||
**options
|
|
||||||
)
|
|
||||||
env.globals.update({
|
|
||||||
'entry_kinds': entry_kinds,
|
|
||||||
'package': load_package_json(),
|
|
||||||
'static': staticfiles_storage.url,
|
|
||||||
'url': reverse,
|
|
||||||
})
|
|
||||||
return env
|
|
43
lemoncurry/jinja2/__init__.py
Normal file
43
lemoncurry/jinja2/__init__.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from jinja2 import Environment
|
||||||
|
|
||||||
|
from compressor.contrib.jinja2ext import CompressorExtension
|
||||||
|
from django_activeurl.ext.django_jinja import ActiveUrl
|
||||||
|
|
||||||
|
from entries.kinds import all as entry_kinds
|
||||||
|
from wellknowns.favicons import icons as favicons
|
||||||
|
|
||||||
|
from .ago import ago
|
||||||
|
from .markdown import markdown
|
||||||
|
from ..theme import color as theme_color
|
||||||
|
from ..utils import friendly_url, load_package_json
|
||||||
|
|
||||||
|
|
||||||
|
def environment(**options):
|
||||||
|
env = Environment(
|
||||||
|
extensions=[ActiveUrl, CompressorExtension],
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
**options
|
||||||
|
)
|
||||||
|
env.filters.update(
|
||||||
|
{
|
||||||
|
"ago": ago,
|
||||||
|
"friendly_url": friendly_url,
|
||||||
|
"markdown": markdown,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
env.globals.update(
|
||||||
|
{
|
||||||
|
"entry_kinds": entry_kinds,
|
||||||
|
"favicons": favicons,
|
||||||
|
"package": load_package_json(),
|
||||||
|
"settings": settings,
|
||||||
|
"static": staticfiles_storage.url,
|
||||||
|
"theme_color": theme_color,
|
||||||
|
"url": reverse,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return env
|
9
lemoncurry/jinja2/ago.py
Normal file
9
lemoncurry/jinja2/ago.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from ago import human
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def ago(dt: datetime) -> str:
|
||||||
|
# We have to convert the datetime we get to local time first, because ago
|
||||||
|
# just strips the timezone from a timezone-aware datetime.
|
||||||
|
dt = dt.astimezone()
|
||||||
|
return human(dt, precision=1, past_tense="{}", abbreviate=True)
|
23
lemoncurry/jinja2/bleach.py
Normal file
23
lemoncurry/jinja2/bleach.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from bleach.sanitizer import Cleaner, ALLOWED_TAGS
|
||||||
|
from bleach.linkifier import LinkifyFilter
|
||||||
|
from jinja2 import pass_eval_context
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
TAGS = ["cite", "code", "details", "p", "pre", "img", "span", "summary"]
|
||||||
|
TAGS.extend(ALLOWED_TAGS)
|
||||||
|
ATTRIBUTES = {
|
||||||
|
"a": ["href", "title", "class"],
|
||||||
|
"details": ["open"],
|
||||||
|
"img": ["alt", "src", "title"],
|
||||||
|
"span": ["class"],
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaner = Cleaner(tags=TAGS, attributes=ATTRIBUTES, filters=(LinkifyFilter,))
|
||||||
|
|
||||||
|
|
||||||
|
@pass_eval_context
|
||||||
|
def bleach(ctx, html):
|
||||||
|
res = cleaner.clean(html)
|
||||||
|
if ctx.autoescape:
|
||||||
|
res = Markup(res)
|
||||||
|
return res
|
|
@ -1,37 +1,58 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html{% block html_attr %} dir="ltr" lang="en"{% endblock %}>
|
<html{% block html_attr %} dir="ltr" lang="en" data-bs-theme="dark"{% 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">
|
||||||
|
|
||||||
<title class="p-name">{% if title %}{{ title }} ~ {% endif %}{{ request.site.name }}</title>
|
<title class="p-name">{% if title %}{{ title }} ~ {% endif %}{{ request.site.name }}</title>
|
||||||
|
{% if atom is defined %}
|
||||||
|
<link rel="alternate" type="application/atom+xml" href="{{ atom }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% if rss is defined %}
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="{{ rss }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
|
<base href="{{ request.build_absolute_uri(url('home:index')) }}" />
|
||||||
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
|
<link rel="authorization_endpoint" href="{{ url('lemonauth:indie') }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/monokai.min.css"
|
<link rel="canonical" href="{{ request.build_absolute_uri() }}" />
|
||||||
integrity="sha384-bHqbpRh/XW+phptvH9nQvMKHwPH1ZbOxpIeAB2D2OIEL4Ni7aZzZgMFpsRra+v1g" crossorigin="anonymous">
|
<link rel="hub" href="{{ settings.PUSH_HUB }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/openwebicons@1.4.3/css/openwebicons.min.css"
|
<link rel="manifest" href="{{ url('wellknowns:manifest') }}" />
|
||||||
integrity="sha384-Ljk0G9f8GyEhAzrdHNkQc89A/Kpq+sy09gejdAPyMyTDnPe4aDfS/ppZ/rDGM0Y9" crossorigin="anonymous">
|
<link rel="micropub" href="{{ url('micropub:micropub') }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@2.5.2/dist/tippy.css"
|
<link rel="token_endpoint" href="{{ url('lemonauth:token') }}" />
|
||||||
integrity="sha384-JsezPuW/bNd38848K5/8rIEbB+23QGQ8faCF8xEmjFT3i1qujgGiewfYevzMO3J1" crossorigin="anonymous">
|
|
||||||
|
<meta name="generator" content="{{ package.name }} {{ package.version }}" />
|
||||||
|
<meta name="theme-color" content="{{ theme_color(3) }}" />
|
||||||
|
{% for i in favicons %}
|
||||||
|
<link rel="{{ i.rel }}" type="{{ i.mime }}" sizes="{{ i.sizes }}" href="{{ i.url }}" />
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/monokai.min.css"
|
||||||
|
integrity="sha384-88Jvj9Q2LiBDwL7w3yciRTcH5q2zzvMFYIm4xX9/evqxJsxA33Xk9XYKcvUlPITo" crossorigin="anonymous">
|
||||||
|
<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">
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@3.4.1/dist/tippy.css"
|
||||||
|
integrity="sha384-hm3Wtrva6FibonAOqHHXSpMxvGbz2g7l5FK5avbuNviir5MK6Ap4o3EOohztzHHm" crossorigin="anonymous">
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link rel="stylesheet" type="text/stylus" href="{{ static('lemoncurry/css/layout.styl') }}">
|
<link rel="stylesheet" type="text/stylus" href="{{ static('lemoncurry/css/layout.styl') }}">
|
||||||
{% block styles %}{% endblock %}
|
{% block styles %}{% endblock %}
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|
||||||
<script type="text/javascript" defer src="https://use.fontawesome.com/releases/v5.0.13/js/all.js"
|
<script type="text/javascript" defer src="https://kit.fontawesome.com/a3aade9b41.js" crossorigin="anonymous"></script>
|
||||||
integrity="sha384-xymdQtn1n3lH2wcu0qhcdaOpQwyoarkgLVxC/wZ5q7h9gHtxICrpcaSUfygqZGOe" crossorigin="anonymous"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body{% block body_attr %}{% endblock %}>
|
<body{% block body_attr %}{% endblock %}>
|
||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-md navbar-dark">
|
<nav class="navbar navbar-expand-md"><div class="container-fluid">
|
||||||
<a class="navbar-brand" ref="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">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% activeurl %}<div class="collapse navbar-collapse" id="navbar">
|
{% activeurl %}
|
||||||
|
<div class="collapse navbar-collapse" id="navbar">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% for kind in entry_kinds %}
|
{% for kind in entry_kinds %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
@ -45,6 +66,12 @@
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url('lemonauth:tokens') }}">
|
||||||
|
<i class="fas fa-cookie-bite fa-fw" aria-hidden="true"></i>
|
||||||
|
tokens
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url('admin:index') }}">
|
<a class="nav-link" href="{{ url('admin:index') }}">
|
||||||
<i class="fas fa-cog fa-fw" aria-hidden="true"></i>
|
<i class="fas fa-cog fa-fw" aria-hidden="true"></i>
|
||||||
|
@ -66,9 +93,10 @@
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>{% endactiveurl %}
|
</div>
|
||||||
|
{% endactiveurl %}
|
||||||
|
|
||||||
</nav>
|
</div></nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
@ -78,19 +106,54 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>all content licensed under <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0</a></p>
|
<p>all content licensed under <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0</a></p>
|
||||||
|
{% if entries is defined and entries.has_other_pages() %}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination">
|
||||||
|
{% if entries.has_previous() %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" rel="prev" href="{{ entries.reverse(entries.previous_page_number()) }}">
|
||||||
|
<i class="fas fa-step-backward" aria-hidden="true"></i> <span class="sr-only">previous page</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% for i in entries.paginator.page_range %}
|
||||||
|
{% if i == entries.number %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }} <span class="sr-only">(current page)</span></span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ entries.reverse(i) }}">{{ i }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if entries.has_next() %}
|
||||||
|
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" rel="next" href="{{ entries.reverse(entries.next_page_number()) }}">
|
||||||
|
<i class="fas fa-step-forward" aria-hidden="true"></i> <span class="sr-only">next page</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
<p>powered by <a rel="code-repository" href="{{ package.repository }}/src/tag/v{{ package.version }}">{{ package.name }} {{ package.version }}</a></p>
|
<p>powered by <a rel="code-repository" href="{{ package.repository }}/src/tag/v{{ package.version }}">{{ package.name }} {{ package.version }}</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"></script>
|
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" crossorigin="anonymous"
|
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
|
integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"
|
<script src="https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T"></script>
|
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" crossorigin="anonymous"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-ZeLYJ2PNSQjvogWP559CDAf02Qb8FE5OyQicqtz/+UhZutbrwyr87Be7NPH/RgyC"></script>
|
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"></script>
|
||||||
<script src="https://unpkg.com/tippy.js@2.5.2/dist/tippy.standalone.min.js" crossorigin="anonymous"
|
<script src="https://unpkg.com/tippy.js@6.3.7/dist/tippy-bundle.umd.js" crossorigin="anonymous"
|
||||||
integrity="sha384-VEMCz3fC5atUNN+ezSHq2AZIBciT3aWGEZsStnW58gtO9PYb3wenWsYNoxLTbi/M"></script>
|
integrity="sha384-dtMr4wkcxQWUqsJFgElu4AttgIhOsjr2vYIzP2mv0MZbD/uJ6OHxFdbgE3MOKabN"></script>
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
hljs.initHighlightingOnLoad();
|
hljs.initHighlightingOnLoad();
|
||||||
|
|
18
lemoncurry/jinja2/markdown.py
Normal file
18
lemoncurry/jinja2/markdown.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from jinja2 import pass_eval_context
|
||||||
|
from markdown import Markdown
|
||||||
|
|
||||||
|
from .bleach import bleach
|
||||||
|
|
||||||
|
md = Markdown(
|
||||||
|
extensions=(
|
||||||
|
"extra",
|
||||||
|
"sane_lists",
|
||||||
|
"smarty",
|
||||||
|
"toc",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_eval_context
|
||||||
|
def markdown(ctx, source):
|
||||||
|
return bleach(ctx, md.reset().convert(source))
|
16
lemoncurry/middleware.py
Normal file
16
lemoncurry/middleware.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseException(Exception):
|
||||||
|
def __init__(self, response: HttpResponse) -> None:
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseExceptionMiddleware(MiddlewareMixin):
|
||||||
|
def process_exception(
|
||||||
|
self, request: HttpRequest, exception: Exception
|
||||||
|
) -> HttpResponse:
|
||||||
|
if isinstance(exception, ResponseException):
|
||||||
|
return exception.response
|
||||||
|
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 = [] # type: List[str]
|
ALLOWED_HOSTS: 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,109 +58,104 @@ 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.admin',
|
"django.contrib.admindocs",
|
||||||
'django.contrib.admindocs',
|
"django.contrib.auth",
|
||||||
'django.contrib.auth',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.humanize",
|
||||||
'django.contrib.humanize',
|
"django.contrib.sessions",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sites",
|
||||||
'django.contrib.sites',
|
"django.contrib.sitemaps",
|
||||||
'django.contrib.sitemaps',
|
"django.contrib.messages",
|
||||||
'django.contrib.messages',
|
"django.contrib.staticfiles",
|
||||||
'django.contrib.staticfiles',
|
"annoying",
|
||||||
|
"compressor",
|
||||||
'analytical',
|
"computed_property",
|
||||||
'annoying',
|
"corsheaders",
|
||||||
'compressor',
|
"debug_toolbar",
|
||||||
'computed_property',
|
"django_activeurl",
|
||||||
'corsheaders',
|
"django_agent_trust",
|
||||||
'debug_toolbar',
|
"django_extensions",
|
||||||
'django_activeurl',
|
"django_otp",
|
||||||
'django_agent_trust',
|
"django_otp.plugins.otp_static",
|
||||||
'django_extensions',
|
"django_otp.plugins.otp_totp",
|
||||||
'django_otp',
|
"django_rq",
|
||||||
'django_otp.plugins.otp_static',
|
"meta",
|
||||||
'django_otp.plugins.otp_totp',
|
"entries",
|
||||||
'django_rq',
|
"home",
|
||||||
'favicon',
|
"lemonauth",
|
||||||
'meta',
|
"lemonshort",
|
||||||
|
"micropub",
|
||||||
'entries',
|
"users",
|
||||||
'home',
|
"webmention",
|
||||||
'lemonauth',
|
"wellknowns",
|
||||||
'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",
|
||||||
]
|
]
|
||||||
|
|
||||||
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": {
|
||||||
'PARSER_CLASS': 'redis.connection.HiredisParser',
|
"SERIALIZER": "lemoncurry.msgpack.MSGPackModernSerializer",
|
||||||
'SERIALIZER': 'lemoncurry.msgpack.MSGPackModernSerializer',
|
|
||||||
},
|
},
|
||||||
'VERSION': 2,
|
"VERSION": 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,51 +163,51 @@ 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"),
|
||||||
'CONN_MAX_AGE': 3600
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -224,21 +219,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/
|
||||||
|
@ -250,28 +245,25 @@ 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,5 +1,7 @@
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ["*"]
|
||||||
META_SITE_DOMAIN = '00dani.dev'
|
META_SITE_DOMAIN = "00dani.lo"
|
||||||
META_FB_APPID = '142105433189339'
|
META_FB_APPID = "142105433189339"
|
||||||
|
STATIC_URL = "https://static.00dani.lo/"
|
||||||
|
MEDIA_URL = "https://media.00dani.lo/"
|
||||||
|
|
|
@ -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 + 'media/'
|
MEDIA_URL = STATIC_URL + "m/"
|
||||||
META_SITE_DOMAIN = '00dani.me'
|
META_SITE_DOMAIN = "00dani.me"
|
||||||
META_FB_APPID = '145311792869199'
|
META_FB_APPID = "145311792869199"
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
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")
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
$monokai_bg = #272822
|
$monokai_bg = #272822
|
||||||
$sm = 576px
|
|
||||||
$md = 768px
|
|
||||||
$lg = 992px
|
|
||||||
$xl = 1200px
|
|
||||||
|
|
||||||
html
|
html
|
||||||
background-color $base00
|
background-color $base00
|
||||||
|
|
||||||
a
|
a
|
||||||
color $base0D
|
color $base0D
|
||||||
|
text-decoration none
|
||||||
&:hover
|
&:hover
|
||||||
color $base0C
|
color $base0C
|
||||||
|
|
||||||
|
@ -27,19 +24,21 @@ code, pre, .code, .pre
|
||||||
border-color $base00
|
border-color $base00
|
||||||
color $base07
|
color $base07
|
||||||
|
|
||||||
|
.list-group-item
|
||||||
|
background-color $base03
|
||||||
|
|
||||||
[class^="openwebicons-"], [class*=" openwebicons-"]
|
[class^="openwebicons-"], [class*=" openwebicons-"]
|
||||||
&::before
|
&::before
|
||||||
text-decoration none
|
text-decoration none
|
||||||
line-height 1
|
line-height 1
|
||||||
|
|
||||||
for placement in top bottom left right
|
.tippy-box[data-theme~='dark']
|
||||||
.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
|
||||||
|
@ -66,7 +65,7 @@ body
|
||||||
|
|
||||||
|
|
||||||
> main
|
> main
|
||||||
padding 2rem
|
padding 2rem 1rem
|
||||||
width 100%
|
width 100%
|
||||||
flex 1
|
flex 1
|
||||||
display flex
|
display flex
|
||||||
|
@ -105,6 +104,12 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,20 @@ const {safeLoad} = require('js-yaml');
|
||||||
|
|
||||||
const themePath = join(__dirname, '..', '..', 'base16-materialtheme-scheme', 'material-darker.yaml');
|
const themePath = join(__dirname, '..', '..', 'base16-materialtheme-scheme', 'material-darker.yaml');
|
||||||
|
|
||||||
|
const breakpoints = {
|
||||||
|
sm: 576,
|
||||||
|
md: 768,
|
||||||
|
lg: 992,
|
||||||
|
xl: 1200,
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = function() {
|
module.exports = function() {
|
||||||
const theme = safeLoad(readFileSync(themePath, 'utf8'));
|
const theme = safeLoad(readFileSync(themePath, 'utf8'));
|
||||||
return function(style) {
|
return function(style) {
|
||||||
|
for (let key in breakpoints) {
|
||||||
|
style.define('$' + key, new stylus.nodes.Unit(breakpoints[key], 'px'));
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
const key = 'base0' + i.toString(16).toUpperCase();
|
const key = 'base0' + i.toString(16).toUpperCase();
|
||||||
const hex = theme[key];
|
const hex = theme[key];
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
{% load analytical compress favicon lemoncurry_tags meta static theme_colour %}<!doctype html>
|
|
||||||
<html dir="ltr" lang="en" class="{% block html_class %}{% endblock %}">
|
|
||||||
<head{% meta_namespaces %}>{% site_name as site_name %}{% request_uri request as uri %}{% request_origin request as origin %}
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
|
||||||
|
|
||||||
<base href="{{ origin }}" />
|
|
||||||
<link rel="canonical" href="{{ uri }}" />
|
|
||||||
<title class="p-name">{% if title %}{{ title }} ~ {% endif %}{{ site_name }}</title>
|
|
||||||
|
|
||||||
{% analytical_head_top %}
|
|
||||||
{% if atom %}<link rel="alternate" type="application/atom+xml" href="{{ atom }}" />{% endif %}
|
|
||||||
{% if rss %}<link rel="alternate" type="application/rss+xml" href="{{ rss }}" /> {% endif %}
|
|
||||||
{% block head %}{% endblock %}
|
|
||||||
|
|
||||||
<link rel="authorization_endpoint" href="{{ origin }}{% url 'lemonauth:indie' %}" />
|
|
||||||
<link rel="token_endpoint" href="{{ origin }}{% url 'lemonauth:token' %}" />
|
|
||||||
<link rel="micropub" href="{{ origin }}{% url 'micropub:micropub' %}" />
|
|
||||||
<link rel="openid.delegate" href="{{ uri }}" />
|
|
||||||
<link rel="openid.server" href="https://openid.indieauth.com/openid" />
|
|
||||||
|
|
||||||
<link rel="hub" href="{% get_push_hub %}" />
|
|
||||||
<link rel="self" href="{{ uri }}" />
|
|
||||||
|
|
||||||
<link rel="manifest" href="{% url 'wellknowns:manifest' %}" />
|
|
||||||
<meta name="theme-color" content="{% theme_colour 2 %}" />
|
|
||||||
{% get_package_json as package %}
|
|
||||||
<meta name="generator" content="{{ package.name }} {{ package.version }}" />
|
|
||||||
|
|
||||||
<meta property="og:url" content="{{ uri }}" />
|
|
||||||
<meta property="og:title" content="{% firstof title site_name %}" />
|
|
||||||
{% include 'meta/meta.html' %}
|
|
||||||
{% get_favicons 'favicon/' %}
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
|
|
||||||
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/monokai.min.css"
|
|
||||||
integrity="sha384-bHqbpRh/XW+phptvH9nQvMKHwPH1ZbOxpIeAB2D2OIEL4Ni7aZzZgMFpsRra+v1g" crossorigin="anonymous" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/openwebicons@1.4.3/css/openwebicons.min.css"
|
|
||||||
integrity="sha384-Ljk0G9f8GyEhAzrdHNkQc89A/Kpq+sy09gejdAPyMyTDnPe4aDfS/ppZ/rDGM0Y9" crossorigin="anonymous" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tippy.js@2.5.2/dist/tippy.css"
|
|
||||||
integrity="sha384-JsezPuW/bNd38848K5/8rIEbB+23QGQ8faCF8xEmjFT3i1qujgGiewfYevzMO3J1" crossorigin="anonymous" />
|
|
||||||
{% compress css %}
|
|
||||||
<link rel="stylesheet" type="text/stylus" href="{% static 'lemoncurry/css/layout.styl' %}" />
|
|
||||||
{% block styles %}{% endblock %}
|
|
||||||
{% endcompress %}
|
|
||||||
<script type="text/javascript" defer src="https://use.fontawesome.com/releases/v5.0.13/js/all.js"
|
|
||||||
integrity="sha384-xymdQtn1n3lH2wcu0qhcdaOpQwyoarkgLVxC/wZ5q7h9gHtxICrpcaSUfygqZGOe" crossorigin="anonymous"></script>
|
|
||||||
{% analytical_head_bottom %}
|
|
||||||
</head>
|
|
||||||
<body{% block body_attr %}{% endblock %}>
|
|
||||||
{% analytical_body_top %}
|
|
||||||
<header>
|
|
||||||
<nav class="navbar navbar-expand-md navbar-dark">
|
|
||||||
<a class="navbar-brand" rel="home" href="{% url 'home:index' %}">{% site_name %}</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
|
|
||||||
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbar">
|
|
||||||
{% block nav_left %}{% nav_left request %}{% endblock %}
|
|
||||||
{% block nav_right %}{% nav_right request %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{% if request.resolver_match.view_name %}
|
|
||||||
{% nav_crumbs request.resolver_match %}
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{% block main %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>all content licensed under <a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0</a></p>
|
|
||||||
|
|
||||||
{% if entries.has_other_pages %}
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination">
|
|
||||||
{% if entries.prev %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" rel="prev" href="{{ entries.prev.url }}">
|
|
||||||
<i class="fas fa-step-backward"></i><span class="sr-only">previous page</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for page in entries.pages %}
|
|
||||||
{% if page.current %}
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">{{ page.i }} <span class="sr-only">(current page)</span></span>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="{{ page.url }}">{{ page.i }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if entries.next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" rel="next" href="{{ entries.next.url }}">
|
|
||||||
<i class="fas fa-step-forward"></i><span class="sr-only">next page</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>powered by <a rel="code-repository" href="{{ package.repository }}/tree/v{{ package.version }}">{{ package.name }} {{ package.version }}</a></p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-ZeLYJ2PNSQjvogWP559CDAf02Qb8FE5OyQicqtz/+UhZutbrwyr87Be7NPH/RgyC"></script>
|
|
||||||
<script src="https://unpkg.com/tippy.js@2.5.2/dist/tippy.standalone.min.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-VEMCz3fC5atUNN+ezSHq2AZIBciT3aWGEZsStnW58gtO9PYb3wenWsYNoxLTbi/M"></script>
|
|
||||||
|
|
||||||
{% compress js %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
hljs.initHighlightingOnLoad();
|
|
||||||
</script>
|
|
||||||
{% block foot %}{% endblock %}
|
|
||||||
{% endcompress %}
|
|
||||||
{% analytical_body_bottom %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% load jsonify %}{% if crumbs %}
|
|
||||||
<nav class="breadcrumbs" aria-label="breadcrumb" role="navigation">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
{% for crumb in crumbs %}
|
|
||||||
<li class="breadcrumb-item"><a href="{{ crumb.url }}">{{ crumb.label }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{% firstof current.label title %}</li>
|
|
||||||
</ol>
|
|
||||||
<script type="application/ld+json">{{ breadcrumb_list | jsonify }}</script>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{% load activeurl %}{% activeurl %}<ul class="navbar-nav">
|
|
||||||
{% for item in items %}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ item.url }}">
|
|
||||||
<i class="{{ item.icon }} fa-fw"></i>
|
|
||||||
{{ item.label }}
|
|
||||||
</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>{% endactiveurl %}
|
|
|
@ -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,12 +5,13 @@ 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', 'p', 'pre', 'img', 'span']
|
tags = ["cite", "code", "details", "p", "pre", "img", "span", "summary"]
|
||||||
tags.extend(ALLOWED_TAGS)
|
tags.extend(ALLOWED_TAGS)
|
||||||
attributes = {
|
attributes = {
|
||||||
'a': ('href', 'title', 'class'),
|
"a": ["href", "title", "class"],
|
||||||
'img': ('alt', 'src', 'title'),
|
"details": ["open"],
|
||||||
'span': ('class',),
|
"img": ["alt", "src", "title"],
|
||||||
|
"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,4 +1,3 @@
|
||||||
|
|
||||||
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
|
||||||
|
@ -40,67 +39,71 @@ 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 = (MenuItem(
|
items = (
|
||||||
label=k.plural,
|
MenuItem(label=k.plural, icon=k.icon, url=("entries:index", (k,)))
|
||||||
icon=k.icon,
|
for k in kinds.all
|
||||||
url=('entries:index', (k,))
|
)
|
||||||
) for k in kinds.all)
|
return {"items": items, "request": request}
|
||||||
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(label='log out', icon='fas fa-sign-out-alt',
|
MenuItem(
|
||||||
url='lemonauth:logout'),
|
label="log out", icon="fas fa-sign-out-alt", url="lemonauth:logout"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
items = (
|
items = (
|
||||||
MenuItem(label='log in', icon='fas fa-sign-in-alt',
|
MenuItem(label="log in", icon="fas fa-sign-in-alt", url="lemonauth:login"),
|
||||||
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',
|
{
|
||||||
'position': i + 1,
|
"@type": "ListItem",
|
||||||
'item': {
|
"position": i + 1,
|
||||||
'@id': context['origin'] + crumb.url,
|
"item": {
|
||||||
'@type': 'WebPage',
|
"@id": context["origin"] + crumb.url,
|
||||||
'name': crumb.label
|
"@type": "WebPage",
|
||||||
|
"name": crumb.label,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} for i, crumb in enumerate(crumbs)]
|
for i, crumb in enumerate(crumbs)
|
||||||
item_list_element.append({
|
]
|
||||||
'@type': 'ListItem',
|
item_list_element.append(
|
||||||
'position': len(item_list_element) + 1,
|
{
|
||||||
'item': {
|
"@type": "ListItem",
|
||||||
'id': context['uri'],
|
"position": len(item_list_element) + 1,
|
||||||
'@type': 'WebPage',
|
"item": {
|
||||||
'name': current.label or context.get('title'),
|
"id": context["uri"],
|
||||||
|
"@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,12 +3,14 @@ from django import template
|
||||||
from markdown import Markdown
|
from markdown import Markdown
|
||||||
from .bleach import bleach
|
from .bleach import bleach
|
||||||
|
|
||||||
md = Markdown(extensions=(
|
md = Markdown(
|
||||||
'markdown.extensions.extra',
|
extensions=(
|
||||||
'markdown.extensions.headerid',
|
"extra",
|
||||||
'markdown.extensions.sane_lists',
|
"sane_lists",
|
||||||
'markdown.extensions.smarty',
|
"smarty",
|
||||||
))
|
"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,12 +4,14 @@ from yaml import safe_load
|
||||||
|
|
||||||
path = join(
|
path = join(
|
||||||
settings.BASE_DIR,
|
settings.BASE_DIR,
|
||||||
'lemoncurry', 'static',
|
"lemoncurry",
|
||||||
'base16-materialtheme-scheme', 'material-darker.yaml',
|
"static",
|
||||||
|
"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,33 +27,37 @@ 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('favicon.ico', RedirectView.as_view(
|
path(
|
||||||
url=settings.MEDIA_URL + 'favicon/favicon.ico')),
|
"favicon.ico",
|
||||||
path('micropub', include('micropub.urls')),
|
RedirectView.as_view(url=settings.MEDIA_URL + "favicon/favicon.ico"),
|
||||||
path('s/', include('lemonshort.urls')),
|
),
|
||||||
path('webmention', include('webmention.urls')),
|
path("micropub", include("micropub.urls")),
|
||||||
|
path("s/", include("lemonshort.urls")),
|
||||||
path('django-rq/', include('django_rq.urls')),
|
path("webmention", include("webmention.urls")),
|
||||||
path('sitemap.xml', sitemap.index, maps, name='sitemap'),
|
path("django-rq/", include("django_rq.urls")),
|
||||||
path('sitemaps/<section>.xml', sitemap.sitemap, maps,
|
path("sitemap.xml", sitemap.index, maps, name="sitemap"),
|
||||||
name='django.contrib.sitemaps.views.sitemap'),
|
path(
|
||||||
|
"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 += (
|
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),)
|
||||||
)
|
|
||||||
|
|
|
@ -6,24 +6,44 @@ from django.http import HttpResponse, JsonResponse
|
||||||
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from types import SimpleNamespace
|
from typing import Any, Dict, Optional
|
||||||
from urllib.parse import urlencode, urljoin
|
from urllib.parse import urlencode, urljoin, urlparse
|
||||||
|
|
||||||
from .templatetags.markdown import markdown
|
from .templatetags.markdown import markdown
|
||||||
|
|
||||||
cache = SimpleNamespace(package_json=None)
|
|
||||||
|
class PackageJson:
|
||||||
|
data: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def load(self) -> Dict[str, Any]:
|
||||||
|
if self.data is None:
|
||||||
|
with open(join(settings.BASE_DIR, "package.json")) as f:
|
||||||
|
self.data = json.load(f)
|
||||||
|
assert self.data is not None
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
|
||||||
def load_package_json():
|
PACKAGE = PackageJson()
|
||||||
if cache.package_json:
|
|
||||||
return cache.package_json
|
|
||||||
with open(join(settings.BASE_DIR, 'package.json')) as f:
|
def friendly_url(url):
|
||||||
cache.package_json = json.load(f)
|
if "//" not in url:
|
||||||
return cache.package_json
|
url = "//" + url
|
||||||
|
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
||||||
|
if path == "/":
|
||||||
|
return netloc
|
||||||
|
return "{}\u200B{}".format(netloc, path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_package_json() -> Dict[str, Any]:
|
||||||
|
return PACKAGE.load()
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
@ -36,19 +56,18 @@ def uri(request):
|
||||||
|
|
||||||
def form_encoded_response(content):
|
def form_encoded_response(content):
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
urlencode(content),
|
urlencode(content), content_type="application/x-www-form-urlencoded"
|
||||||
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)
|
||||||
|
@ -56,11 +75,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):
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
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,9 +7,11 @@ chars = ascii_uppercase + ascii_lowercase
|
||||||
conv = BaseConverter(chars)
|
conv = BaseConverter(chars)
|
||||||
|
|
||||||
|
|
||||||
def abc_to_id(abc):
|
class AbcIdConverter:
|
||||||
return int(conv.decode(abc))
|
regex = "[a-zA-Z]+"
|
||||||
|
|
||||||
|
def to_python(self, value: str) -> int:
|
||||||
|
return int(conv.decode(value))
|
||||||
|
|
||||||
def id_to_abc(id):
|
def to_url(self, value: int) -> str:
|
||||||
return conv.encode(id)
|
return conv.encode(value)
|
||||||
|
|
|
@ -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 id_to_abc
|
from .convert import AbcIdConverter
|
||||||
|
|
||||||
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)] + id_to_abc(entity.id)
|
return base + prefixes[type(entity)] + AbcIdConverter().to_url(entity.id)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue