Compare commits
No commits in common. "main" and "Release-v20201202" have entirely different histories.
main
...
Release-v2
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: domenicogentner
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,14 +1,8 @@
|
||||
.idea
|
||||
__pycache__/
|
||||
venv
|
||||
db/*.db
|
||||
db/*.db-shm
|
||||
db/*.db-wal
|
||||
*.old
|
||||
*.server
|
||||
/backup/
|
||||
labertasche.yaml
|
||||
/.secret
|
||||
/credentials.yaml
|
||||
*.bak
|
||||
smileys.yaml
|
||||
db/labertasche.db-shm
|
||||
db/labertasche.db-wal
|
||||
output
|
||||
/output/
|
||||
*.sql
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "static/css/sass/bulma"]
|
||||
path = static/css/sass/bulma
|
||||
url = git@github.com:jgthms/bulma.git
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2077 Domeniko Gentner <code@tuxstash.de>
|
||||
Copyright (c) 2020 Domeniko Gentner <code@tuxstash.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
2
Pipfile
2
Pipfile
@ -15,7 +15,7 @@ flask-login = "*"
|
||||
sqlalchemy = "*"
|
||||
requests = "*"
|
||||
py3-validate-email = "*"
|
||||
validators = "*"
|
||||
flask-migrate = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
150
Pipfile.lock
generated
150
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3a5cabc81d97143e23ec48af6c789d70ba16128d02104f8323100443b44e6b10"
|
||||
"sha256": "bda9276f38dcb49704cadb2f9097ecdfa1dafdd4e4b3d6666dfcb92d24f0ea57"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -16,6 +16,13 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"alembic": {
|
||||
"hashes": [
|
||||
"sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c",
|
||||
"sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"antispam": {
|
||||
"hashes": [
|
||||
"sha256:e188b424ea9b76c408a592a5ff60eb1280f45f26b404db4d5e96123f485de39b"
|
||||
@ -25,17 +32,17 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
|
||||
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
|
||||
],
|
||||
"version": "==2020.12.5"
|
||||
"version": "==2020.11.8"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==4.0.0"
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@ -44,13 +51,6 @@
|
||||
],
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
|
||||
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7",
|
||||
@ -89,6 +89,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"flask-migrate": {
|
||||
"hashes": [
|
||||
"sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732",
|
||||
"sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"flask-sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
|
||||
@ -118,6 +126,13 @@
|
||||
],
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
|
||||
"sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
|
||||
],
|
||||
"version": "==1.1.3"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
@ -163,6 +178,21 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.2.12"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"python-editor": {
|
||||
"hashes": [
|
||||
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
|
||||
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
|
||||
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
|
||||
],
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||
@ -184,11 +214,11 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.25.1"
|
||||
"version": "==2.25.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@ -199,47 +229,47 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:04f995fcbf54e46cddeb4f75ce9dfc17075d6ae04ac23b2bacb44b3bc6f6bf11",
|
||||
"sha256:0c6406a78a714a540d980a680b86654feadb81c8d0eecb59f3d6c554a4c69f19",
|
||||
"sha256:0c72b90988be749e04eff0342dcc98c18a14461eb4b2ad59d611b57b31120f90",
|
||||
"sha256:108580808803c7732f34798eb4a329d45b04c562ed83ee90f09f6a184a42b766",
|
||||
"sha256:1418f5e71d6081aa1095a1d6b567a562d2761996710bdce9b6e6ba20a03d0864",
|
||||
"sha256:17610d573e698bf395afbbff946544fbce7c5f4ee77b5bcb1f821b36345fae7a",
|
||||
"sha256:216ba5b4299c95ed179b58f298bda885a476b16288ab7243e89f29f6aeced7e0",
|
||||
"sha256:2ff132a379838b1abf83c065be54cef32b47c987aedd06b82fc76476c85225eb",
|
||||
"sha256:314f5042c0b047438e19401d5f29757a511cfc2f0c40d28047ca0e4c95eabb5b",
|
||||
"sha256:318b5b727e00662e5fc4b4cd2bf58a5116d7c1b4dd56ffaa7d68f43458a8d1ed",
|
||||
"sha256:3ab5b44a07b8c562c6dcb7433c6a6c6e03266d19d64f87b3333eda34e3b9936b",
|
||||
"sha256:426ece890153ccc52cc5151a1a0ed540a5a7825414139bb4c95a868d8da54a52",
|
||||
"sha256:491fe48adc07d13e020a8b07ef82eefc227003a046809c121bea81d3dbf1832d",
|
||||
"sha256:4a84c7c7658dd22a33dab2e2aa2d17c18cb004a42388246f2e87cb4085ef2811",
|
||||
"sha256:54da615e5b92c339e339fe8536cce99fe823b6ed505d4ea344852aefa1c205fb",
|
||||
"sha256:5a7f224cdb7233182cec2a45d4c633951268d6a9bcedac37abbf79dd07012aea",
|
||||
"sha256:61628715931f4962e0cdb2a7c87ff39eea320d2aa96bd471a3c293d146f90394",
|
||||
"sha256:62285607a5264d1f91590abd874d6a498e229d5840669bd7d9f654cfaa599bd0",
|
||||
"sha256:62fb881ba51dbacba9af9b779211cf9acff3442d4f2993142015b22b3cd1f92a",
|
||||
"sha256:68428818cf80c60dc04aa0f38da20ad39b28aba4d4d199f949e7d6e04444ea86",
|
||||
"sha256:6aaa13ee40c4552d5f3a59f543f0db6e31712cc4009ec7385407be4627259d41",
|
||||
"sha256:70121f0ae48b25ef3e56e477b88cd0b0af0e1f3a53b5554071aa6a93ef378a03",
|
||||
"sha256:715b34578cc740b743361f7c3e5f584b04b0f1344f45afc4e87fbac4802eb0a0",
|
||||
"sha256:758fc8c4d6c0336e617f9f6919f9daea3ab6bb9b07005eda9a1a682e24a6cacc",
|
||||
"sha256:7d4b8de6bb0bc736161cb0bbd95366b11b3eb24dd6b814a143d8375e75af9990",
|
||||
"sha256:81d8d099a49f83111cce55ec03cc87eef45eec0d90f9842b4fc674f860b857b0",
|
||||
"sha256:888d5b4b5aeed0d3449de93ea80173653e939e916cc95fe8527079e50235c1d2",
|
||||
"sha256:95bde07d19c146d608bccb9b16e144ec8f139bcfe7fd72331858698a71c9b4f5",
|
||||
"sha256:9bf572e4f5aa23f88dd902f10bb103cb5979022a38eec684bfa6d61851173fec",
|
||||
"sha256:bab5a1e15b9466a25c96cda19139f3beb3e669794373b9ce28c4cf158c6e841d",
|
||||
"sha256:bd4b1af45fd322dcd1fb2a9195b4f93f570d1a5902a842e3e6051385fac88f9c",
|
||||
"sha256:bde677047305fe76c7ee3e4492b545e0018918e44141cc154fe39e124e433991",
|
||||
"sha256:c389d7cc2b821853fb018c85457da3e7941db64f4387720a329bc7ff06a27963",
|
||||
"sha256:d055ff750fcab69ca4e57b656d9c6ad33682e9b8d564f2fbe667ab95c63591b0",
|
||||
"sha256:d53f59744b01f1440a1b0973ed2c3a7de204135c593299ee997828aad5191693",
|
||||
"sha256:f115150cc4361dd46153302a640c7fa1804ac207f9cc356228248e351a8b4676",
|
||||
"sha256:f1e88b30da8163215eab643962ae9d9252e47b4ea53404f2c4f10f24e70ddc62",
|
||||
"sha256:f8191fef303025879e6c3548ecd8a95aafc0728c764ab72ec51a0bdf0c91a341"
|
||||
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
|
||||
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
|
||||
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
|
||||
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
|
||||
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
|
||||
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
|
||||
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
|
||||
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
|
||||
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
|
||||
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
|
||||
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
|
||||
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
|
||||
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
|
||||
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
|
||||
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
|
||||
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
|
||||
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
|
||||
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
|
||||
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
|
||||
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
|
||||
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
|
||||
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
|
||||
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
|
||||
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
|
||||
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
|
||||
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
|
||||
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
|
||||
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
|
||||
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
|
||||
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
|
||||
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
|
||||
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
|
||||
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
|
||||
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
|
||||
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
|
||||
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
|
||||
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
|
||||
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.22"
|
||||
"version": "==1.3.20"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@ -248,14 +278,6 @@
|
||||
],
|
||||
"version": "==1.26.2"
|
||||
},
|
||||
"validators": {
|
||||
"hashes": [
|
||||
"sha256:0143dcca8a386498edaf5780cbd5960da1a4c85e0719f3ee5c9b41249c4fefbd",
|
||||
"sha256:37cd9a9213278538ad09b5b9f9134266e7c226ab1fede1d500e29e0a8fbb9ea6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
|
12
README.md
12
README.md
@ -72,7 +72,7 @@ The config looks like this:
|
||||
|
||||
Other options:
|
||||
|
||||
* [gunicorn](https://gunicorn.org/) + Apache/Nginx with Proxy Pass
|
||||
* [gunicorn](https://gunicorn.org/https://gunicorn.org/) + Apache/Nginx with Proxy Pass
|
||||
|
||||
Once you can see the administrative page, you can start integrating it into Hugo.
|
||||
|
||||
@ -101,12 +101,12 @@ Within that template the following structure is needed:
|
||||
{{ if (fileExists $location ) }}
|
||||
{{ $dataJ := getJSON $location }}
|
||||
{{ range $dataJ.comments }}
|
||||
{# HTML and template codes here #}
|
||||
{# This is to display replies to this comment, you can use them same variables #}
|
||||
{{ range where $dataJ.replies "replied_to" .comment_id }}
|
||||
{# HTML and template codes here for replies #}
|
||||
{{end}}
|
||||
{# HTML and template codes here #}
|
||||
{{ end }}
|
||||
{# This is to display replies to this comment, you can use them same variables #}
|
||||
{{ range where $dataJ.replies "replied_to" .comment_id }}
|
||||
{# HTML and template codes here for replies #}
|
||||
{{end}}
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
# How to use this example
|
||||
|
||||
Please run
|
||||
|
||||
`hugo --bind dev.localhost --baseURL http://dev.localhost --disableLiveReload`
|
||||
|
||||
in this folder to view this example. Point your web browser then to
|
||||
[dev.localhost](http://dev.localhost:1313).
|
||||
|
||||
Please also add
|
||||
|
||||
`127.0.0.1 dev.localhost`
|
||||
|
||||
to
|
||||
|
||||
* Windows: `%WINDIR%\system32\drivers\etc\hosts`
|
||||
* MAC/Linux: `/etc/hosts`
|
||||
|
||||
before running the example.
|
||||
|
||||
**There is another readme when you view the page.**
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
@ -1,4 +0,0 @@
|
||||
baseURL = "http://dev.localhost/"
|
||||
languageCode = "en-us"
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
ignoreErrors = ["error-disable-taxonomy"]
|
@ -1,94 +0,0 @@
|
||||
---
|
||||
title: "Labertasche minimal implementation example"
|
||||
date: 2020-12-03 09:00:00
|
||||
categories: blog
|
||||
---
|
||||
|
||||
This is a minimal example on how to implement Labertasche,
|
||||
using Bulma CSS. The CSS is not that important, however, it
|
||||
also shows how to utilize a modal dialogue to give your users
|
||||
a good experience.
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Setup
|
||||
|
||||
Please modify `mail_credentials.yaml` and make sure mail can be sent.
|
||||
Everything else is set up. You can run flask with pycharm or on a local
|
||||
server. It is up to you. I recommend using pycharm with the flask parameters
|
||||
`--host=dev.localhost --port=1314`. Make sure `dev.localhost` is in your
|
||||
hosts file and resolves to `127.0.0.1`. This is necessary to set a cookie domain.
|
||||
The server will not be able to run without.
|
||||
|
||||
## Where to start?
|
||||
|
||||
Start by reading `layouts/_default/baseof.html`. Notice the Javascript.
|
||||
It has the default `labertasche.js` included and a custom file, where I
|
||||
handle the callbacks. In production, you would concat these files using
|
||||
the Hugo asset pipeline. I've left them separate, so you can see what is custom and what is included.
|
||||
|
||||
The next stop should be `single.html`. There you can find the first go block
|
||||
needed, which adds the comments to each article in Hugo. Query for sections
|
||||
if you want to exclude certain sections or only allow one, e.g. `blog`.
|
||||
|
||||
Last but not least, `comments.html` in the partials folder. This is where
|
||||
basically all the magic happens. Read the javascript functions as they appear.
|
||||
Basically, all I am doing is to query the DOM elements and adding/removing
|
||||
classes as I go, to display certain things. There is also a quick explanation further down.
|
||||
|
||||
**Please note**: This version has a modified reply function, so it displays the
|
||||
hidden field with the reply id.
|
||||
This does not occur on the production version, but can be helpful for debugging.
|
||||
|
||||
## Javascript functions explained
|
||||
|
||||
This is a quick and short explanation of all javascript functions. Yes, you may use and modify them.
|
||||
|
||||
### labertasche_text_counter()
|
||||
|
||||
This function counts the amount of characters put into the text area. This is purely cosmetic and only the first
|
||||
filter. If users have disabled Javascript, they could circumvent this, so the server checks lengths too.
|
||||
|
||||
### labertasche_validate_mail()
|
||||
|
||||
This checks if the entered text is a valid mail address, with a regex match. This does not check if the
|
||||
domain exists or if the mail is _really_ an email, but that is done server side. It's only used to minimize false
|
||||
requests.
|
||||
|
||||
### labertasche_modal_hide()
|
||||
|
||||
This hides the modal dialog when the button on the modal is clicked.
|
||||
|
||||
### labertasche_comment_not_found()
|
||||
|
||||
When a comment is not valid, Labertasche will redirect to `dev.localhost?cnf=true`. This function shows a modal
|
||||
to inform the user about it. The JS for checking this parameter is in `baseof.html`.
|
||||
|
||||
### labertasche_comment_deleted()
|
||||
|
||||
Same as above, but with `dev.localhost?deleted=true`. This happens when a user deletes the comment via the link
|
||||
sent by mail.
|
||||
|
||||
### labertasche_post_callback(state)
|
||||
|
||||
This is the callback used via the Labertasche post function. It simply displays different modals when certain error
|
||||
codes are received. This is extremely useful, because you can inform your user about what is happening.
|
||||
|
||||
### labertasche_reply_callback(state, comment_id)
|
||||
|
||||
The callback for the reply callback. This does a little more, it displays a new button which the user can press to
|
||||
disable the reply and go to a parent comment. This is useful, because the user does not have to reload the site and
|
||||
therefore, does not need to type it all again, if the reply was done in error.
|
||||
|
||||
## Feedback
|
||||
|
||||
Hope this example makes it more comfortable to use Labertasche, please send me a mail or open an issue if anything
|
||||
is unclear.
|
||||
|
||||
## Try it out!
|
||||
|
||||
Scroll down and comment. This is only locally. Please note: If livereload is enabled, you may not see all dialogs.
|
||||
Turn livereload in Hugo off, if you want to see all of them:
|
||||
`--disableLiveReload`.
|
||||
|
||||
The example comments also will disappear when you comment, as they are not included in the database.
|
@ -1,61 +0,0 @@
|
||||
---
|
||||
title: "Stramine ad coniugiale hi Procne"
|
||||
date: 2020-12-04 08:00:00
|
||||
categories: blog
|
||||
---
|
||||
|
||||
## Qui velox repperit
|
||||
|
||||
Lorem markdownum spatio animas animorum Scyrumve Noctis gramine, fata, sit
|
||||
cives, cui mea. Abesto Thesea coniecit, in rictus *quem pedis caret* tutaeque
|
||||
sacra.
|
||||
|
||||
1. Urit deae freto nubifer oculi
|
||||
2. Ferrumque dilata quaeque
|
||||
3. Mihi luminis color tandem mirum quodque
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Videt accipiunt habet
|
||||
|
||||
Potest rapto: nata honores, primos, laudamus scrutantur in. Similis incursurus
|
||||
enim inritata postes, est caelo, sis *nondum*, spumantiaque licet tenens
|
||||
conbibitur excutis levis. Spargit dedere laetissimus liquidi, ad mergit, lintea
|
||||
*armis erunt esse* aratri, sideraque piceis.
|
||||
|
||||
Gestare petentes saevo multoque, ad *esset inhibere* omnibus, iter de Dixerat
|
||||
dira. Illi mora sed altera ferrum tibi, qui ignis aris nocti quatiens est.
|
||||
|
||||
## Amplexus stantes paciscor tot unum
|
||||
|
||||
Amens fugit membra flabat gemellam et Venus **protinus**. Gyaros esse tibi exhausta. Nulla sed
|
||||
numina linguae plura, prosiluit tamen, inscius, cui Phoebus circumspexit
|
||||
spatiumque **indigenae caecaque**.
|
||||
|
||||
if (server + san < w(ospf, webMemory, speed_column +
|
||||
sli_vaporware_definition)) {
|
||||
kernelBarFile = archieSmishing;
|
||||
base_png_click(rte_warm, dongle);
|
||||
}
|
||||
var midi = addressP.router(reader.koffice(dslClickKeylogger(rpm, 2,
|
||||
dataRom), 5));
|
||||
cd_media = koffice.dos.shortcut(3, html_boot.horizontal_trash_extension(
|
||||
subdirectory)) - tunneling(login, camelcase_cursor_opacity(
|
||||
flash_graphic, soap, serp_e_debug));
|
||||
sdk_lte_software(5);
|
||||
|
||||
## Attollit unde fingens
|
||||
|
||||
Longeque frangunt, spectant temptavit, reperta invito, tectis face vos mirabile
|
||||
Cycladas. Reliquit voverat, quattuor imago utinam crudelem rapta, nomina ullos
|
||||
latuit resurgere. Terraque vitae.
|
||||
|
||||
1. Senex et ipse esse cruentior caluere
|
||||
2. Sub quae
|
||||
3. Ubi sunt sedens cladis certamine maior hiscere
|
||||
|
||||
Aequantibus admota; cuncta sit quod fugias dextra certaminis oro ecce auditis
|
||||
pater. Fluunt herbas si est. Animam precesque esse gradumque videndo vultum,
|
||||
lapides, fera **corpora temperat**, adnuit fortis. Se et Ceycis; ille tergo
|
||||
frondes hospitibus quoque et? Dixit inposuit in cetera pinus triplices convicia;
|
||||
rupit intus suorum, et?
|
@ -1 +0,0 @@
|
||||
{"comments": [{"comment_id": 9, "email": "commenter9@", "content": "9 This is a test comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": null, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}], "replies": [{"comment_id": 10, "email": "commenter10@", "content": "10 This is a reply to the previous comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": 9, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}]}
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"comments": [
|
||||
{
|
||||
"comment_id": 1,
|
||||
"email": "commenter1@",
|
||||
"content": "This is an example comment with over 40 characters.",
|
||||
"created_on": "2020-12-04 12:23:14",
|
||||
"replied_to": null,
|
||||
"gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"
|
||||
}
|
||||
],
|
||||
"replies": [
|
||||
{
|
||||
"comment_id": 2,
|
||||
"email": "commenter2@",
|
||||
"content": "This is an example reply, to test if this works.",
|
||||
"created_on": "2020-12-04 12:24:19",
|
||||
"replied_to": 1,
|
||||
"gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"
|
||||
}
|
||||
]
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"comments": [], "replies": [{"comment_id": 2, "email": "commenter2@", "content": "2 This is a reply to the previous comment and has no actual value. Please test all methods on this.", "created_on": "2020-12-16 23:37:00", "replied_to": 1, "gravatar": "d9eef4df0ae5bfc1a9a9b1e39a99c07f"}]}
|
@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
<link rel="stylesheet" href="/css/labertasche.css" media="screen">
|
||||
<title>Labertasche Example</title>
|
||||
</head>
|
||||
<body class="is-family-sans-serif bg-darkslate">
|
||||
<section>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<a class="navbar-item is-size-4" href="/">
|
||||
Labertasche Example
|
||||
</a>
|
||||
<div class="navbar-start"></div>
|
||||
<div class="navbar-end"></div>
|
||||
</nav>
|
||||
<div class="p-4">
|
||||
{{ block "main" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
<script defer src="/js/labertasche.js"></script>
|
||||
<script defer src="/js/mysite.js"></script>
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Comments
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get("cnf") === "true"){
|
||||
labertasche_comment_not_found();
|
||||
}
|
||||
if (urlParams.get("deleted") === "true"){
|
||||
labertasche_comment_deleted();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- Modal for notifications -->
|
||||
<div class="modal" id="labertasche-modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="content p-5 mb-0 is-rounded bg-yayellow has-text-black">
|
||||
<p class="title">Labertasche</p>
|
||||
</div>
|
||||
<div class="content p-5 mb-0 bg-deepmatte has-text-white" id="labertasche-modal-text">
|
||||
|
||||
</div>
|
||||
<div class="content p-5 bg-deepmatte has-text-white border-top">
|
||||
<button onclick="labertasche_modal_hide();" class="button is-warning" aria-label="close">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="labertasche_modal_hide();" class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,7 +0,0 @@
|
||||
{{ block "frontpage_article" . }}
|
||||
<article class="p-3 bg-deepmatte brdr-yayellow">
|
||||
<p class="title has-text-white">{{ .Title }}</p>
|
||||
<p>{{ .Summary }}</p>
|
||||
<p><a href="{{.RelPermalink}}">Read the whole article...</a></p>
|
||||
</article>
|
||||
{{ end }}
|
@ -1,12 +0,0 @@
|
||||
{{ define "main" }}
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
{{ $last_article := (.Site.GetPage "blog" .Section).Pages.ByPublishDate }}
|
||||
{{ range last 2 $last_article }}
|
||||
<div class="mt-4">
|
||||
{{ .Render "frontpage_article" }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
@ -1,13 +0,0 @@
|
||||
{{ define "main" }}
|
||||
<article class="container p-3 bg-deepmatte brdr-yayellow">
|
||||
<p class="title has-text-white">{{ .Title }}</p>
|
||||
<div class="content has-text-justified has-text-white">{{ .Content }}</div>
|
||||
</article>
|
||||
<article class="container p-3 bg-deepmatte brdr-yayellow mt-5">
|
||||
<div>
|
||||
{{ $file := replaceRE "^(.*)[\\/]$" "data$1.json" .Page.RelPermalink }}
|
||||
{{ .Scratch.Set "location" $file }}
|
||||
{{ partial "partials/comments" . }}
|
||||
</div>
|
||||
</article>
|
||||
{{ end }}
|
@ -1,111 +0,0 @@
|
||||
{{ $location := .Scratch.Get "location" }}
|
||||
<!--suppress XmlDuplicatedId -->
|
||||
<h1 class="is-uppercase has-text-white">comments</h1>
|
||||
|
||||
<div class="mb-5" id="labertasche-comment-section" data-remote="http://dev.localhost:1314/comments/default/new">
|
||||
<div class="control is-expanded">
|
||||
<input onkeypress="labertasche_validate_mail();"
|
||||
onfocusout="labertasche_validate_mail();"
|
||||
maxlength="100"
|
||||
id="labertasche-mail"
|
||||
class="input"
|
||||
type="email"
|
||||
placeholder="joedoe@example.com">
|
||||
<label for="labertasche-mail"></label>
|
||||
</div>
|
||||
<div class="control is-expanded mt-3">
|
||||
<textarea oninput="labertasche_text_counter();"
|
||||
id="labertasche-text"
|
||||
class="textarea"
|
||||
rows="5"
|
||||
maxlength="1000"
|
||||
placeholder="40 minimum characters, type something nice..."></textarea>
|
||||
<label for="labertasche-text"></label>
|
||||
<p id="labertasche-text-helper"
|
||||
class="help is-danger">Characters: <span id="labertasche-counter">0/1000</span></p>
|
||||
</div>
|
||||
<div class="control mt-3">
|
||||
<button onclick="labertasche_post_comment(this, labertasche_post_callback);"
|
||||
class="button is-warning px-6 mr-4 is-medium"
|
||||
id="labertasche-comment-button">
|
||||
<span>Comment</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<div class="media mb-5 brdr-yayellow my-shadow-subtle bg-compliment">
|
||||
<figure class="media-left ml-0 mb-0">
|
||||
<p class="image is-128x128">
|
||||
<img alt="gravatar portrait" src="/images/default.jpg">
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content mr-5 mt-2 has-text-left">
|
||||
Pinned by <span class="fg-yellow">admin@example.com</span>
|
||||
<br><br>
|
||||
<span class="mt-5 has-text-justified">
|
||||
<span>
|
||||
Come join the discussion and write something nice. You will have to confirm your comment by mail,
|
||||
so make sure it is legit and not a throwaway. Only the name part of it will be displayed, so
|
||||
don't worry about spam.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{ if (fileExists $location ) }}
|
||||
{{ $dataJ := getJSON $location }}
|
||||
{{ range $dataJ.comments }}
|
||||
<article>
|
||||
<div class="media mb-5 brdr-yayellow my-shadow-subtle bg-compliment">
|
||||
<figure class="media-left ml-0 mb-0">
|
||||
<p class="image is-128x128">
|
||||
<img alt="gravatar portrait" src="https://www.gravatar.com/avatar/{{.gravatar}}.jpg">
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content mr-5 mt-2">
|
||||
<a id="comment_{{.comment_id}}" href="#comment_{{.comment_id}}">#{{.comment_id}}</a>
|
||||
Posted by <span class="fg-yellow">{{.email}}</span> <small>on {{.created_on}}</small>
|
||||
<br><br>
|
||||
<span class="mt-5">
|
||||
{{.content}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="is-fullwidth bg-yayellow has-text-centered">
|
||||
<!--suppress JSUnresolvedVariable -->
|
||||
<a class="has-text-black" href="#labertasche-comment-section"
|
||||
onclick="labertasche_reply_to({{.comment_id}}, labertasche_reply_callback);">
|
||||
reply
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{{ range where $dataJ.replies "replied_to" .comment_id }}
|
||||
<article>
|
||||
<div class="media margin-left-128 mb-5 brdr-yayellow my-shadow-subtle bg-compliment">
|
||||
<figure class="media-left ml-0 mb-0">
|
||||
<p class="image is-128x128">
|
||||
<img alt="gravatar portrait" src="https://www.gravatar.com/avatar/{{.gravatar}}.jpg">
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content mr-5 mt-2">
|
||||
<a id="comment_{{.comment_id}}" href="#comment_{{.comment_id}}">#{{.comment_id}}</a>
|
||||
Posted by <span class="fg-yellow">{{.email}}</span> <small>on {{.created_on}}
|
||||
</small>
|
||||
<br><br>
|
||||
<span class="mt-5">
|
||||
{{.content}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
{{ end }}
|
||||
{{ end }}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.5 KiB |
@ -1,122 +0,0 @@
|
||||
//**********************************************************************************
|
||||
// * _author : Domeniko Gentner
|
||||
// * _mail : code@tuxstash.de
|
||||
// * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
// * _license : This project is under MIT License
|
||||
// *********************************************************************************/
|
||||
|
||||
/*
|
||||
|
||||
//Callback example for post. Possible messages:
|
||||
// post-min-length
|
||||
// post-max-length
|
||||
// post-invalid-json
|
||||
// post-duplicate
|
||||
// post-internal-server-error
|
||||
// post-success
|
||||
// post-before-fetch
|
||||
function labertasche_callback(state)
|
||||
{
|
||||
if (state === "post-before-fetch"){
|
||||
|
||||
}
|
||||
if (state === "post-min-length"){
|
||||
|
||||
}
|
||||
if (state === "post-success"){
|
||||
|
||||
}
|
||||
if (state === "post-fetch-exception" || state === "post-internal-server-error"){
|
||||
|
||||
}
|
||||
if (state === "post-invalid-email"){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for initiating and cancelling replies.
|
||||
// Posstible message: 'on' and 'off'
|
||||
function labertasche_reply_callback()
|
||||
{
|
||||
if (state === "on"){
|
||||
}
|
||||
|
||||
if (state === "off"){
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
function labertasche_reply_to(comment_id, callback)
|
||||
{
|
||||
let comments = document.getElementById('labertasche-comment-section');
|
||||
if (comments){
|
||||
if (document.getElementById('labertasche-replied-to')){
|
||||
document.getElementById('labertasche-replied-to').remove();
|
||||
callback('off', comment_id);
|
||||
if (comment_id === -1){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let reply = document.createElement("input");
|
||||
reply.setAttribute("type", "text");
|
||||
reply.setAttribute("id", "labertasche-replied-to");
|
||||
//reply.classList.add("is-hidden");
|
||||
reply.value = comment_id;
|
||||
comments.appendChild(reply);
|
||||
callback('on', comment_id);
|
||||
}
|
||||
else{
|
||||
console.log("Missing text input with id labertasche-comment-section");
|
||||
}
|
||||
}
|
||||
|
||||
function labertasche_post_comment(btn, callback)
|
||||
{
|
||||
let remote = document.getElementById('labertasche-comment-section').dataset.remote;
|
||||
let comment = document.getElementById('labertasche-text').value.trim();
|
||||
let mail = document.getElementById('labertasche-mail').value.trim();
|
||||
let reply = document.getElementById('labertasche-replied-to');
|
||||
|
||||
if (mail.length <= 0 || comment.length < 40){
|
||||
callback('post-min-length');
|
||||
if(btn) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let reply_value = null
|
||||
if (reply != null){
|
||||
reply_value = reply.value;
|
||||
}
|
||||
|
||||
callback('post-before-fetch');
|
||||
fetch(remote,
|
||||
{
|
||||
mode:"cors",
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin':'*',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST",
|
||||
// use real location
|
||||
body: JSON.stringify({ "email": mail,
|
||||
"content": comment,
|
||||
"location": window.location.pathname,
|
||||
"replied_to": reply_value
|
||||
})
|
||||
})
|
||||
.then(async function(response){
|
||||
let result = await response.json();
|
||||
callback(result['status']);
|
||||
})
|
||||
.catch(function(exc){
|
||||
console.log(exc);
|
||||
callback('post-fetch-exception');
|
||||
})
|
||||
|
||||
// Don't reload the page
|
||||
return false;
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
|
||||
|
||||
function labertasche_text_counter()
|
||||
{
|
||||
let txt = document.getElementById('labertasche-text');
|
||||
let cntr = document.getElementById('labertasche-counter');
|
||||
let maxlen = txt.getAttribute("maxlength");
|
||||
let helper = document.getElementById("labertasche-text-helper");
|
||||
if (cntr && txt){
|
||||
cntr.innerText = txt.value.length + "/" + maxlen;
|
||||
if (txt.value.length > 40){
|
||||
if (helper.classList.contains('is-danger')){
|
||||
helper.classList.remove("is-danger");
|
||||
helper.classList.add("is-success");
|
||||
txt.classList.add('is-success');
|
||||
txt.classList.remove('is-danger');
|
||||
}
|
||||
}
|
||||
if (txt.value.length < 40){
|
||||
if (helper.classList.contains('is-success')){
|
||||
helper.classList.remove("is-success");
|
||||
helper.classList.add("is-danger");
|
||||
txt.classList.add('is-danger');
|
||||
txt.classList.remove('is-success');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labertasche_validate_mail()
|
||||
{
|
||||
let email = document.getElementById("labertasche-mail");
|
||||
let is_valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||
if (is_valid){
|
||||
email.classList.remove("is-danger")
|
||||
email.classList.add("is-success")
|
||||
}
|
||||
else{
|
||||
email.classList.add("is-danger")
|
||||
email.classList.remove("is-success")
|
||||
}
|
||||
}
|
||||
|
||||
function labertasche_modal_hide(url=null)
|
||||
{
|
||||
let modal = document.getElementById('labertasche-modal');
|
||||
if (modal != null){
|
||||
if (modal.classList.contains("is-active")){
|
||||
modal.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
if (!modal.dataset.url) {
|
||||
window.location.reload(true);
|
||||
}
|
||||
else{
|
||||
window.location = modal.dataset.url;
|
||||
}
|
||||
}
|
||||
|
||||
function labertasche_comment_not_found()
|
||||
{
|
||||
let modal = document.getElementById('labertasche-modal');
|
||||
let modal_text = document.getElementById('labertasche-modal-text');
|
||||
modal_text.innerText = "The link you followed was not valid. It either doesn't exist or was already used.";
|
||||
modal.setAttribute('data-url', window.location.protocol + "//" + window.location.host)
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
|
||||
function labertasche_comment_deleted()
|
||||
{
|
||||
let modal = document.getElementById('labertasche-modal');
|
||||
let modal_text = document.getElementById('labertasche-modal-text');
|
||||
modal_text.innerText = "Your comment has been deleted. Thank you for being here.";
|
||||
modal.setAttribute('data-url', window.location.protocol + "//" + window.location.host)
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
|
||||
/*
|
||||
post-min-length
|
||||
post-max-length
|
||||
post-invalid-json
|
||||
post-duplicate
|
||||
post-internal-server-error
|
||||
post-success
|
||||
post-before-fetch
|
||||
*/
|
||||
function labertasche_post_callback(state)
|
||||
{
|
||||
// Elements
|
||||
let modal = document.getElementById('labertasche-modal');
|
||||
let modal_text = document.getElementById('labertasche-modal-text');
|
||||
let button = document.getElementById('labertasche-comment-button');
|
||||
|
||||
if (state === "post-before-fetch"){
|
||||
button.classList.add("is-loading");
|
||||
}
|
||||
if (state === "post-min-length"){
|
||||
button.classList.remove("is-loading");
|
||||
modal_text.innerText = "Your comment was not entered because it is too short. Please write at least 40 characters."
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
if (state === "post-success"){
|
||||
button.classList.remove("is-loading");
|
||||
if (state['sendotp']) {
|
||||
modal_text.innerText = "Your comment was entered, but you need to confirm it, before it becomes active. Please check your mail!"
|
||||
}
|
||||
else{
|
||||
modal_text.innerText = "Your comment was successfully entered."
|
||||
}
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
if (state === "post-fetch-exception" || state === "post-internal-server-error"){
|
||||
button.classList.remove("is-loading");
|
||||
modal_text.innerText = "Your comment was not entered because there was an error, which was recorded and reported automatically.";
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
if (state === "post-duplicate"){
|
||||
button.classList.remove("is-loading");
|
||||
modal_text.innerText = "This comment was already made.";
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
if (state === "post-invalid-email"){
|
||||
button.classList.remove("is-loading");
|
||||
modal_text.innerText = "The email you have entered appears to be invalid. Please contact me if you think this was in error.";
|
||||
modal.classList.add('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
function labertasche_reply_callback(state, comment_id)
|
||||
{
|
||||
if (state === "on"){
|
||||
let comment_btn = document.getElementById('labertasche-comment-button');
|
||||
let parent = comment_btn.parentElement
|
||||
let new_btn = document.createElement("button");
|
||||
new_btn.classList.add("button");
|
||||
new_btn.classList.add("is-danger");
|
||||
new_btn.classList.add("is-medium");
|
||||
new_btn.classList.add("px-6");
|
||||
new_btn.setAttribute("id", "labertasche-cancel-reply");
|
||||
new_btn.onclick = function() { labertasche_reply_to(-1, labertasche_reply_callback); }
|
||||
new_btn.innerHTML = '<span>Cancel Reply</span>';
|
||||
parent.appendChild(new_btn);
|
||||
|
||||
comment_btn.innerHTML = "<span class='is-medium'>Reply to #" + comment_id + "</span>";
|
||||
}
|
||||
|
||||
if (state === "off"){
|
||||
console.log("off");
|
||||
let comment_btn = document.getElementById('labertasche-comment-button');
|
||||
comment_btn.innerHTML = "<span class='is-medium'>Comment</span>";
|
||||
let cancel = document.getElementById('labertasche-cancel-reply');
|
||||
if (cancel){
|
||||
cancel.remove();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from platform import system
|
||||
from labertasche.settings import LegacySettings
|
||||
from os import access, W_OK
|
||||
from sys import exit
|
||||
|
||||
print("""
|
||||
This will convert the current config file to the new system.
|
||||
This will create new files in /etc/labertasche:
|
||||
- .secret: The current secret of this app
|
||||
- credentials.yaml: This file will contain your selected username and password
|
||||
- labertasche.yaml: This file will contain the basic configuration
|
||||
- smileys.yaml: This will contain all your smileys.
|
||||
""")
|
||||
|
||||
base_path = '.'
|
||||
if system().lower() == 'linux':
|
||||
base_path = '/etc/labertasche/'
|
||||
|
||||
|
||||
if not access(base_path, W_OK):
|
||||
print(f"I do not have write access to this path: {base_path}. Please correct that and run the script again.")
|
||||
exit(1)
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
legacy = LegacySettings()
|
||||
legacy.convert_to_v2()
|
||||
except Exception as e:
|
||||
print("""
|
||||
Something went wrong. Your config is still available as labertasche.bak.
|
||||
Consider reporting this as a bug on github please. The message was:\n
|
||||
""")
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
print("""
|
||||
The upgrade is now complete. Your previous settings file has been stored as labertasche.bak.
|
||||
LEAVE THIS FILE AS IS UNTIL AFTER THE DATABASE UPGRADE!
|
||||
Please start the flask app and follow the database upgrade instructions.
|
||||
""")
|
@ -1,108 +0,0 @@
|
||||
/**********************************************************************************
|
||||
* _author : Domeniko Gentner
|
||||
* _mail : code@tuxstash.de
|
||||
* _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
* _license : This project is under MIT License
|
||||
**********************************************************************************
|
||||
*
|
||||
* This script generates sample data for the example implementation.
|
||||
* Feed it into the automatically created database with either DBBeaver
|
||||
* or the sqlite command line tool.
|
||||
*
|
||||
* Please note: Labertasche must have run once to create the database!
|
||||
*
|
||||
**********************************************************************************
|
||||
*/
|
||||
|
||||
/* delete old data */
|
||||
DELETE FROM t_comments;
|
||||
DELETE FROM t_projects;
|
||||
DELETE FROM t_email;
|
||||
DELETE FROM t_comments;
|
||||
DELETE FROM t_location;
|
||||
|
||||
/* Create example projects */
|
||||
INSERT INTO t_projects (id_project, name)
|
||||
VALUES
|
||||
(1, 'default'),
|
||||
(2, 'example.com'),
|
||||
(3, 'tuxstash.de'),
|
||||
(4, 'beispiel.de'),
|
||||
(5, 'labertasche.tuxstash.de')
|
||||
;
|
||||
|
||||
/* Create existing locations for each project */
|
||||
INSERT INTO t_location (id_location, location, project_id)
|
||||
VALUES
|
||||
(1, '/blog/stramine/', 1),
|
||||
(2, '/blog/readme/', 1),
|
||||
(3, '/blog/article-1/', 2),
|
||||
(4, '/blog/article-2/', 2),
|
||||
(5, '/blog/article-3/', 3),
|
||||
(6, '/blog/article-4/', 3),
|
||||
(7, '/blog/article-5/', 4),
|
||||
(8, '/blog/article-6/', 4),
|
||||
(9, '/blog/article-7/', 5),
|
||||
(10, '/blog/article-8/', 5)
|
||||
;
|
||||
|
||||
/* Create some emails that are blocked and allowed */
|
||||
INSERT INTO t_email (id_email, email, is_allowed, is_blocked, project_id)
|
||||
VALUES
|
||||
(1, "commenter1@example.com", true, false, 1),
|
||||
(2, "commenter2@example.com", false, true, 1),
|
||||
(3, "commenter3@example.com", true, false, 2),
|
||||
(4, "commenter4@example.com", false, true, 2),
|
||||
(5, "commenter5@example.com", true, false, 3),
|
||||
(6, "commenter6@example.com", false, true, 3),
|
||||
(7, "commenter7@example.com", true, false, 4),
|
||||
(8, "commenter8@example.com", false, true, 4),
|
||||
(9, "commenter9@example.com", true, false, 5),
|
||||
(10, "commenter10@example.com", false, true, 5)
|
||||
;
|
||||
|
||||
|
||||
/* Create some comments */
|
||||
INSERT INTO t_comments (comments_id, location_id, email, content, created_on, is_published, is_spam, spam_score, replied_to, confirmation, deletion, gravatar, project_id)
|
||||
VALUES
|
||||
(1, 1, 'commenter1@example.com', '1 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1),
|
||||
(2, 1, 'commenter2@example.com', '2 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 1, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1),
|
||||
(3, 2, 'commenter3@example.com', '3 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1),
|
||||
(4, 2, 'commenter4@example.com', '4 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 3, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 1),
|
||||
|
||||
(5, 3, 'commenter5@example.com', '5 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2),
|
||||
(6, 3, 'commenter6@example.com', '6 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 5, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2),
|
||||
(7, 4, 'commenter7@example.com', '7 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2),
|
||||
(8, 4, 'commenter8@example.com', '8 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 7, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 2),
|
||||
|
||||
(9, 5, 'commenter9@example.com', '9 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', false, true, 0.09, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3),
|
||||
(10, 5, 'commenter10@example.com', '10 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 9, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3),
|
||||
(11, 6, 'commenter11@example.com', '11 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.09, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3),
|
||||
(12, 6, 'commenter12@example.com', '12 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 11, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 3),
|
||||
|
||||
(13, 7, 'commenter13@example.com', '13 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4),
|
||||
(14, 7, 'commenter14@example.com', '14 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 13, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4),
|
||||
(15, 8, 'commenter15@example.com', '15 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4),
|
||||
(16, 8, 'commenter16@example.com', '16 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 16, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 4),
|
||||
|
||||
(17, 9, 'commenter17@example.com', '17 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5),
|
||||
(18, 9, 'commenter18@example.com', '18 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 18, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5),
|
||||
(19, 10, 'commenter19@example.com', '19 This is a test comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, NULL, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5),
|
||||
(20, 10, 'commenter20@example.com', '20 This is a reply to the previous comment and has no actual value. Please test all methods on this.', '2020-12-16 23:37:00.000000', true, false, 0.99, 19, NULL, NULL, 'd9eef4df0ae5bfc1a9a9b1e39a99c07f', 5)
|
||||
;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
{
|
||||
"html_language": "de",
|
||||
"browser_language": "de-DE",
|
||||
"ok": "ok",
|
||||
"link": "link",
|
||||
"cancel": "abbrechen",
|
||||
"dashboard": "dashboard",
|
||||
"username": "benutzername",
|
||||
"password": "passwort",
|
||||
"login": "login",
|
||||
"logout": "abmelden",
|
||||
"error": "fehler",
|
||||
"warning": "warnung",
|
||||
"comments": "kommentare",
|
||||
"unpublished": "unveröffentlicht",
|
||||
"published": "veröffentlicht",
|
||||
"spam": "spam",
|
||||
"edit": "editieren",
|
||||
"manage": "verwalten",
|
||||
"export": "export",
|
||||
"delete": "löschen",
|
||||
"new": "neu",
|
||||
"project": "projekt",
|
||||
"new_project": "Neues Projekt",
|
||||
"project_name": "Projekt Name",
|
||||
"statistics": "statistiken",
|
||||
"address": "adresse",
|
||||
"status": "status",
|
||||
"manage_mail": "Mail Addressen verwalten",
|
||||
"manage_comments": "Kommentare verwalten",
|
||||
"manage_comments_delete_comment": "Diesen Kommentar löschen",
|
||||
"manage_comments_delete_and_block": "Diesen Kommentar löschen und EMail Adresse blocken.",
|
||||
"manage_comments_allow_comment": "Diesen Kommentar erlauben, aber Email Adresse nicht freischalten.",
|
||||
"manage_comments_allow_and_approve": "Diesen Kommentar erlauben und Email Adresse freischalten.",
|
||||
"manage_spam": "Spam verwalten",
|
||||
"select_article": "Artikel auswählen",
|
||||
"hooray_no_spam": "Hurra, kein Spam!",
|
||||
"spam_score": "Spamerkennung",
|
||||
"tooltip_spam_score": "Je höher der Spam Wert ist, desto höher die Chance, dass es sich um Spam handelt.",
|
||||
"stats_label_regular_comments": "reguläre Kommentare",
|
||||
"stats_label_unpublished_comments": "unveröffentlichte Kommentare",
|
||||
"stats_last_7_days": "Aktivität der letzten 7 Tage",
|
||||
"stats_total_percentage": "Verhältnis von Kommentaren zu Spam",
|
||||
"select_project_to_manage": "Projekte:",
|
||||
"tooltip_create_new_project": "Neues Projekt erzeugen",
|
||||
"tooltip_delete_project": "Projekt und dazugehörige Daten löschen",
|
||||
"tooltip_edit_project": "Einstellungen und Daten des Projekts ändern",
|
||||
"tooltip_export_all_comments": "Alle Kommentare nach Hugo exportieren.<br>Wird normalerweise nicht benötigt.",
|
||||
"tooltip_manage_this_project": "Dieses Projekt verwalten",
|
||||
"placeholder_search_mail": "Mail Adressen durchsuchen",
|
||||
"export_all_comments": "Alle Kommentare exportieren",
|
||||
"export_warning_text": "Dies wird alle Kommentare neu exportieren. Normalerweise wird das nicht benötigt, kann aber in gewissen Situatonen hilfreich sein.",
|
||||
"delete_project_warning": "Du bist dabei ein Projekt zu löschen. Dies wird alle dazugehörigen Daten auch löschen. Bitte führe einen manuellen SQL Dump durch, falls du die Daten behalten willst.",
|
||||
"wish_to_proceed": "Möchtest du fortfahren?",
|
||||
"tooltip_email_blocked": "Email ist momentan gesperrt. Zum entsperren klicken.",
|
||||
"tooltip_email_allowed": "Email darf momentan ohne Bestätigung posten. Zum Sperren klicken.",
|
||||
"tooltip_delete_email": "Eintrag löschen. Email unterliegt wieder den normalen Regeln.",
|
||||
"javascript_required_field_empty": "Ein Pflichtfeld wurde leer gelassen!",
|
||||
"javascript_invalid_project_name": "Der Projektname ist nicht gültig, bitte nur Buchstaben und Zahlen verwenden!",
|
||||
"javascript_project_duplicate": "Ein Projekt mit diesem name existiert bereits!",
|
||||
"javascript_blogurl_invalid": "Die blog-url ist ungültig!",
|
||||
"javascript_output_nonexistent": "Der Ausgabe Pfad existiert nicht!",
|
||||
"javascript_gravatar_cache_nonexistent": "Der caching Pfad existiert nicht!",
|
||||
"javascript_exception": "Es gab eine unerwartet Ausnahme. Bitte eröffne ein issue auf Github!",
|
||||
"javascript_edit_project_modal_title": "Projekt %name% editieren",
|
||||
"description_hugo_url": "Die URL der Hugo Seite für dieses Projekt.",
|
||||
"tooltip_hugo_url": "Die URL sollte so aussehen: https://beispiel.de",
|
||||
"tooltip_project_name": "Bitte wähle einen einzigartigen Namen für das Projekt.",
|
||||
"description_output_path": "Hugo Datenverzeichnis",
|
||||
"tooltip_output_path": "Der Pfad zum Datenverzeichnis der Hugo Installation. Kann relativ sein.",
|
||||
"description_gravatar_cache": "Gravatar Bilder lokal speichern?",
|
||||
"tooltip_gravatar_cache": "Wenn dies aktiviert ist, wird Labertasche Gravatare herunterladen und hier speichern.",
|
||||
"tooltip_gravatar_dir": "Der Pfad zum Zwischenspeicher für Gravatare. Kann relativ sein.",
|
||||
"description_gravatar_dir": "Lokales Verzeichnis für Gravatar",
|
||||
"tooltip_gravatar_size": "Die Größe der Gravatare. Sollte ein vielfaches von 2 sein, bswp. 64, 128 oder 256.",
|
||||
"description_gravatar_size": "Gravatar Bildgröße",
|
||||
"description_send_otp": "OTP zur Veröffentlichung senden?",
|
||||
"tooltip_send_otp": "Wenn aktiviert, bekommt der User ein Einmalpasswort zugesendet, mit dem der Kommentar veröffentlich wird (empfohlen). Wenn deaktiviert, wird der Kommentar immer veröffentlicht (ausser Spam).",
|
||||
"description_enable_smileys": "Smiley Addon aktivieren?",
|
||||
"tooltip_enable_smileys": "Wenn aktiviert, werden simple Text Smileys mit Emojis ersetzt. In /etc/labertasche/smileys.yaml findest du die aktivierten Smileys.",
|
||||
"message_project_404": "Das angegebene Projekt wurde nicht gefunden. Wurde es vielleicht gelöscht? Wenn du denkst, dass dies ein Bug ist, melde es bitte auf Github!"
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
{
|
||||
"html_language": "en",
|
||||
"browser_language": "en-US",
|
||||
"ok": "ok",
|
||||
"cancel": "cancel",
|
||||
"link": "link",
|
||||
"dashboard": "dashboard",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"login": "login",
|
||||
"logout": "logout",
|
||||
"error": "error",
|
||||
"warning": "warning",
|
||||
"comments": "comments",
|
||||
"unpublished": "unpublished",
|
||||
"published": "published",
|
||||
"spam": "spam",
|
||||
"edit": "edit",
|
||||
"manage": "manage",
|
||||
"export": "export",
|
||||
"delete": "delete",
|
||||
"new": "new",
|
||||
"project": "project",
|
||||
"new_project": "new project",
|
||||
"statistics": "statistics",
|
||||
"address": "address",
|
||||
"status": "status",
|
||||
"project_name": "Projekt Name",
|
||||
"manage_mail": "manage mail addresses",
|
||||
"stats_label_regular_comments": "regular comments",
|
||||
"stats_label_unpublished_comments": "unpublished comments",
|
||||
"stats_last_7_days": "Activity last 7 days",
|
||||
"stats_total_percentage": "Total comment - spam ratio",
|
||||
"manage_comments": "manage comments",
|
||||
"manage_comments_delete_comment": "Delete this comment",
|
||||
"manage_comments_delete_and_block": "Delete this comment and block mail address",
|
||||
"manage_comments_allow_comment": "Approve this comment, don't approve mail",
|
||||
"manage_comments_allow_and_approve": "Approve this comment and approve mail",
|
||||
"select_article": "Select article",
|
||||
"select_project_to_manage": "Projects",
|
||||
"hooray_no_spam": "Hooray, no Spam!",
|
||||
"spam_score": "score",
|
||||
"tooltip_spam_score": "The higher the spam score is, the more likely it is spam",
|
||||
"tooltip_create_new_project": "Create a new project",
|
||||
"tooltip_delete_project": "Delete the project and all of its content",
|
||||
"tooltip_edit_project": "Edit the name of the project and it's properties",
|
||||
"tooltip_export_all_comments": "Export all comments to Hugo.<br>This is normally not needed.",
|
||||
"tooltip_manage_this_project": "Manage this project",
|
||||
"placeholder_search_mail": "Search mail",
|
||||
"tooltip_email_blocked": "Email is currently blocked. Click to unblock.",
|
||||
"tooltip_email_allowed": "Email is currently excempt from spam detection. Click to block.",
|
||||
"tooltip_delete_email": "Delete entry, Email has to follow the regular rules.",
|
||||
"export_all_comments": "Export all comments",
|
||||
"export_warning_text": "This will export all comments of this project to all locations. Usually this is not needed, but can be helpful, if you have imported backups or similar.",
|
||||
"wish_to_proceed": "Do you wish to proceed?",
|
||||
"delete_project_warning": "You are about to delete a project. All associated data will be unrecoverably lost! Please perform a manual sql dump if you would like to retain that data.",
|
||||
"javascript_required_field_empty": "A required field has been left empty!",
|
||||
"javascript_invalid_project_name": "The project name is not valid. Please only use alphanumeric characters!",
|
||||
"javascript_project_duplicate": "A project with this name already exists!",
|
||||
"javascript_blogurl_invalid": "The blog-url is invalid!",
|
||||
"javascript_output_nonexistent": "This output path does not exist!",
|
||||
"javascript_gravatar_cache_nonexistent": "The cache path does not exist!",
|
||||
"javascript_exception": "There was an unexpected exception. Please open an issue on Github!",
|
||||
"javascript_edit_project_modal_title": "Edit project %name%",
|
||||
"javascript_new_project_modal_title": "New Project",
|
||||
"description_hugo_url": "URL of your Hugo site for this project",
|
||||
"tooltip_hugo_url": "An URL is formed like this: https://example.com",
|
||||
"tooltip_project_name": "Please select an unique name for your project.",
|
||||
"description_output_path": "Hugo Data dir",
|
||||
"tooltip_output_path": "The path to the data directory of your Hugo installation. Path can be relative.",
|
||||
"description_gravatar_cache": "Cache Gravatar images locally?",
|
||||
"tooltip_gravatar_cache": "If enabled, Labertasche will download gravatars to this location",
|
||||
"tooltip_gravatar_dir": "The directory where to save the Gravatar images. Path can be relative.",
|
||||
"description_gravatar_dir": "Gravatar caching directory.",
|
||||
"tooltip_gravatar_size": "The numeric size of the images to download. Must be a power of 2, e.g 64, 128, 256",
|
||||
"description_gravatar_size": "Gravatar image size",
|
||||
"description_send_otp": "Send OTP to publish?",
|
||||
"tooltip_send_otp": "If enabled, the user will be mailed a one time password to publish the comment (recommended). If disabled, it will be published by default (except spam).",
|
||||
"description_enable_smileys": "Enable Smiley Addon?",
|
||||
"tooltip_enable_smileys": "If enabled, simple text Smileys will be replaced with Emojis. Please see /etc/labertasche/smileys.yaml for more.",
|
||||
"message_project_404": "The specified project was not found! Did you delete it? If you believe this to be a bug, please report it."
|
||||
}
|
@ -15,7 +15,6 @@
|
||||
// post-internal-server-error
|
||||
// post-success
|
||||
// post-before-fetch
|
||||
// post-project-not-found
|
||||
function labertasche_callback(state)
|
||||
{
|
||||
if (state === "post-before-fetch"){
|
||||
@ -37,7 +36,7 @@ function labertasche_callback(state)
|
||||
|
||||
// Callback for initiating and cancelling replies.
|
||||
// Posstible message: 'on' and 'off'
|
||||
function labertasche_reply_callback(state)
|
||||
function labertasche_reply_callback()
|
||||
{
|
||||
if (state === "on"){
|
||||
}
|
||||
@ -87,12 +86,6 @@ function labertasche_post_comment(btn, callback)
|
||||
return false;
|
||||
}
|
||||
|
||||
// When there is no reply, use null, otherwise an error occurs on chrome
|
||||
let reply_value = null
|
||||
if (reply != null){
|
||||
reply_value = reply.value;
|
||||
}
|
||||
|
||||
callback('post-before-fetch');
|
||||
fetch(remote,
|
||||
{
|
||||
@ -107,7 +100,7 @@ function labertasche_post_comment(btn, callback)
|
||||
body: JSON.stringify({ "email": mail,
|
||||
"content": comment,
|
||||
"location": window.location.pathname,
|
||||
"replied_to": reply_value
|
||||
"replied_to": reply.value
|
||||
})
|
||||
})
|
||||
.then(async function(response){
|
||||
|
46
labertasche.yaml
Normal file
46
labertasche.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
|
||||
system:
|
||||
web_url: "http://comments.example.com" # Url where the comment system is served
|
||||
blog_url: "http://myblog.example.com" # Url of your website
|
||||
cookie-domain: "dev.localhost" # Url where the comment system is served
|
||||
database_uri: "sqlite:///db/labertasche.db" # Database URI. Default is sqlite.
|
||||
secret: "123456" # CHANGE ME! THIS IS IMPORTANT!
|
||||
output: "/path/to/hugo_dir/data" # Base path for the output json
|
||||
debug: false # Leave this as is, this is for development.
|
||||
send_otp_to_publish: true # Disables confirmation w/ OTP via mail
|
||||
|
||||
gravatar:
|
||||
cache: true # Enable caching of gravatar images
|
||||
static_dir: "/path/to/hugo_dir//static/images/gravatar/" # Where to store cached images
|
||||
size: 256 # only applies if images are cached,
|
||||
# otherwise use ?s=size at the end of the gravatar url
|
||||
dashboard:
|
||||
username: "admin" # CHANGE ME!
|
||||
password: "admin" # CHANGE ME!
|
||||
|
||||
addons:
|
||||
smileys: true # Enable smiley replacements, set to false if unwanted
|
||||
|
||||
# If you want to expand this, please use this list:
|
||||
# https://www.w3schools.com/charsets/ref_emoji_smileys.asp
|
||||
# You need to both versions, upper and lowercase, if you want both to works
|
||||
# This is a simple search and replace action
|
||||
smileys:
|
||||
":)": "😀"
|
||||
":d": "😁"
|
||||
":D": "😁"
|
||||
";)": "😉"
|
||||
":p": "😋"
|
||||
":P": "😋"
|
||||
":8": "😎"
|
||||
"(:": "🙃"
|
||||
"$)": "🤑"
|
||||
":o": "😲"
|
||||
":O": "😲"
|
||||
|
@ -13,8 +13,7 @@ from labertasche import (
|
||||
blueprints,
|
||||
helper,
|
||||
mail,
|
||||
settings,
|
||||
language
|
||||
settings
|
||||
)
|
||||
|
||||
_all_ = [
|
||||
@ -23,6 +22,5 @@ _all_ = [
|
||||
blueprints,
|
||||
helper,
|
||||
mail,
|
||||
settings,
|
||||
language
|
||||
settings
|
||||
]
|
||||
|
@ -9,5 +9,4 @@
|
||||
from .bp_comments import bp_comments
|
||||
from .bp_login import bp_login
|
||||
from .bp_dashboard import bp_dashboard
|
||||
from .bp_jsconnector import bp_jsconnector
|
||||
from .bp_upgrades import bp_dbupgrades
|
||||
|
||||
|
@ -14,37 +14,30 @@ from flask_cors import cross_origin
|
||||
from sqlalchemy import exc
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import is_valid_json, default_timestamp, check_gravatar, export_location
|
||||
from labertasche.mail import Mail
|
||||
from labertasche.models import TComments, TLocation, TEmail, TProjects
|
||||
from labertasche.settings import Smileys
|
||||
from labertasche.mail import mail
|
||||
from labertasche.models import TComments, TLocation, TEmail
|
||||
from labertasche.settings import Settings
|
||||
from secrets import compare_digest
|
||||
|
||||
|
||||
# Blueprint
|
||||
bp_comments = Blueprint("bp_comments", __name__, url_prefix='/comments')
|
||||
|
||||
|
||||
# Route for adding new comments
|
||||
@bp_comments.route("/<name>/new", methods=['POST'])
|
||||
def check_and_insert_new_comment(name):
|
||||
|
||||
# Get project
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
|
||||
# Check refferer, this is not bullet proof
|
||||
if not compare_digest(request.origin, project.blogurl):
|
||||
return make_response(jsonify(status="not-allowed"), 403)
|
||||
|
||||
if not project:
|
||||
return make_response(jsonify(status="post-project-not-found"), 400)
|
||||
|
||||
if compare_digest(request.method, "POST"):
|
||||
smileys = Smileys()
|
||||
sender = Mail()
|
||||
@bp_comments.route("/new", methods=['POST'])
|
||||
@cross_origin()
|
||||
def check_and_insert_new_comment():
|
||||
if request.method == 'POST':
|
||||
settings = Settings()
|
||||
smileys = settings.smileys
|
||||
addons = settings.addons
|
||||
sender = mail()
|
||||
|
||||
# Check length of content and abort if too long or too short
|
||||
if request.content_length > 2048:
|
||||
return make_response(jsonify(status="post-max-length"), 400)
|
||||
if request.content_length == 0:
|
||||
if request.content_length <= 0:
|
||||
return make_response(jsonify(status="post-min-length"), 400)
|
||||
|
||||
# get json from request
|
||||
@ -55,8 +48,9 @@ def check_and_insert_new_comment(name):
|
||||
|
||||
# Validate json and check length again
|
||||
if not is_valid_json(new_comment) or \
|
||||
len(new_comment['content']) < 40 or \
|
||||
len(new_comment['email']) < 5:
|
||||
len(new_comment['content']) < 40 or \
|
||||
len(new_comment['email']) < 5:
|
||||
print("too short", file=stderr)
|
||||
return make_response(jsonify(status='post-invalid-json'), 400)
|
||||
|
||||
# Strip any HTML from message body
|
||||
@ -65,9 +59,9 @@ def check_and_insert_new_comment(name):
|
||||
content = re.sub(tags, '', new_comment['content']).strip()
|
||||
content = re.sub(special, '', content).strip()
|
||||
|
||||
# Convert smileys if enabled
|
||||
if project.addon_smileys:
|
||||
for key, value in smileys.smileys.items():
|
||||
# Convert smileys
|
||||
if addons['smileys']:
|
||||
for key, value in smileys.items():
|
||||
content = content.replace(key, value)
|
||||
|
||||
# Validate replied_to field is integer
|
||||
@ -114,8 +108,8 @@ def check_and_insert_new_comment(name):
|
||||
is_spam = False
|
||||
|
||||
# Look for location
|
||||
loc_query = db.session.query(TLocation) \
|
||||
.filter(TLocation.location == new_comment['location'])
|
||||
loc_query = db.session.query(TLocation)\
|
||||
.filter(TLocation.location == new_comment['location'])
|
||||
|
||||
if loc_query.first():
|
||||
# Location exists, set existing location id
|
||||
@ -125,8 +119,7 @@ def check_and_insert_new_comment(name):
|
||||
else:
|
||||
# Insert new location
|
||||
loc_table = {
|
||||
'location': new_comment['location'],
|
||||
'project_id': project.id_project
|
||||
'location': new_comment['location']
|
||||
}
|
||||
new_loc = TLocation(**loc_table)
|
||||
db.session.add(new_loc)
|
||||
@ -138,17 +131,12 @@ def check_and_insert_new_comment(name):
|
||||
new_comment.pop("location")
|
||||
|
||||
# insert comment
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if project.sendotp:
|
||||
new_comment.update({"is_published": False})
|
||||
else:
|
||||
new_comment.update({"is_published": True})
|
||||
new_comment.update({"is_published": False})
|
||||
new_comment.update({"created_on": default_timestamp()})
|
||||
new_comment.update({"is_spam": is_spam})
|
||||
new_comment.update({"spam_score": has_score})
|
||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'], project.name)})
|
||||
new_comment.update({"project_id": project.id_project})
|
||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'])})
|
||||
t_comment = TComments(**new_comment)
|
||||
db.session.add(t_comment)
|
||||
db.session.commit()
|
||||
@ -156,33 +144,33 @@ def check_and_insert_new_comment(name):
|
||||
db.session.refresh(t_comment)
|
||||
|
||||
# Send confirmation link and store returned value
|
||||
if project.sendotp:
|
||||
hashes = sender.send_confirmation_link(new_comment['email'], project.name)
|
||||
setattr(t_comment, "confirmation", hashes[0])
|
||||
setattr(t_comment, "deletion", hashes[1])
|
||||
db.session.commit()
|
||||
hashes = sender.send_confirmation_link(new_comment['email'])
|
||||
setattr(t_comment, "confirmation", hashes[0])
|
||||
setattr(t_comment, "deletion", hashes[1])
|
||||
db.session.commit()
|
||||
|
||||
except exc.IntegrityError as e:
|
||||
# Comment body exists, because content is unique
|
||||
print(f"Duplicate from {request.environ['REMOTE_ADDR']}, error is:\n{e}", file=stderr)
|
||||
return make_response(jsonify(status="post-duplicate"), 400)
|
||||
|
||||
except Exception: # must be at bottom
|
||||
except Exception as e: # must be at bottom
|
||||
# mail(f"check_and_insert_new_comment has thrown an error: {e}", )
|
||||
print("---------------------------------------------")
|
||||
print(e, file=stderr)
|
||||
print("---------------------------------------------")
|
||||
return make_response(jsonify(status="post-internal-server-error"), 400)
|
||||
|
||||
export_location(t_comment.location_id)
|
||||
return make_response(jsonify(status="post-success",
|
||||
comment_id=t_comment.comments_id,
|
||||
sendotp=project.sendotp), 200)
|
||||
return make_response(jsonify(status="post-success", comment_id=t_comment.comments_id), 200)
|
||||
|
||||
|
||||
# Route for confirming comments
|
||||
@bp_comments.route("/<name>/confirm/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("/confirm/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_confirmation_link(name, email_hash):
|
||||
def check_confirmation_link(email_hash):
|
||||
settings = Settings()
|
||||
comment = db.session.query(TComments).filter(TComments.confirmation == email_hash).first()
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
|
||||
if comment:
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||
if compare_digest(comment.confirmation, email_hash):
|
||||
@ -190,27 +178,27 @@ def check_confirmation_link(name, email_hash):
|
||||
if not comment.is_spam:
|
||||
setattr(comment, "is_published", True)
|
||||
db.session.commit()
|
||||
url = f"{project.blogurl}{location.location}#comment_{comment.comments_id}"
|
||||
url = f"{settings.system['blog_url']}{location.location}#comment_{comment.comments_id}"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
||||
|
||||
|
||||
# Route for deleting comments
|
||||
@bp_comments.route("<name>/delete/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("/delete/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_deletion_link(name, email_hash):
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
comment = db.session.query(TComments).filter(TComments.deletion == email_hash).first()
|
||||
|
||||
def check_deletion_link(email_hash):
|
||||
settings = Settings()
|
||||
query = db.session.query(TComments).filter(TComments.deletion == email_hash)
|
||||
comment = query.first()
|
||||
if comment:
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||
if compare_digest(comment.deletion, email_hash):
|
||||
db.session.delete(comment)
|
||||
query.delete()
|
||||
db.session.commit()
|
||||
url = f"{project.blogurl}?deleted=true"
|
||||
url = f"{settings.system['blog_url']}?deleted=true"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
207
labertasche/blueprints/bp_dashboard.py
Normal file
207
labertasche/blueprints/bp_dashboard.py
Normal file
@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint, render_template, request, redirect
|
||||
from flask_login import login_required
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TLocation, TComments, TEmail
|
||||
from labertasche.helper import dates_of_the_week, export_location
|
||||
from sqlalchemy import func
|
||||
import re
|
||||
|
||||
# Blueprint
|
||||
bp_dashboard = Blueprint("bp_dashboard", __name__, url_prefix='/dashboard')
|
||||
|
||||
|
||||
@bp_dashboard.route('/')
|
||||
@login_required
|
||||
def dashboard_index():
|
||||
dates = dates_of_the_week()
|
||||
spam = list()
|
||||
published = list()
|
||||
unpublished = list()
|
||||
for each in dates:
|
||||
spam_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date())\
|
||||
.filter(TComments.is_spam == True).all()
|
||||
|
||||
pub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
|
||||
.filter(TComments.is_spam == False)\
|
||||
.filter(TComments.is_published == True).all()
|
||||
|
||||
unpub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
|
||||
.filter(TComments.is_spam == False)\
|
||||
.filter(TComments.is_published == False).all()
|
||||
|
||||
published.append(len(pub_comments))
|
||||
spam.append(len(spam_comments))
|
||||
unpublished.append(len(unpub_comments))
|
||||
|
||||
return render_template('dashboard.html', dates=dates, spam=spam, published=published, unpublished=unpublished)
|
||||
|
||||
|
||||
@bp_dashboard.route('/review-spam/', methods=["POST", "GET"])
|
||||
@bp_dashboard.route('/review-spam/<int:location_id>', methods=["POST", "GET"])
|
||||
@login_required
|
||||
def dashboard_review_spam(location_id=None):
|
||||
all_locations = db.session.query(TLocation).all()
|
||||
|
||||
# Check post
|
||||
if request.method == "POST":
|
||||
location_id = request.form.get('selected_location')
|
||||
|
||||
# no parameters found
|
||||
if location_id is None:
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location_id)
|
||||
|
||||
try:
|
||||
if int(location_id) >= 1:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id)\
|
||||
.filter(TComments.is_spam == True)
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location_id,
|
||||
spam_comments=spam_comments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
export_location(location_id)
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location_id)
|
||||
|
||||
|
||||
@bp_dashboard.route('/manage-comments/', methods=["POST", "GET"])
|
||||
@bp_dashboard.route('/manage-comments/<int:location_id>', methods=["POST", "GET"])
|
||||
@login_required
|
||||
def dashboard_manage_regular_comments(location_id=None):
|
||||
all_locations = db.session.query(TLocation).all()
|
||||
|
||||
# Check post
|
||||
if request.method == "POST":
|
||||
location_id = request.form.get('selected_location')
|
||||
|
||||
# no parameters found
|
||||
if location_id is None:
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location_id)
|
||||
|
||||
try:
|
||||
if int(location_id) >= 1:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id) \
|
||||
.filter(TComments.is_spam == False)
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location_id,
|
||||
spam_comments=spam_comments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
export_location(location_id)
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location_id)
|
||||
|
||||
|
||||
@bp_dashboard.route('/manage-mail/')
|
||||
@login_required
|
||||
def dashboard_allow_email():
|
||||
addresses = db.session.query(TEmail).all()
|
||||
return render_template("manage_mail_addresses.html", addresses=addresses)
|
||||
|
||||
|
||||
@bp_dashboard.route('/toggle-mail-allowed/<int:id_email>')
|
||||
@login_required
|
||||
def dashboard_allow_email_toggle(id_email):
|
||||
address = db.session.query(TEmail).filter(TEmail.id_email == id_email).first()
|
||||
if address:
|
||||
setattr(address, "is_allowed", (not address.is_allowed))
|
||||
setattr(address, "is_blocked", (not address.is_blocked))
|
||||
db.session.commit()
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@bp_dashboard.route('/reset-mail-reputation/<int:id_email>')
|
||||
@login_required
|
||||
def dashboard_reset_mail_reputation(id_email):
|
||||
db.session.query(TEmail).filter(TEmail.id_email == id_email).delete()
|
||||
db.session.commit()
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@bp_dashboard.route('/delete-comment/<int:location_id>/<int:comment_id>', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_review_spam_delete_comment(location_id, comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
db.session.delete(comment)
|
||||
db.session.commit()
|
||||
|
||||
# Remove after last slash, to keep the location but get rid of the comment id
|
||||
url = re.match("^(.*[/])", request.referrer)[0]
|
||||
export_location(location_id)
|
||||
return redirect(f"{url}/{location_id}")
|
||||
|
||||
|
||||
@bp_dashboard.route('/allow-comment/<int:location_id>/<int:comment_id>', methods=['GET'])
|
||||
@login_required
|
||||
def dashboard_review_spam_allow_comment(comment_id, location_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
setattr(comment, 'is_published', True)
|
||||
setattr(comment, 'is_spam', False)
|
||||
db.session.commit()
|
||||
|
||||
url = re.match("^(.*[/])", request.referrer)[0]
|
||||
export_location(location_id)
|
||||
return redirect(f"{url}/{location_id}")
|
||||
|
||||
|
||||
@bp_dashboard.route('/block-mail/<int:location_id>/<int:comment_id>', methods=["GET"])
|
||||
@login_required
|
||||
def dashboard_review_spam_block_mail(location_id, comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
|
||||
if mail:
|
||||
setattr(mail, 'is_allowed', False)
|
||||
setattr(mail, 'is_blocked', True)
|
||||
else:
|
||||
new_mail = {
|
||||
"email": comment.first().email,
|
||||
"is_allowed": False,
|
||||
"is_blocked": True
|
||||
}
|
||||
db.session.add(TEmail(**new_mail))
|
||||
|
||||
# Delete all comments made by this mail address
|
||||
db.session.query(TComments).filter(TComments.email == comment.email).delete()
|
||||
db.session.commit()
|
||||
|
||||
url = re.match("^(.*[/])", request.referrer)[0]
|
||||
export_location(location_id)
|
||||
return redirect(f"{url}/{location_id}")
|
||||
|
||||
|
||||
@bp_dashboard.route('/allow-user/<int:location_id>/<int:comment_id>', methods=["GET"])
|
||||
@login_required
|
||||
def dashboard_review_spam_allow_user(location_id, comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
mail = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
|
||||
if mail:
|
||||
setattr(mail, 'is_allowed', True)
|
||||
setattr(mail, 'is_blocked', False)
|
||||
else:
|
||||
new_mail = {
|
||||
"email": comment.email,
|
||||
"is_allowed": True,
|
||||
"is_blocked": False
|
||||
}
|
||||
db.session.add(TEmail(**new_mail))
|
||||
|
||||
# Allow all comments made by this mail address
|
||||
all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all()
|
||||
if all_comments:
|
||||
for comment in all_comments:
|
||||
setattr(comment, 'is_published', True)
|
||||
setattr(comment, 'is_spam', False)
|
||||
|
||||
db.session.commit()
|
||||
url = re.match("^(.*[/])", request.referrer)[0]
|
||||
export_location(location_id)
|
||||
return redirect(f"{url}/{location_id}")
|
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint
|
||||
|
||||
# Blueprint
|
||||
bp_dashboard = Blueprint("bp_dashboard", __name__, url_prefix='/dashboard')
|
||||
|
||||
# Files with routes
|
||||
from .projects import dashboard_project_list
|
||||
from .mail import dashboard_manage_mail
|
||||
from .spam import dashboard_review_spam
|
||||
from .comments import dashboard_manage_regular_comments
|
@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_dashboard
|
||||
from flask import render_template, request, redirect, url_for
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TLocation, TComments
|
||||
from labertasche.helper import export_location, get_id_from_project_name
|
||||
|
||||
|
||||
@cross_origin
|
||||
@bp_dashboard.route('/<project>/manage-comments/', methods=["GET"])
|
||||
@login_required
|
||||
def dashboard_manage_regular_comments(project: str):
|
||||
location_id = 0
|
||||
proj_id = get_id_from_project_name(project)
|
||||
all_locations = db.session.query(TLocation)\
|
||||
.filter(TLocation.project_id == proj_id)\
|
||||
.all()
|
||||
|
||||
# Check if there is a comment, otherwise don't show on management page
|
||||
# This can happen when the last comment was deleted, the location
|
||||
# won't be removed.
|
||||
tmp_list = list()
|
||||
for each in all_locations:
|
||||
comment_count = db.session.query(TComments.comments_id)\
|
||||
.filter(TComments.location_id == each.id_location)\
|
||||
.filter(TComments.is_spam == False) \
|
||||
.count()
|
||||
if comment_count > 0:
|
||||
tmp_list.append(each)
|
||||
|
||||
all_locations = tmp_list
|
||||
|
||||
# Project does not exist, error code is used by Javascript, not Flask
|
||||
if proj_id == -1:
|
||||
return redirect(url_for("bp_dashboard.dashboard_project_list", error=404))
|
||||
|
||||
if request.args.get('location'):
|
||||
location_id = request.args.get('location')
|
||||
|
||||
# no parameters found
|
||||
if location_id is None:
|
||||
return render_template("manage-comments.html", locations=all_locations,
|
||||
selected=location_id, title="Manage Comments",
|
||||
action="comments")
|
||||
|
||||
try:
|
||||
if int(location_id) >= 1:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.location_id == location_id) \
|
||||
.filter(TComments.is_spam == False)
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location_id,
|
||||
spam_comments=spam_comments, project=project,
|
||||
title="Manage Comments", action="comments")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return render_template("manage-comments.html", locations=all_locations,
|
||||
selected=location_id, project=project, title="Manage Comments",
|
||||
action="comments")
|
||||
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_dashboard
|
||||
from flask import render_template
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TEmail
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
@cross_origin()
|
||||
@bp_dashboard.route('/manage-mail/')
|
||||
@bp_dashboard.route('/<project>/manage-mail/')
|
||||
@login_required
|
||||
def dashboard_manage_mail(project: str = None):
|
||||
"""
|
||||
Shows the panel to manage email addresses
|
||||
:param project: Not used
|
||||
:return: The template used to display the route
|
||||
"""
|
||||
|
||||
addresses = db.session.query(TEmail).all()
|
||||
return render_template("manage-mail.html", addresses=addresses)
|
@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_dashboard
|
||||
from flask import render_template, redirect, url_for, request
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TComments, TProjects
|
||||
from labertasche.helper import get_id_from_project_name, dates_of_the_week
|
||||
|
||||
|
||||
@cross_origin
|
||||
@bp_dashboard.route("/")
|
||||
@login_required
|
||||
def dashboard_project_list():
|
||||
"""
|
||||
Displays an overview of all projects.
|
||||
:return: The overview template.
|
||||
"""
|
||||
try:
|
||||
t_projects = db.session.query(TProjects).all()
|
||||
except OperationalError:
|
||||
# Database not up-to-date
|
||||
return redirect(url_for('bp_dbupgrades.upgrade_db_to_v2'))
|
||||
|
||||
projects = list()
|
||||
for each in t_projects:
|
||||
comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \
|
||||
.filter(TComments.is_published == True) \
|
||||
.filter(TComments.is_spam == False).count()
|
||||
unpub_comments = db.session.query(TComments).filter(TComments.project_id == each.id_project) \
|
||||
.filter(TComments.is_spam == False) \
|
||||
.filter(TComments.is_published == False).count()
|
||||
spam = db.session.query(TComments).filter(TComments.project_id == each.id_project) \
|
||||
.filter(TComments.is_spam == True).count()
|
||||
|
||||
projects.append(dict({
|
||||
"id_project": each.id_project,
|
||||
"name": each.name,
|
||||
"total_comments": comments,
|
||||
"total_spam": spam,
|
||||
"total_unpublished": unpub_comments
|
||||
}))
|
||||
return render_template('project-list.html', projects=projects)
|
||||
|
||||
|
||||
@cross_origin
|
||||
@bp_dashboard.route('/<project>/')
|
||||
@login_required
|
||||
def dashboard_project_stats(project: str):
|
||||
"""
|
||||
Displays the project dashboard
|
||||
|
||||
:param project: The project to show
|
||||
:return: The template for the route
|
||||
"""
|
||||
proj_id = get_id_from_project_name(project)
|
||||
|
||||
# Project does not exist, error code is used by Javascript, not Flask
|
||||
if proj_id == -1:
|
||||
return redirect(url_for("bp_dashboard.dashboard_project_list", error=404))
|
||||
|
||||
# Total graphs
|
||||
total_spam = db.session.query(TComments).filter(TComments.is_spam == True).count()
|
||||
|
||||
total_comments = db.session.query(TComments) \
|
||||
.filter(TComments.is_spam == False)\
|
||||
.filter(TComments.is_published == True).count()
|
||||
|
||||
total_unpublished = db.session.query(TComments).filter(TComments.is_spam == False)\
|
||||
.filter(TComments.is_published == False).count()
|
||||
|
||||
# 7 day graph
|
||||
dates = dates_of_the_week()
|
||||
spam = list()
|
||||
published = list()
|
||||
unpublished = list()
|
||||
for each in dates:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.project_id == proj_id) \
|
||||
.filter(func.DATE(TComments.created_on) == each.date()) \
|
||||
.filter(TComments.is_spam == True).all()
|
||||
|
||||
pub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
|
||||
.filter(TComments.project_id == proj_id) \
|
||||
.filter(TComments.is_spam == False) \
|
||||
.filter(TComments.is_published == True).all()
|
||||
|
||||
unpub_comments = db.session.query(TComments).filter(func.DATE(TComments.created_on) == each.date()) \
|
||||
.filter(TComments.project_id == proj_id) \
|
||||
.filter(TComments.is_spam == False) \
|
||||
.filter(TComments.is_published == False).all()
|
||||
|
||||
published.append(len(pub_comments))
|
||||
spam.append(len(spam_comments))
|
||||
unpublished.append(len(unpub_comments))
|
||||
|
||||
return render_template('project-stats.html', dates=dates, spam=spam, project=project,
|
||||
published=published, unpublished=unpublished,
|
||||
total_spam=total_spam, total_comments=total_comments,
|
||||
total_unpublished=total_unpublished)
|
||||
|
@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_dashboard
|
||||
from flask import render_template, request, redirect, url_for
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TLocation, TComments
|
||||
from labertasche.helper import export_location, get_id_from_project_name
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dashboard.route('<project>/manage-spam/', methods=["GET"])
|
||||
@login_required
|
||||
def dashboard_review_spam(project: str):
|
||||
"""
|
||||
Shows the manage spam template
|
||||
:param project: The project used for displaying data
|
||||
:return: The template to display for this rouet
|
||||
"""
|
||||
location_id = 0
|
||||
proj_id = get_id_from_project_name(project)
|
||||
all_locations = db.session.query(TLocation).filter(TLocation.project_id == proj_id).all()
|
||||
|
||||
# Check if there is a comment, otherwise don't show on management page
|
||||
# This can happen when the last comment was deleted, the location
|
||||
# won't be removed.
|
||||
tmp_list = list()
|
||||
for each in all_locations:
|
||||
comment_count = db.session.query(TComments.comments_id) \
|
||||
.filter(TComments.location_id == each.id_location) \
|
||||
.filter(TComments.is_spam == True) \
|
||||
.count()
|
||||
if comment_count > 0:
|
||||
tmp_list.append(each)
|
||||
|
||||
all_locations = tmp_list
|
||||
|
||||
# Project does not exist, error code is used by Javascript, not Flask
|
||||
if proj_id == -1:
|
||||
return redirect(url_for("bp_dashboard.dashboard_project_list", error=404))
|
||||
|
||||
if request.args.get('location'):
|
||||
location_id = request.args.get('location')
|
||||
|
||||
# no parameters found
|
||||
if location_id is None:
|
||||
return render_template("manage-comments.html", locations=all_locations,
|
||||
selected=location_id, title="Review Spam", action="spam")
|
||||
|
||||
try:
|
||||
if int(location_id) >= 1:
|
||||
spam_comments = db.session.query(TComments) \
|
||||
.filter(TComments.project_id == proj_id) \
|
||||
.filter(TComments.location_id == location_id) \
|
||||
.filter(TComments.is_spam == True)
|
||||
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location_id,
|
||||
spam_comments=spam_comments, project=project, title="Review Spam", action="spam")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
export_location(location_id)
|
||||
return render_template("manage-comments.html", locations=all_locations,
|
||||
selected=location_id, project=project, title="Review Spam", action="spam")
|
||||
|
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint
|
||||
|
||||
# Blueprint
|
||||
bp_jsconnector = Blueprint("bp_jsconnector", __name__, url_prefix='/api/')
|
||||
|
||||
from .projects import api_create_project, api_delete_project, api_edit_project_name
|
||||
from .mail import api_toggle_email_reputation, api_reset_mail_reputation
|
||||
from .language import api_translation
|
||||
from .comments import api_comment_allow_user, api_comment_allow_comment, \
|
||||
api_comment_block_mail, api_comments_delete_comment
|
@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_jsconnector
|
||||
from flask import request, redirect, make_response, jsonify
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import export_location, get_id_from_project_name
|
||||
from labertasche.models import TComments, TEmail, TLocation
|
||||
|
||||
# This file contains the routes for the manage comments menu point.
|
||||
# They are called via GET
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-delete/<int:comment_id>', methods=['GET'])
|
||||
@login_required
|
||||
def api_comments_delete_comment(comment_id):
|
||||
db.session.query(TComments).filter(TComments.comments_id == comment_id).delete()
|
||||
db.session.commit()
|
||||
|
||||
# Get location id from get params
|
||||
location_id = request.args.get('location')
|
||||
|
||||
export_location(location_id)
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-allow/<int:comment_id>', methods=['GET'])
|
||||
@login_required
|
||||
def api_comment_allow_comment(comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
setattr(comment, 'is_published', True)
|
||||
setattr(comment, 'is_spam', False)
|
||||
db.session.commit()
|
||||
|
||||
# Get location id from get params
|
||||
location_id = request.args.get('location')
|
||||
|
||||
export_location(location_id)
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-allow-user/<int:comment_id>', methods=["GET"])
|
||||
@login_required
|
||||
def api_comment_allow_user(comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
|
||||
if addr:
|
||||
setattr(addr, 'is_allowed', True)
|
||||
setattr(addr, 'is_blocked', False)
|
||||
else:
|
||||
new_mail = {
|
||||
"email": comment.email,
|
||||
"is_allowed": True,
|
||||
"is_blocked": False
|
||||
}
|
||||
db.session.add(TEmail(**new_mail))
|
||||
|
||||
# Allow all comments made by this mail address
|
||||
all_comments = db.session.query(TComments).filter(TComments.email == comment.email).all()
|
||||
if all_comments:
|
||||
for comment in all_comments:
|
||||
setattr(comment, 'is_published', True)
|
||||
setattr(comment, 'is_spam', False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get location id from get params
|
||||
location_id = request.args.get('location')
|
||||
|
||||
export_location(location_id)
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-block-mail/<int:comment_id>', methods=["GET"])
|
||||
@login_required
|
||||
def api_comment_block_mail(comment_id):
|
||||
comment = db.session.query(TComments).filter(TComments.comments_id == comment_id).first()
|
||||
if comment:
|
||||
addr = db.session.query(TEmail).filter(TEmail.email == comment.email).first()
|
||||
if addr:
|
||||
setattr(addr, 'is_allowed', False)
|
||||
setattr(addr, 'is_blocked', True)
|
||||
else:
|
||||
new_mail = {
|
||||
"email": comment.first().email,
|
||||
"is_allowed": False,
|
||||
"is_blocked": True
|
||||
}
|
||||
db.session.add(TEmail(**new_mail))
|
||||
|
||||
# Delete all comments made by this mail address
|
||||
db.session.query(TComments).filter(TComments.email == comment.email).delete()
|
||||
db.session.commit()
|
||||
|
||||
# Get location id from get params
|
||||
location_id = request.args.get('location')
|
||||
|
||||
export_location(location_id)
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-export-all/<name>', methods=["GET"])
|
||||
@login_required
|
||||
def api_export_all_by_project(name):
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 400)
|
||||
|
||||
try:
|
||||
locations = db.session.query(TLocation).all()
|
||||
for each in locations:
|
||||
export_location(each.id_location)
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='sql-error', msg=str(e)), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_jsconnector
|
||||
from flask import make_response, jsonify, request
|
||||
from flask_cors import cross_origin
|
||||
from flask_login import login_required
|
||||
from labertasche.language import Language
|
||||
|
||||
|
||||
@cross_origin
|
||||
@bp_jsconnector.route('/language/')
|
||||
@login_required
|
||||
def api_translation():
|
||||
lang = Language(request=request)
|
||||
return make_response(jsonify(lang.i18n), 200)
|
@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_jsconnector
|
||||
from flask import request, redirect
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TEmail
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/mail-toggle-status/<int:id_email>')
|
||||
@login_required
|
||||
def api_toggle_email_reputation(id_email):
|
||||
address = db.session.query(TEmail).filter(TEmail.id_email == id_email).first()
|
||||
if address:
|
||||
setattr(address, "is_allowed", (not address.is_allowed))
|
||||
setattr(address, "is_blocked", (not address.is_blocked))
|
||||
db.session.commit()
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/mail-reset-reputation/<int:id_email>')
|
||||
@login_required
|
||||
def api_reset_mail_reputation(id_email):
|
||||
db.session.query(TEmail).filter(TEmail.id_email == id_email).delete()
|
||||
db.session.commit()
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_jsconnector
|
||||
from flask import request, make_response, jsonify
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import get_id_from_project_name
|
||||
from labertasche.models import TProjects, TComments, TEmail, TLocation
|
||||
from validators import url as validate_url
|
||||
from pathlib import Path
|
||||
from secrets import compare_digest
|
||||
import re
|
||||
|
||||
|
||||
def validate_project(project, is_edit=False):
|
||||
"""
|
||||
Validates important bits of a project database entry
|
||||
|
||||
:param project: The json from the request, containing the data for a project.
|
||||
:param is_edit: If we are updating the database, we need to know, so we don't check for dupes on urls.
|
||||
:return: A response with the error or None if the project is valid.
|
||||
"""
|
||||
# Validate length
|
||||
if not len(project['name']) and \
|
||||
not len(project['blogurl']) and \
|
||||
not len(project['output']):
|
||||
return make_response(jsonify(status='too-short'), 400)
|
||||
|
||||
# Validate project name
|
||||
if not re.match('^\\w+$', project['name']):
|
||||
return make_response(jsonify(status='invalid-project-name'), 400)
|
||||
|
||||
# Check if project name already exists
|
||||
name_check = db.session.query(TProjects.name).filter(TProjects.name == project['name']).first()
|
||||
if name_check and not is_edit:
|
||||
return make_response(jsonify(status='project-exists'), 400)
|
||||
|
||||
# Validate existing only if we are not editing
|
||||
url_exists = False
|
||||
if not is_edit:
|
||||
url_exists = db.session.query(TProjects.blogurl).filter(TProjects.blogurl == project['blogurl']).first()
|
||||
|
||||
if not validate_url(project['blogurl']) or url_exists:
|
||||
return make_response(jsonify(status='invalid-blog-url'), 400)
|
||||
|
||||
# Validate output path
|
||||
output = Path(project['output']).absolute()
|
||||
# The second check is needed, since javascript is passing an empty string instead of
|
||||
# null. For some reason, this makes SQLAlchemy accept the data and commit it to the db
|
||||
# without exception. This check prevents this issue from happening.
|
||||
if not output.exists() or len(project['output'].strip()) == 0:
|
||||
return make_response(jsonify(status='invalid-path-output'), 400)
|
||||
|
||||
# Validate cache path, if caching is enabled
|
||||
if project['gravatar_cache']:
|
||||
cache = Path(project['gravatar_cache_dir']).absolute()
|
||||
if not cache.exists() or len(project['gravatar_cache_dir'].strip()) == 0:
|
||||
return make_response(jsonify(status='invalid-path-cache'), 400)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route("/project/new", methods=['POST'])
|
||||
@login_required
|
||||
def api_create_project():
|
||||
"""
|
||||
Called on dashboard project overview to create a new project.
|
||||
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
response = validate_project(request.json)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
try:
|
||||
new_project = request.json
|
||||
# Remove trailing slash
|
||||
if compare_digest(new_project['blogurl'][-1], '/'):
|
||||
new_project['blogurl'] = new_project['blogurl'][:-1]
|
||||
|
||||
db.session.add(TProjects(**new_project))
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
db.session.rollback()
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/edit/<name>', methods=['POST'])
|
||||
@login_required
|
||||
def api_edit_project_name(name: str):
|
||||
"""
|
||||
Renames the project.
|
||||
:param name: The previous name of the project to edit, must exist
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
response = validate_project(request.json, is_edit=True)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
try:
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
|
||||
project_json = request.json
|
||||
# Remove trailing slash to streamline it
|
||||
if compare_digest(project_json['blogurl'][-1], '/'):
|
||||
setattr(project, "blogurl", project_json['blogurl'][:-1].strip())
|
||||
|
||||
setattr(project, "id_project", project.id_project)
|
||||
setattr(project, "name", project_json['name'])
|
||||
setattr(project, "output", project_json['output'].strip())
|
||||
setattr(project, "sendotp", project_json['sendotp'])
|
||||
setattr(project, "gravatar_cache", project_json['gravatar_cache'])
|
||||
setattr(project, "gravatar_cache_dir", project_json['gravatar_cache_dir'])
|
||||
setattr(project, "gravatar_size", project_json['gravatar_size'])
|
||||
setattr(project, "addon_smileys", project_json['addon_smileys'])
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/delete/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_delete_project(name: str):
|
||||
"""
|
||||
Deletes a project from the database and all associated data
|
||||
|
||||
:param name: The name of the project
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 400)
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
db.session.query(TComments).filter(TComments.project_id == proj_id).delete()
|
||||
db.session.query(TLocation).filter(TLocation.project_id == proj_id).delete()
|
||||
db.session.query(TProjects).filter(TProjects.id_project == proj_id).delete()
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
except Exception:
|
||||
return make_response(jsonify(status='exception'), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/exists/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_project_exists(name: str):
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 200)
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/get/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_project_get_data(name: str):
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
if project:
|
||||
return make_response(jsonify(status='ok',
|
||||
id_project=project.id_project,
|
||||
name=project.name,
|
||||
blogurl=project.blogurl,
|
||||
output=project.output,
|
||||
sendotp=project.sendotp,
|
||||
gravatar_cache=project.gravatar_cache,
|
||||
gravatar_cache_dir=project.gravatar_cache_dir,
|
||||
gravatar_size=project.gravatar_size,
|
||||
addon_smileys=project.addon_smileys), 200)
|
||||
else:
|
||||
print('400')
|
||||
return make_response(jsonify(status='not-found'), 400)
|
@ -8,9 +8,7 @@
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.helper import User
|
||||
from labertasche.settings import Credentials
|
||||
from secrets import compare_digest
|
||||
from labertasche.helper import check_auth, User
|
||||
from flask_login import login_user, current_user, logout_user
|
||||
|
||||
# Blueprint
|
||||
@ -21,7 +19,7 @@ bp_login = Blueprint("bp_login", __name__)
|
||||
@bp_login.route('/', methods=['GET'])
|
||||
def show_login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
return redirect(url_for('bp_dashboard.dashboard_index'))
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@ -32,11 +30,9 @@ def login():
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
credentials = Credentials()
|
||||
if compare_digest(username.encode('utf8'), credentials.username.encode('utf8')) and \
|
||||
credentials.compare_password(password):
|
||||
if check_auth(username, password):
|
||||
login_user(User(0), remember=True)
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
return redirect(url_for('bp_dashboard.dashboard_index'))
|
||||
|
||||
# Redirect get request to the login page
|
||||
return redirect(url_for('bp_login.show_login'))
|
@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint
|
||||
|
||||
# Blueprint
|
||||
bp_dbupgrades = Blueprint("bp_dbupgrades", __name__, url_prefix='/upgrade')
|
||||
|
||||
from .db_v2 import upgrade_db_to_v2
|
@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_dbupgrades
|
||||
from flask_cors import cross_origin
|
||||
from flask_login import login_required
|
||||
from flask import render_template, jsonify, make_response, redirect, url_for, current_app
|
||||
from pathlib import Path
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TProjects, TComments, TLocation, TEmail, TVersion
|
||||
from labertasche.settings import LegacySettings
|
||||
from json import dump, load
|
||||
from shutil import copy, make_archive
|
||||
from re import search
|
||||
from secrets import compare_digest
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_backup_folder() -> Path:
|
||||
path = Path(current_app.root_path)
|
||||
path = path / "backup" / "v1"
|
||||
return path
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dbupgrades.route('/db_v2/')
|
||||
@login_required
|
||||
def upgrade_db_to_v2():
|
||||
# TODO: Check if db has already been upgraded
|
||||
status = False
|
||||
try:
|
||||
version = db.session.query(TVersion).first()
|
||||
if version:
|
||||
status = True
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
|
||||
except Exception as e:
|
||||
print(e.__class__)
|
||||
pass
|
||||
|
||||
return render_template("db-upgrades.html", title="DB upgrade V1 to V2",
|
||||
prev_version=1, new_version=2, status=status)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dbupgrades.route('/db_v2/backup/', methods=['GET'])
|
||||
@login_required
|
||||
def upgrade_db_to_v2_backup():
|
||||
path = get_backup_folder()
|
||||
# Create path for backup
|
||||
try:
|
||||
if not path.exists():
|
||||
path.mkdir(mode=777, exist_ok=True, parents=True)
|
||||
except OSError as e:
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 400)
|
||||
|
||||
return make_response(jsonify(status="ok"), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dbupgrades.route('/db_v2/export/')
|
||||
@login_required
|
||||
def upgrade_db_to_v2_export():
|
||||
path = get_backup_folder()
|
||||
|
||||
# make sure nothing is pending
|
||||
db.session.commit()
|
||||
|
||||
# Export tables
|
||||
t_locations = db.session.query(TLocation.id_location, TLocation.location).all()
|
||||
t_emails = db.session.query(TEmail.id_email, TEmail.email, TEmail.is_allowed, TEmail.is_blocked).all()
|
||||
t_comments = db.session.query(TComments.comments_id, TComments.location_id, TComments.email,
|
||||
TComments.content, TComments.created_on, TComments.is_published,
|
||||
TComments.is_spam, TComments.spam_score, TComments.replied_to,
|
||||
TComments.confirmation, TComments.deletion, TComments.gravatar).all()
|
||||
|
||||
locations = []
|
||||
for loc in t_locations:
|
||||
locations.append({
|
||||
"id_location": loc.id_location,
|
||||
"location": loc.location
|
||||
})
|
||||
|
||||
emails = []
|
||||
for mail in t_emails:
|
||||
emails.append({
|
||||
"id_email": mail.id_email,
|
||||
"email": mail.email,
|
||||
"is_allowed": mail.is_allowed,
|
||||
"is_blocked": mail.is_blocked
|
||||
})
|
||||
|
||||
comments = []
|
||||
for comment in t_comments:
|
||||
comments.append({
|
||||
"comments_id": comment.comments_id,
|
||||
"location_id": comment.location_id,
|
||||
"email": comment.email,
|
||||
"content": comment.content,
|
||||
"created_on": f"{comment.created_on.__str__()}",
|
||||
"is_published": comment.is_published,
|
||||
"is_spam": comment.is_spam,
|
||||
"spam_score": comment.spam_score,
|
||||
"replied_to": comment.replied_to,
|
||||
"confirmation": comment.confirmation,
|
||||
"deletion": comment.deletion,
|
||||
"gravatar": comment.gravatar
|
||||
})
|
||||
|
||||
# Output jsons
|
||||
try:
|
||||
p_export_location = path / "locations.json"
|
||||
with p_export_location.open('w') as fp:
|
||||
dump(locations, fp, indent=4, sort_keys=True)
|
||||
|
||||
p_export_mail = path / "emails.json"
|
||||
with p_export_mail.open('w') as fp:
|
||||
dump(emails, fp, indent=4, sort_keys=True)
|
||||
|
||||
p_export_comments = path / "comments.json"
|
||||
with p_export_comments.open('w') as fp:
|
||||
dump(comments, fp, indent=4, sort_keys=True)
|
||||
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='exception-write-json', msg=str(e)), 400)
|
||||
|
||||
# Copy database
|
||||
try:
|
||||
settings = LegacySettings(True)
|
||||
db_uri = settings.system['database_uri']
|
||||
if compare_digest(db_uri[0:6], "sqlite"):
|
||||
m = search("([/]{3})(.*)", db_uri)
|
||||
new_db = get_backup_folder() / "labertasche.db"
|
||||
old_db = Path(current_app.root_path)
|
||||
old_db = old_db / m.group(2)
|
||||
copy(old_db.absolute(), new_db.absolute())
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='exception-copy-db', msg=str(e)), 400)
|
||||
|
||||
make_archive(path, "zip", path)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dbupgrades.route('/db_v2/recreate/')
|
||||
@login_required
|
||||
def upgrade_db_to_v2_recreate():
|
||||
try:
|
||||
db.drop_all()
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
db.create_all()
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_dbupgrades.route('/db_v2/import/')
|
||||
@login_required
|
||||
def upgrade_db_to_v2_import():
|
||||
path = get_backup_folder()
|
||||
settings = LegacySettings(True)
|
||||
|
||||
try:
|
||||
# load location
|
||||
p_loc = (path / 'locations.json').absolute()
|
||||
with p_loc.open('r') as fp:
|
||||
locations = load(fp)
|
||||
|
||||
# load mails
|
||||
m_loc = (path / 'emails.json').absolute()
|
||||
with m_loc.open('r') as fp:
|
||||
mails = load(fp)
|
||||
|
||||
# load comments
|
||||
c_loc = (path / 'comments.json').absolute()
|
||||
with c_loc.open('r') as fp:
|
||||
comments = load(fp)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
return make_response(jsonify(status='exception-filenotfound', msg=str(e)), 400)
|
||||
|
||||
# Create project
|
||||
default_project = {
|
||||
"id_project": 1,
|
||||
"name": "default",
|
||||
"blogurl": settings.system['blog_url'],
|
||||
"output": settings.system['output'],
|
||||
"sendotp": settings.system['send_otp_to_publish'],
|
||||
"gravatar_cache": settings.gravatar['cache'],
|
||||
"gravatar_cache_dir": settings.gravatar['static_dir'],
|
||||
"gravatar_size": settings.gravatar['size'],
|
||||
"addon_smileys": settings.addons['smileys']
|
||||
}
|
||||
|
||||
# Create db version, so we can track it in the future
|
||||
version = {
|
||||
"id_version": 1,
|
||||
"version": 2
|
||||
}
|
||||
|
||||
try:
|
||||
# Add to db
|
||||
db.session.add(TVersion(**version))
|
||||
db.session.add(TProjects(**default_project))
|
||||
|
||||
# walk json and readd to database with project set to project 1
|
||||
for each in mails:
|
||||
db.session.add(TEmail(**each))
|
||||
|
||||
for each in locations:
|
||||
each.update({'project_id': 1})
|
||||
db.session.add(TLocation(**each))
|
||||
|
||||
for each in comments:
|
||||
each.update({'project_id': 1})
|
||||
dt = datetime.fromisoformat(each['created_on'])
|
||||
each.update({'created_on': dt})
|
||||
db.session.add(TComments(**each))
|
||||
|
||||
# Commit
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='exception-database', msg=str(e)), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
@ -7,20 +7,6 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
# naming conventions
|
||||
convention = {
|
||||
"ix": 'ix_%(column_0_label)s',
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_%(column_0_name)s",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s"
|
||||
}
|
||||
metadata = MetaData(naming_convention=convention)
|
||||
|
||||
# Create SQLAlchemy
|
||||
labertasche_db = SQLAlchemy(metadata=metadata, engine_options={
|
||||
'poolclass': NullPool
|
||||
})
|
||||
labertasche_db = SQLAlchemy()
|
||||
|
@ -8,19 +8,21 @@
|
||||
# *********************************************************************************/
|
||||
import datetime
|
||||
import json
|
||||
from labertasche.models import TLocation, TComments
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.database import labertasche_db as db
|
||||
from functools import wraps
|
||||
from hashlib import md5
|
||||
from flask import request
|
||||
from flask_login import UserMixin
|
||||
from secrets import compare_digest
|
||||
from pathlib import Path
|
||||
from sys import stderr
|
||||
from re import match as re_match
|
||||
from labertasche.models import TLocation, TComments, TProjects
|
||||
from labertasche.database import labertasche_db as db
|
||||
import requests
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
"""
|
||||
Class for flask-login, which represents a user
|
||||
"""
|
||||
def __init__(self, user_id):
|
||||
self.id = user_id
|
||||
|
||||
@ -35,7 +37,7 @@ def is_valid_json(j):
|
||||
try:
|
||||
json.dumps(j)
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
except json.JSONDecodeError as e:
|
||||
print("not valid json")
|
||||
return False
|
||||
|
||||
@ -75,30 +77,61 @@ def alchemy_query_to_dict(obj):
|
||||
|
||||
# Come on, it's a mail hash, don't complain
|
||||
# noinspection InsecureHash
|
||||
def check_gravatar(email: str, name: str):
|
||||
def check_gravatar(email: str):
|
||||
"""
|
||||
Builds the gravatar email hash, which uses md5.
|
||||
You may use ?size=128 for example to dictate size in the final template.
|
||||
:param email: the email to use for the hash
|
||||
:param name: The project name
|
||||
:return: the gravatar url of the image
|
||||
"""
|
||||
from requests import get
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
settings = Settings()
|
||||
options = settings.gravatar
|
||||
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
|
||||
|
||||
if project.gravatar_cache:
|
||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={project.gravatar_size}"
|
||||
response = get(url)
|
||||
if options['cache']:
|
||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={options['size']}"
|
||||
response = requests.get(url)
|
||||
if response.ok:
|
||||
outfile = Path(f"{project.gravatar_cache_dir}/{gravatar_hash}.jpg")
|
||||
with outfile.open('wb') as fp:
|
||||
response.raw.decode_content = True
|
||||
for chunk in response:
|
||||
fp.write(chunk)
|
||||
outfile = Path(f"{options['static_dir']}/{gravatar_hash}.jpg")
|
||||
if not outfile.exists():
|
||||
with outfile.open('wb') as fp:
|
||||
response.raw.decode_content = True
|
||||
for chunk in response:
|
||||
fp.write(chunk)
|
||||
|
||||
return gravatar_hash
|
||||
|
||||
|
||||
def check_auth(username: str, password: str):
|
||||
"""
|
||||
Compares username and password from the settings file in a safe way.
|
||||
Direct string comparison is vulnerable to timing attacks
|
||||
https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
|
||||
:param username: username entered by the user
|
||||
:param password: password entered by the user
|
||||
:return: True if equal, False if not
|
||||
"""
|
||||
settings = Settings()
|
||||
if compare_digest(username, settings.dashboard['username']) and \
|
||||
compare_digest(password, settings.dashboard['password']):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def basic_login_required(f):
|
||||
"""
|
||||
Decorator for basic auth
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapped_view(**kwargs):
|
||||
auth = request.authorization
|
||||
if not (auth and check_auth(auth.username, auth.password)):
|
||||
return ('Unauthorized', 401, {
|
||||
'WWW-Authenticate': 'Basic realm="Login Required"'
|
||||
})
|
||||
return f(**kwargs)
|
||||
return wrapped_view
|
||||
|
||||
|
||||
def export_location(location_id: int) -> bool:
|
||||
"""
|
||||
Exports the comments for the location after the comment was accepted
|
||||
@ -109,28 +142,12 @@ def export_location(location_id: int) -> bool:
|
||||
db.session.flush()
|
||||
|
||||
# Query
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||
loc_query = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||
|
||||
if location:
|
||||
if loc_query:
|
||||
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
|
||||
.filter(TComments.is_published == True) \
|
||||
.filter(TComments.location_id == location.id_location) \
|
||||
.filter(TProjects.id_project == location.project_id) \
|
||||
.all()
|
||||
project = db.session.query(TProjects).filter(TProjects.id_project == location.project_id).first()
|
||||
|
||||
# Removes the last slash
|
||||
path_loc = re_match(".*(?=/)", location.location)[0]
|
||||
|
||||
# Construct export path
|
||||
jsonfile = Path(f"{project.output}/{path_loc}.json").absolute()
|
||||
folder = jsonfile.parents[0]
|
||||
|
||||
# If there are no comments, do not export and remove empty file.
|
||||
# The database is the point of trust.
|
||||
if len(comments) == 0:
|
||||
jsonfile.unlink(missing_ok=True)
|
||||
return True
|
||||
.filter(TComments.location_id == loc_query.id_location)
|
||||
|
||||
bundle = {
|
||||
"comments": [],
|
||||
@ -142,14 +159,20 @@ def export_location(location_id: int) -> bool:
|
||||
continue
|
||||
bundle['comments'].append(alchemy_query_to_dict(comment))
|
||||
|
||||
# Create folder if not exists and write file
|
||||
path_loc = re_match(".*(?=/)", loc_query.location)[0]
|
||||
|
||||
system = Settings().system
|
||||
out = Path(f"{system['output']}/{path_loc}.json")
|
||||
out = out.absolute()
|
||||
folder = out.parents[0]
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
with jsonfile.open('w') as fp:
|
||||
with out.open('w') as fp:
|
||||
json.dump(bundle, fp)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# mail(f"export_comments has thrown an error: {str(e)}")
|
||||
print(e, file=stderr)
|
||||
return False
|
||||
|
||||
@ -170,17 +193,3 @@ def dates_of_the_week():
|
||||
date_list.append(monday)
|
||||
date_list.append((monday + datetime.timedelta(days=1, hours=23, minutes=59, seconds=59)))
|
||||
return date_list
|
||||
|
||||
|
||||
def get_id_from_project_name(name: str) -> int:
|
||||
"""
|
||||
Returns the id of a project name
|
||||
:param name: The display name of the project
|
||||
:return: the ID of the project
|
||||
"""
|
||||
proj = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
|
||||
if proj is None:
|
||||
return -1
|
||||
|
||||
return proj.id_project
|
||||
|
@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from flask import Request, current_app
|
||||
from pathlib import Path
|
||||
from json import load
|
||||
|
||||
|
||||
class Language:
|
||||
|
||||
def __init__(self, request: Request):
|
||||
# Define data
|
||||
self.i18n = dict()
|
||||
self.languages = list()
|
||||
|
||||
# Directory where translations live
|
||||
i18n_dir = Path(current_app.root_path).absolute()
|
||||
i18n_dir = i18n_dir / "i18n"
|
||||
|
||||
# Looks for translations
|
||||
for filename in i18n_dir.glob("*.json"):
|
||||
if filename.is_file():
|
||||
self.languages.append(filename.stem)
|
||||
|
||||
# Check the browser language in the headers
|
||||
self.browser_language = request.accept_languages.best_match(self.languages, default="en-US")
|
||||
|
||||
# Try to Load language accepted by browser
|
||||
try:
|
||||
file = i18n_dir / self.browser_language
|
||||
with file.with_suffix(".json").absolute().open('r', encoding='utf-8') as fp:
|
||||
foreign = load(fp)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Always load english
|
||||
file = i18n_dir / "en-US"
|
||||
with file.with_suffix(".json").absolute().open('r', encoding='utf-8') as fp:
|
||||
self.i18n = load(fp)
|
||||
|
||||
# Merge dicts, so missing keys are replaced with English
|
||||
self.i18n.update(**foreign)
|
||||
|
@ -13,16 +13,12 @@ from pathlib import Path
|
||||
from platform import system
|
||||
from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException
|
||||
from ssl import create_default_context
|
||||
from labertasche.settings import Settings
|
||||
from validate_email import validate_email_or_fail
|
||||
from secrets import token_urlsafe
|
||||
from labertasche.models import TProjects
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.language import Language
|
||||
from flask import render_template, request
|
||||
|
||||
|
||||
class Mail:
|
||||
class mail:
|
||||
|
||||
def __init__(self):
|
||||
path = Path("/etc/labertasche/mail_credentials.json")
|
||||
@ -65,45 +61,32 @@ class Mail:
|
||||
except SMTPException as e:
|
||||
print(f"SMTPException: {e}")
|
||||
|
||||
def send_confirmation_link(self, email: str, name: str) -> tuple:
|
||||
def send_confirmation_link(self, email):
|
||||
"""
|
||||
Send confirmation link after entering a comment
|
||||
:param email: The address to send the mail to
|
||||
:param name: The name of the project
|
||||
:return: A tuple with the confirmation token and the deletion token, in this order
|
||||
"""
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
if not project:
|
||||
return None, None
|
||||
|
||||
settings = Settings()
|
||||
language = Language(request)
|
||||
|
||||
confirm_digest = token_urlsafe(48)
|
||||
delete_digest = token_urlsafe(48)
|
||||
|
||||
confirm_url = f"{settings.weburl}/comments/{project.name}/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.weburl}/comments/{project.name}/delete/{delete_digest}"
|
||||
confirm_url = f"{settings.system['web_url']}/comments/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.system['web_url']}/comments/delete/{delete_digest}"
|
||||
|
||||
html_tpl = f"mail/comment_confirmation_{language.browser_language}.html"
|
||||
txt_tpl = f"mail/comment_confirmation_{language.browser_language}_txt.html"
|
||||
txt_what = f"Hey there. You have made a comment on {settings.system['blog_url']}. Please confirm it by " \
|
||||
f"copying this link into your browser:\n{confirm_url}\nIf you want to delete your comment for,"\
|
||||
f"whatever reason, please use this link:\n{delete_url}"
|
||||
|
||||
if not Path(f"./templates/{html_tpl}").exists():
|
||||
html_tpl = f"mail/comment_confirmation_en-US.html"
|
||||
if not Path(f"./templates/{txt_tpl}").exists():
|
||||
html_tpl = f"mail/comment_confirmation_en-US_txt.html"
|
||||
|
||||
txt_what = render_template(txt_tpl,
|
||||
blogurl=project.blogurl,
|
||||
confirmation_url=confirm_url,
|
||||
deletion_url=delete_url).replace('<pre>', "").replace('</pre>', '')
|
||||
|
||||
html_what = render_template(html_tpl,
|
||||
blogurl=project.blogurl,
|
||||
confirmation_url=confirm_url,
|
||||
deletion_url=delete_url)
|
||||
html_what = f"Hey there. You have made a comment on {settings.system['blog_url']}.<br>Please confirm it by " \
|
||||
f"clicking on this <a href='{confirm_url}'>link</a>.<br>"\
|
||||
f"In case you want to delete your comment later, please click <a href='{delete_url}'>here</a>."\
|
||||
f"<br><br>If you think this is in error or someone made this comment in your name, please "\
|
||||
f"write me a <a href='mailto:contact@tuxstash.de'>mail</a> to discuss options such as " \
|
||||
f"blocking your mail from being used."
|
||||
|
||||
self.send(txt_what, html_what, email)
|
||||
|
||||
return confirm_digest, delete_digest
|
||||
|
||||
def validate(self, addr):
|
||||
|
@ -9,5 +9,3 @@
|
||||
from .t_comments import TComments
|
||||
from .t_location import TLocation
|
||||
from .t_emails import TEmail
|
||||
from .t_projects import TProjects
|
||||
from .t_version import TVersion
|
||||
|
@ -16,7 +16,7 @@ class TComments(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
comments_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
comments_id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# foreign keys
|
||||
location_id = db.Column(db.Integer, ForeignKey('t_location.id_location'), nullable=False)
|
||||
@ -28,8 +28,7 @@ class TComments(db.Model):
|
||||
is_published = db.Column(db.Boolean, nullable=False)
|
||||
is_spam = db.Column(db.Boolean, nullable=False)
|
||||
spam_score = db.Column(db.Float, nullable=False)
|
||||
replied_to = db.Column(db.Integer, ForeignKey('t_comments.comments_id'), nullable=True, default=None)
|
||||
replied_to = db.Column(db.Integer, ForeignKey('t_comments.comments_id'), nullable=True)
|
||||
confirmation = db.Column(db.Text, nullable=True)
|
||||
deletion = db.Column(db.Text, nullable=True)
|
||||
gravatar = db.Column(db.Text, nullable=True)
|
||||
project_id = db.Column(db.Integer, ForeignKey('t_projects.id_project'), nullable=False)
|
||||
|
@ -7,7 +7,6 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
from sqlalchemy import ForeignKey
|
||||
|
||||
|
||||
class TEmail(db.Model):
|
||||
@ -16,7 +15,7 @@ class TEmail(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_email = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
id_email = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# data
|
||||
email = db.Column(db.Integer, unique=True)
|
||||
|
@ -7,7 +7,6 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
from sqlalchemy import ForeignKey, UniqueConstraint
|
||||
|
||||
|
||||
class TLocation(db.Model):
|
||||
@ -16,11 +15,7 @@ class TLocation(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_location = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
id_location = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# data
|
||||
location = db.Column(db.Text, nullable=False)
|
||||
project_id = db.Column(db.Integer, ForeignKey('t_projects.id_project'), nullable=False)
|
||||
|
||||
# Unique constraint
|
||||
UniqueConstraint('location', 'project_id', name="unique_per_project")
|
||||
location = db.Column(db.Text, nullable=False, unique=True)
|
||||
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
|
||||
|
||||
class TProjects(db.Model):
|
||||
# table name
|
||||
__tablename__ = "t_projects"
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_project = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# data
|
||||
name = db.Column(db.Text, nullable=False, unique=True)
|
||||
blogurl = db.Column(db.Text, nullable=False, unique=False)
|
||||
output = db.Column(db.Text, nullable=False, unique=True)
|
||||
sendotp = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
gravatar_cache = db.Column(db.Boolean, nullable=False)
|
||||
gravatar_cache_dir = db.Column(db.Text, nullable=True)
|
||||
gravatar_size = db.Column(db.Integer, nullable=True)
|
||||
|
||||
addon_smileys = db.Column(db.Boolean, nullable=False, default=True)
|
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
|
||||
|
||||
class TVersion(db.Model):
|
||||
# table name
|
||||
__tablename__ = "t_version"
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_version = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# data
|
||||
version = db.Column(db.Integer)
|
@ -9,170 +9,22 @@
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from shutil import copy
|
||||
from hashlib import pbkdf2_hmac
|
||||
from secrets import compare_digest
|
||||
|
||||
|
||||
def hash_password(password, secret=None):
|
||||
"""
|
||||
Hashes the administrator password
|
||||
:param password: The password to hash
|
||||
:param secret: The site secret
|
||||
:return: The hashed value as a hexadecimal string
|
||||
"""
|
||||
if not secret:
|
||||
secret = Secret()
|
||||
h = pbkdf2_hmac('sha512',
|
||||
password=password.encode('utf8'),
|
||||
salt=secret.encode('utf8'),
|
||||
iterations=250000)
|
||||
return h.hex()
|
||||
|
||||
|
||||
class Settings:
|
||||
|
||||
def __init__(self):
|
||||
file = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/labertasche.yaml")
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.weburl = conf['system']['weburl']
|
||||
self.cookie_domain = conf['system']['cookie_domain']
|
||||
self.database_uri = conf['system']['database_uri']
|
||||
self.debug = conf['system']['debug']
|
||||
self.cookie_secure = conf['system']['cookie_secure']
|
||||
|
||||
|
||||
class Secret:
|
||||
|
||||
def __init__(self):
|
||||
file = Path(".secret")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/.secret")
|
||||
|
||||
with file.open('r') as fp:
|
||||
self.key = fp.readline()
|
||||
|
||||
|
||||
class Smileys:
|
||||
|
||||
def __init__(self):
|
||||
file = Path("smileys.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/smileys.yaml")
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.smileys = conf['smileys']
|
||||
|
||||
|
||||
class Credentials:
|
||||
def __init__(self):
|
||||
file = Path("credentials.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/credentials.yaml")
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.username = conf['credentials']['username']
|
||||
self.password = conf['credentials']['password']
|
||||
|
||||
def compare_password(self, userinput):
|
||||
"""
|
||||
Compares 2 passwords with one another
|
||||
|
||||
:param userinput: The input on the login page
|
||||
:return: True if the passwords match, otherwise False
|
||||
"""
|
||||
secret = Secret()
|
||||
hashed = pbkdf2_hmac('sha512',
|
||||
password=userinput.encode('utf8'),
|
||||
salt=secret.key.encode('utf8'),
|
||||
iterations=250000)
|
||||
return compare_digest(self.password, hashed.hex())
|
||||
|
||||
|
||||
# deprecated, leave as is
|
||||
class LegacySettings:
|
||||
"""
|
||||
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
||||
"""
|
||||
def __init__(self, use_backup: bool = False):
|
||||
def __init__(self):
|
||||
file = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/labertasche.yaml")
|
||||
|
||||
# Use backup when conversion is done, this is used in db_v2.py
|
||||
if use_backup:
|
||||
file = file.with_suffix('.bak')
|
||||
|
||||
with file.open('r') as fp:
|
||||
print(f"Loading old conf from {file}")
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.system = conf['system']
|
||||
self.smileys = conf['smileys']
|
||||
self.dashboard = conf['dashboard']
|
||||
self.gravatar = conf['gravatar']
|
||||
self.addons = conf['addons']
|
||||
|
||||
def convert_to_v2(self):
|
||||
old = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
old = Path("/etc/labertasche/labertasche.yaml")
|
||||
|
||||
systemvars = {
|
||||
'system': {
|
||||
'weburl': self.system['web_url'],
|
||||
'cookie_domain': self.system['cookie-domain'],
|
||||
'database_uri': self.system['database_uri'],
|
||||
'debug': self.system['debug'],
|
||||
'cookie_secure': False
|
||||
}
|
||||
}
|
||||
|
||||
credentials = {
|
||||
'credentials': {
|
||||
'username': self.dashboard['username'],
|
||||
'password': hash_password(self.dashboard['password'], self.system['secret'])
|
||||
}
|
||||
}
|
||||
|
||||
smileys = {
|
||||
'smileys': self.smileys
|
||||
}
|
||||
|
||||
# backup old config
|
||||
print("Copying old config to backup")
|
||||
copy(old, old.with_suffix('.bak'))
|
||||
|
||||
# Write new config files
|
||||
p_sys = Path('labertasche.yaml')
|
||||
p_credentials = Path('credentials.yaml')
|
||||
p_smileys = Path('smileys.yaml')
|
||||
p_secret = Path('.secret')
|
||||
|
||||
if system().lower() == 'linux':
|
||||
p_sys = '/etc/labertasche/' / p_sys
|
||||
p_credentials = '/etc/labertasche/' / p_credentials
|
||||
p_smileys = '/etc/labertasche/' / p_smileys
|
||||
p_secret = '/etc/labertasche/' / p_secret
|
||||
|
||||
with p_sys.open('w') as fp:
|
||||
print("Dumping system vars as yaml")
|
||||
yaml.dump(systemvars, fp)
|
||||
with p_credentials.open('w') as fp:
|
||||
print("Dumping credentials as yaml")
|
||||
yaml.dump(credentials, fp)
|
||||
with p_smileys.open('w') as fp:
|
||||
print("Dumping smileys as yaml")
|
||||
yaml.dump(smileys, fp)
|
||||
with p_secret.open('w') as fp:
|
||||
print("Dumping secret")
|
||||
fp.write(self.system['secret'])
|
||||
self.smileys = conf['smileys']
|
||||
|
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
96
migrations/env.py
Normal file
96
migrations/env.py
Normal file
@ -0,0 +1,96 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
81
server.py
81
server.py
@ -6,74 +6,59 @@
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
import logging
|
||||
from flask import Flask, redirect, url_for
|
||||
from flask_cors import CORS
|
||||
from sqlalchemy import event
|
||||
# noinspection PyProtectedMember
|
||||
from sqlalchemy.engine import Engine
|
||||
from logging import getLogger, ERROR as LOGGING_ERROR
|
||||
from flask import Flask, redirect, url_for, request
|
||||
from flask_cors import CORS
|
||||
from sqlalchemy import event, inspect
|
||||
from labertasche.settings import Settings, Secret
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.database import labertasche_db
|
||||
from labertasche.language import Language
|
||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades
|
||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard
|
||||
from labertasche.helper import User
|
||||
from flask_login import LoginManager
|
||||
from datetime import timedelta
|
||||
from flask_migrate import Migrate
|
||||
|
||||
|
||||
# Load settings
|
||||
settings = Settings()
|
||||
secret = Secret()
|
||||
|
||||
# Flask App
|
||||
laberflask = Flask(__name__)
|
||||
laberflask.config.update(dict(
|
||||
SESSION_COOKIE_DOMAIN=settings.cookie_domain,
|
||||
SESSION_COOKIE_SECURE=settings.cookie_secure,
|
||||
REMEMBER_COOKIE_SECURE=settings.cookie_secure,
|
||||
REMEMBER_COOKIE_DURATION=timedelta(days=7),
|
||||
REMEMBER_COOKIE_HTTPONLY=True,
|
||||
REMEMBER_COOKIE_REFRESH_EACH_REQUEST=True,
|
||||
DEBUG=settings.debug,
|
||||
SECRET_KEY=secret.key,
|
||||
TEMPLATES_AUTO_RELOAD=settings.debug,
|
||||
SQLALCHEMY_DATABASE_URI=settings.database_uri,
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
||||
JSON_AS_ASCII=False
|
||||
SESSION_COOKIE_DOMAIN=settings.system['cookie-domain'],
|
||||
DEBUG=settings.system['debug'],
|
||||
SECRET_KEY=settings.system['secret'],
|
||||
TEMPLATES_AUTO_RELOAD=True,
|
||||
SQLALCHEMY_DATABASE_URI=settings.system['database_uri'],
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
||||
))
|
||||
|
||||
# Mark secret for deletion
|
||||
del secret
|
||||
# flask migrate
|
||||
migrate = Migrate(laberflask, labertasche_db, render_as_batch=True)
|
||||
|
||||
# CORS
|
||||
CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']}})
|
||||
|
||||
# Import blueprints
|
||||
laberflask.register_blueprint(bp_comments)
|
||||
laberflask.register_blueprint(bp_dashboard)
|
||||
laberflask.register_blueprint(bp_login)
|
||||
laberflask.register_blueprint(bp_jsconnector)
|
||||
laberflask.register_blueprint(bp_dbupgrades)
|
||||
|
||||
# Disable Werkzeug's verbosity during development
|
||||
log = getLogger('werkzeug')
|
||||
log.setLevel(LOGGING_ERROR)
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
# Initialize ORM
|
||||
labertasche_db.init_app(laberflask)
|
||||
with laberflask.app_context():
|
||||
labertasche_db.create_all()
|
||||
|
||||
# Set up login manager
|
||||
loginmgr = LoginManager(laberflask)
|
||||
loginmgr.login_view = 'bp_admin_login.login'
|
||||
|
||||
# Initialize ORM
|
||||
labertasche_db.init_app(laberflask)
|
||||
with laberflask.app_context():
|
||||
table_names = inspect(labertasche_db.get_engine()).get_table_names()
|
||||
is_empty = table_names == []
|
||||
# Only create tables if the db is empty, so we can do a controlled upgrade.
|
||||
if is_empty:
|
||||
labertasche_db.create_all()
|
||||
|
||||
|
||||
# CORS
|
||||
cors = CORS(laberflask, resources={r"/comments/*": {"origins": "*"}})
|
||||
|
||||
|
||||
# There is only one user
|
||||
@loginmgr.user_loader
|
||||
def user_loader(user_id):
|
||||
if user_id != "0":
|
||||
@ -81,26 +66,14 @@ def user_loader(user_id):
|
||||
return User(user_id)
|
||||
|
||||
|
||||
# User not authorized
|
||||
@loginmgr.unauthorized_handler
|
||||
def login_invalid():
|
||||
return redirect(url_for('bp_login.show_login'))
|
||||
|
||||
|
||||
# Enable write-ahead-log for sqlite databases
|
||||
# noinspection PyUnusedLocal
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
if settings.database_uri[0:6] == 'sqlite':
|
||||
if settings.system["database_uri"][0:6] == 'sqlite':
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||
cursor.close()
|
||||
|
||||
|
||||
# Inject i18n dictionaries into all templates
|
||||
@laberflask.context_processor
|
||||
def inject_language():
|
||||
lang = Language(request)
|
||||
return {"i18n": lang.i18n}
|
||||
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# """
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/piradio
|
||||
# * _license : This project is under GPL.v2
|
||||
# *********************************************************************************/
|
||||
from sys import path as sys_path
|
||||
from os import path as os_path
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
1
static/css/materialdesignicons.min.css
vendored
1
static/css/materialdesignicons.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
Subproject commit 5432d0625b34ebfdc17518baed80897a82018d2d
|
5
static/css/sass/bulma/base/_all.sass
Normal file
5
static/css/sass/bulma/base/_all.sass
Normal file
@ -0,0 +1,5 @@
|
||||
/* Bulma Base */
|
||||
@charset "utf-8"
|
||||
|
||||
@import "minireset.sass"
|
||||
@import "generic.sass"
|
143
static/css/sass/bulma/base/generic.sass
Normal file
143
static/css/sass/bulma/base/generic.sass
Normal file
@ -0,0 +1,143 @@
|
||||
$body-background-color: $scheme-main !default
|
||||
$body-size: 16px !default
|
||||
$body-min-width: 300px !default
|
||||
$body-rendering: optimizeLegibility !default
|
||||
$body-family: $family-primary !default
|
||||
$body-overflow-x: hidden !default
|
||||
$body-overflow-y: scroll !default
|
||||
|
||||
$body-color: $text !default
|
||||
$body-font-size: 1em !default
|
||||
$body-weight: $weight-normal !default
|
||||
$body-line-height: 1.5 !default
|
||||
|
||||
$code-family: $family-code !default
|
||||
$code-padding: 0.25em 0.5em 0.25em !default
|
||||
$code-weight: normal !default
|
||||
$code-size: 0.875em !default
|
||||
|
||||
$small-font-size: 0.875em !default
|
||||
|
||||
$hr-background-color: $background !default
|
||||
$hr-height: 2px !default
|
||||
$hr-margin: 1.5rem 0 !default
|
||||
|
||||
$strong-color: $text-strong !default
|
||||
$strong-weight: $weight-bold !default
|
||||
|
||||
$pre-font-size: 0.875em !default
|
||||
$pre-padding: 1.25rem 1.5rem !default
|
||||
$pre-code-font-size: 1em !default
|
||||
|
||||
html
|
||||
background-color: $body-background-color
|
||||
font-size: $body-size
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
-webkit-font-smoothing: antialiased
|
||||
min-width: $body-min-width
|
||||
overflow-x: $body-overflow-x
|
||||
overflow-y: $body-overflow-y
|
||||
text-rendering: $body-rendering
|
||||
text-size-adjust: 100%
|
||||
|
||||
article,
|
||||
aside,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
section
|
||||
display: block
|
||||
|
||||
body,
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea
|
||||
font-family: $body-family
|
||||
|
||||
code,
|
||||
pre
|
||||
-moz-osx-font-smoothing: auto
|
||||
-webkit-font-smoothing: auto
|
||||
font-family: $code-family
|
||||
|
||||
body
|
||||
color: $body-color
|
||||
font-size: $body-font-size
|
||||
font-weight: $body-weight
|
||||
line-height: $body-line-height
|
||||
|
||||
// Inline
|
||||
|
||||
a
|
||||
color: $link
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
strong
|
||||
color: currentColor
|
||||
&:hover
|
||||
color: $link-hover
|
||||
|
||||
code
|
||||
background-color: $code-background
|
||||
color: $code
|
||||
font-size: $code-size
|
||||
font-weight: $code-weight
|
||||
padding: $code-padding
|
||||
|
||||
hr
|
||||
background-color: $hr-background-color
|
||||
border: none
|
||||
display: block
|
||||
height: $hr-height
|
||||
margin: $hr-margin
|
||||
|
||||
img
|
||||
height: auto
|
||||
max-width: 100%
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"]
|
||||
vertical-align: baseline
|
||||
|
||||
small
|
||||
font-size: $small-font-size
|
||||
|
||||
span
|
||||
font-style: inherit
|
||||
font-weight: inherit
|
||||
|
||||
strong
|
||||
color: $strong-color
|
||||
font-weight: $strong-weight
|
||||
|
||||
// Block
|
||||
|
||||
fieldset
|
||||
border: none
|
||||
|
||||
pre
|
||||
+overflow-touch
|
||||
background-color: $pre-background
|
||||
color: $pre
|
||||
font-size: $pre-font-size
|
||||
overflow-x: auto
|
||||
padding: $pre-padding
|
||||
white-space: pre
|
||||
word-wrap: normal
|
||||
code
|
||||
background-color: transparent
|
||||
color: currentColor
|
||||
font-size: $pre-code-font-size
|
||||
padding: 0
|
||||
|
||||
table
|
||||
td,
|
||||
th
|
||||
vertical-align: top
|
||||
&:not([align])
|
||||
text-align: inherit
|
||||
th
|
||||
color: $text-strong
|
1
static/css/sass/bulma/base/helpers.sass
Normal file
1
static/css/sass/bulma/base/helpers.sass
Normal file
@ -0,0 +1 @@
|
||||
@warn "The helpers.sass file is DEPRECATED. It has moved into its own /helpers folder. Please import sass/helpers/_all instead."
|
79
static/css/sass/bulma/base/minireset.sass
Normal file
79
static/css/sass/bulma/base/minireset.sass
Normal file
@ -0,0 +1,79 @@
|
||||
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
|
||||
// Blocks
|
||||
html,
|
||||
body,
|
||||
p,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
blockquote,
|
||||
figure,
|
||||
fieldset,
|
||||
legend,
|
||||
textarea,
|
||||
pre,
|
||||
iframe,
|
||||
hr,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
// Headings
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6
|
||||
font-size: 100%
|
||||
font-weight: normal
|
||||
|
||||
// List
|
||||
ul
|
||||
list-style: none
|
||||
|
||||
// Form
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea
|
||||
margin: 0
|
||||
|
||||
// Box sizing
|
||||
html
|
||||
box-sizing: border-box
|
||||
|
||||
*
|
||||
&,
|
||||
&::before,
|
||||
&::after
|
||||
box-sizing: inherit
|
||||
|
||||
// Media
|
||||
img,
|
||||
video
|
||||
height: auto
|
||||
max-width: 100%
|
||||
|
||||
// Iframe
|
||||
iframe
|
||||
border: 0
|
||||
|
||||
// Table
|
||||
table
|
||||
border-collapse: collapse
|
||||
border-spacing: 0
|
||||
|
||||
td,
|
||||
th
|
||||
padding: 0
|
||||
&:not([align])
|
||||
text-align: inherit
|
15
static/css/sass/bulma/components/_all.sass
Normal file
15
static/css/sass/bulma/components/_all.sass
Normal file
@ -0,0 +1,15 @@
|
||||
/* Bulma Components */
|
||||
@charset "utf-8"
|
||||
|
||||
@import "breadcrumb.sass"
|
||||
@import "card.sass"
|
||||
@import "dropdown.sass"
|
||||
@import "level.sass"
|
||||
@import "media.sass"
|
||||
@import "menu.sass"
|
||||
@import "message.sass"
|
||||
@import "modal.sass"
|
||||
@import "navbar.sass"
|
||||
@import "pagination.sass"
|
||||
@import "panel.sass"
|
||||
@import "tabs.sass"
|
75
static/css/sass/bulma/components/breadcrumb.sass
Normal file
75
static/css/sass/bulma/components/breadcrumb.sass
Normal file
@ -0,0 +1,75 @@
|
||||
$breadcrumb-item-color: $link !default
|
||||
$breadcrumb-item-hover-color: $link-hover !default
|
||||
$breadcrumb-item-active-color: $text-strong !default
|
||||
|
||||
$breadcrumb-item-padding-vertical: 0 !default
|
||||
$breadcrumb-item-padding-horizontal: 0.75em !default
|
||||
|
||||
$breadcrumb-item-separator-color: $border-hover !default
|
||||
|
||||
.breadcrumb
|
||||
@extend %block
|
||||
@extend %unselectable
|
||||
font-size: $size-normal
|
||||
white-space: nowrap
|
||||
a
|
||||
align-items: center
|
||||
color: $breadcrumb-item-color
|
||||
display: flex
|
||||
justify-content: center
|
||||
padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal
|
||||
&:hover
|
||||
color: $breadcrumb-item-hover-color
|
||||
li
|
||||
align-items: center
|
||||
display: flex
|
||||
&:first-child a
|
||||
+ltr-property("padding", 0, false)
|
||||
&.is-active
|
||||
a
|
||||
color: $breadcrumb-item-active-color
|
||||
cursor: default
|
||||
pointer-events: none
|
||||
& + li::before
|
||||
color: $breadcrumb-item-separator-color
|
||||
content: "\0002f"
|
||||
ul,
|
||||
ol
|
||||
align-items: flex-start
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: flex-start
|
||||
.icon
|
||||
&:first-child
|
||||
+ltr-property("margin", 0.5em)
|
||||
&:last-child
|
||||
+ltr-property("margin", 0.5em, false)
|
||||
// Alignment
|
||||
&.is-centered
|
||||
ol,
|
||||
ul
|
||||
justify-content: center
|
||||
&.is-right
|
||||
ol,
|
||||
ul
|
||||
justify-content: flex-end
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
||||
// Styles
|
||||
&.has-arrow-separator
|
||||
li + li::before
|
||||
content: "\02192"
|
||||
&.has-bullet-separator
|
||||
li + li::before
|
||||
content: "\02022"
|
||||
&.has-dot-separator
|
||||
li + li::before
|
||||
content: "\000b7"
|
||||
&.has-succeeds-separator
|
||||
li + li::before
|
||||
content: "\0227B"
|
83
static/css/sass/bulma/components/card.sass
Normal file
83
static/css/sass/bulma/components/card.sass
Normal file
@ -0,0 +1,83 @@
|
||||
$card-color: $text !default
|
||||
$card-background-color: $scheme-main !default
|
||||
$card-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default
|
||||
$card-radius: 0.25rem !default
|
||||
$card-overflow: hidden !default
|
||||
|
||||
$card-header-background-color: transparent !default
|
||||
$card-header-color: $text-strong !default
|
||||
$card-header-padding: 0.75rem 1rem !default
|
||||
$card-header-shadow: 0 0.125em 0.25em rgba($scheme-invert, 0.1) !default
|
||||
$card-header-weight: $weight-bold !default
|
||||
|
||||
$card-content-background-color: transparent !default
|
||||
$card-content-padding: 1.5rem !default
|
||||
|
||||
$card-footer-background-color: transparent !default
|
||||
$card-footer-border-top: 1px solid $border-light !default
|
||||
$card-footer-padding: 0.75rem !default
|
||||
|
||||
$card-media-margin: $block-spacing !default
|
||||
|
||||
.card
|
||||
background-color: $card-background-color
|
||||
border-radius: $card-radius
|
||||
box-shadow: $card-shadow
|
||||
color: $card-color
|
||||
max-width: 100%
|
||||
overflow: $card-overflow
|
||||
position: relative
|
||||
|
||||
.card-header
|
||||
background-color: $card-header-background-color
|
||||
align-items: stretch
|
||||
box-shadow: $card-header-shadow
|
||||
display: flex
|
||||
|
||||
.card-header-title
|
||||
align-items: center
|
||||
color: $card-header-color
|
||||
display: flex
|
||||
flex-grow: 1
|
||||
font-weight: $card-header-weight
|
||||
padding: $card-header-padding
|
||||
&.is-centered
|
||||
justify-content: center
|
||||
|
||||
.card-header-icon
|
||||
align-items: center
|
||||
cursor: pointer
|
||||
display: flex
|
||||
justify-content: center
|
||||
padding: $card-header-padding
|
||||
|
||||
.card-image
|
||||
display: block
|
||||
position: relative
|
||||
|
||||
.card-content
|
||||
background-color: $card-content-background-color
|
||||
padding: $card-content-padding
|
||||
|
||||
.card-footer
|
||||
background-color: $card-footer-background-color
|
||||
border-top: $card-footer-border-top
|
||||
align-items: stretch
|
||||
display: flex
|
||||
|
||||
.card-footer-item
|
||||
align-items: center
|
||||
display: flex
|
||||
flex-basis: 0
|
||||
flex-grow: 1
|
||||
flex-shrink: 0
|
||||
justify-content: center
|
||||
padding: $card-footer-padding
|
||||
&:not(:last-child)
|
||||
+ltr-property("border", $card-footer-border-top)
|
||||
|
||||
// Combinations
|
||||
|
||||
.card
|
||||
.media:not(:last-child)
|
||||
margin-bottom: $card-media-margin
|
81
static/css/sass/bulma/components/dropdown.sass
Normal file
81
static/css/sass/bulma/components/dropdown.sass
Normal file
@ -0,0 +1,81 @@
|
||||
$dropdown-menu-min-width: 12rem !default
|
||||
|
||||
$dropdown-content-background-color: $scheme-main !default
|
||||
$dropdown-content-arrow: $link !default
|
||||
$dropdown-content-offset: 4px !default
|
||||
$dropdown-content-padding-bottom: 0.5rem !default
|
||||
$dropdown-content-padding-top: 0.5rem !default
|
||||
$dropdown-content-radius: $radius !default
|
||||
$dropdown-content-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default
|
||||
$dropdown-content-z: 20 !default
|
||||
|
||||
$dropdown-item-color: $text !default
|
||||
$dropdown-item-hover-color: $scheme-invert !default
|
||||
$dropdown-item-hover-background-color: $background !default
|
||||
$dropdown-item-active-color: $link-invert !default
|
||||
$dropdown-item-active-background-color: $link !default
|
||||
|
||||
$dropdown-divider-background-color: $border-light !default
|
||||
|
||||
.dropdown
|
||||
display: inline-flex
|
||||
position: relative
|
||||
vertical-align: top
|
||||
&.is-active,
|
||||
&.is-hoverable:hover
|
||||
.dropdown-menu
|
||||
display: block
|
||||
&.is-right
|
||||
.dropdown-menu
|
||||
left: auto
|
||||
right: 0
|
||||
&.is-up
|
||||
.dropdown-menu
|
||||
bottom: 100%
|
||||
padding-bottom: $dropdown-content-offset
|
||||
padding-top: initial
|
||||
top: auto
|
||||
|
||||
.dropdown-menu
|
||||
display: none
|
||||
+ltr-position(0, false)
|
||||
min-width: $dropdown-menu-min-width
|
||||
padding-top: $dropdown-content-offset
|
||||
position: absolute
|
||||
top: 100%
|
||||
z-index: $dropdown-content-z
|
||||
|
||||
.dropdown-content
|
||||
background-color: $dropdown-content-background-color
|
||||
border-radius: $dropdown-content-radius
|
||||
box-shadow: $dropdown-content-shadow
|
||||
padding-bottom: $dropdown-content-padding-bottom
|
||||
padding-top: $dropdown-content-padding-top
|
||||
|
||||
.dropdown-item
|
||||
color: $dropdown-item-color
|
||||
display: block
|
||||
font-size: 0.875rem
|
||||
line-height: 1.5
|
||||
padding: 0.375rem 1rem
|
||||
position: relative
|
||||
|
||||
a.dropdown-item,
|
||||
button.dropdown-item
|
||||
+ltr-property("padding", 3rem)
|
||||
text-align: inherit
|
||||
white-space: nowrap
|
||||
width: 100%
|
||||
&:hover
|
||||
background-color: $dropdown-item-hover-background-color
|
||||
color: $dropdown-item-hover-color
|
||||
&.is-active
|
||||
background-color: $dropdown-item-active-background-color
|
||||
color: $dropdown-item-active-color
|
||||
|
||||
.dropdown-divider
|
||||
background-color: $dropdown-divider-background-color
|
||||
border: none
|
||||
display: block
|
||||
height: 1px
|
||||
margin: 0.5rem 0
|
77
static/css/sass/bulma/components/level.sass
Normal file
77
static/css/sass/bulma/components/level.sass
Normal file
@ -0,0 +1,77 @@
|
||||
$level-item-spacing: ($block-spacing / 2) !default
|
||||
|
||||
.level
|
||||
@extend %block
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
code
|
||||
border-radius: $radius
|
||||
img
|
||||
display: inline-block
|
||||
vertical-align: top
|
||||
// Modifiers
|
||||
&.is-mobile
|
||||
display: flex
|
||||
.level-left,
|
||||
.level-right
|
||||
display: flex
|
||||
.level-left + .level-right
|
||||
margin-top: 0
|
||||
.level-item
|
||||
&:not(:last-child)
|
||||
margin-bottom: 0
|
||||
+ltr-property("margin", $level-item-spacing)
|
||||
&:not(.is-narrow)
|
||||
flex-grow: 1
|
||||
// Responsiveness
|
||||
+tablet
|
||||
display: flex
|
||||
& > .level-item
|
||||
&:not(.is-narrow)
|
||||
flex-grow: 1
|
||||
|
||||
.level-item
|
||||
align-items: center
|
||||
display: flex
|
||||
flex-basis: auto
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
justify-content: center
|
||||
.title,
|
||||
.subtitle
|
||||
margin-bottom: 0
|
||||
// Responsiveness
|
||||
+mobile
|
||||
&:not(:last-child)
|
||||
margin-bottom: $level-item-spacing
|
||||
|
||||
.level-left,
|
||||
.level-right
|
||||
flex-basis: auto
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
.level-item
|
||||
// Modifiers
|
||||
&.is-flexible
|
||||
flex-grow: 1
|
||||
// Responsiveness
|
||||
+tablet
|
||||
&:not(:last-child)
|
||||
+ltr-property("margin", $level-item-spacing)
|
||||
|
||||
.level-left
|
||||
align-items: center
|
||||
justify-content: flex-start
|
||||
// Responsiveness
|
||||
+mobile
|
||||
& + .level-right
|
||||
margin-top: 1.5rem
|
||||
+tablet
|
||||
display: flex
|
||||
|
||||
.level-right
|
||||
align-items: center
|
||||
justify-content: flex-end
|
||||
// Responsiveness
|
||||
+tablet
|
||||
display: flex
|
52
static/css/sass/bulma/components/media.sass
Normal file
52
static/css/sass/bulma/components/media.sass
Normal file
@ -0,0 +1,52 @@
|
||||
$media-border-color: bulmaRgba($border, 0.5) !default
|
||||
$media-spacing: 1rem
|
||||
$media-spacing-large: 1.5rem
|
||||
|
||||
.media
|
||||
align-items: flex-start
|
||||
display: flex
|
||||
text-align: inherit
|
||||
.content:not(:last-child)
|
||||
margin-bottom: 0.75rem
|
||||
.media
|
||||
border-top: 1px solid $media-border-color
|
||||
display: flex
|
||||
padding-top: 0.75rem
|
||||
.content:not(:last-child),
|
||||
.control:not(:last-child)
|
||||
margin-bottom: 0.5rem
|
||||
.media
|
||||
padding-top: 0.5rem
|
||||
& + .media
|
||||
margin-top: 0.5rem
|
||||
& + .media
|
||||
border-top: 1px solid $media-border-color
|
||||
margin-top: $media-spacing
|
||||
padding-top: $media-spacing
|
||||
// Sizes
|
||||
&.is-large
|
||||
& + .media
|
||||
margin-top: $media-spacing-large
|
||||
padding-top: $media-spacing-large
|
||||
|
||||
.media-left,
|
||||
.media-right
|
||||
flex-basis: auto
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
|
||||
.media-left
|
||||
+ltr-property("margin", $media-spacing)
|
||||
|
||||
.media-right
|
||||
+ltr-property("margin", $media-spacing, false)
|
||||
|
||||
.media-content
|
||||
flex-basis: auto
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
text-align: inherit
|
||||
|
||||
+mobile
|
||||
.media-content
|
||||
overflow-x: auto
|
57
static/css/sass/bulma/components/menu.sass
Normal file
57
static/css/sass/bulma/components/menu.sass
Normal file
@ -0,0 +1,57 @@
|
||||
$menu-item-color: $text !default
|
||||
$menu-item-radius: $radius-small !default
|
||||
$menu-item-hover-color: $text-strong !default
|
||||
$menu-item-hover-background-color: $background !default
|
||||
$menu-item-active-color: $link-invert !default
|
||||
$menu-item-active-background-color: $link !default
|
||||
|
||||
$menu-list-border-left: 1px solid $border !default
|
||||
$menu-list-line-height: 1.25 !default
|
||||
$menu-list-link-padding: 0.5em 0.75em !default
|
||||
$menu-nested-list-margin: 0.75em !default
|
||||
$menu-nested-list-padding-left: 0.75em !default
|
||||
|
||||
$menu-label-color: $text-light !default
|
||||
$menu-label-font-size: 0.75em !default
|
||||
$menu-label-letter-spacing: 0.1em !default
|
||||
$menu-label-spacing: 1em !default
|
||||
|
||||
.menu
|
||||
font-size: $size-normal
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
||||
|
||||
.menu-list
|
||||
line-height: $menu-list-line-height
|
||||
a
|
||||
border-radius: $menu-item-radius
|
||||
color: $menu-item-color
|
||||
display: block
|
||||
padding: $menu-list-link-padding
|
||||
&:hover
|
||||
background-color: $menu-item-hover-background-color
|
||||
color: $menu-item-hover-color
|
||||
// Modifiers
|
||||
&.is-active
|
||||
background-color: $menu-item-active-background-color
|
||||
color: $menu-item-active-color
|
||||
li
|
||||
ul
|
||||
+ltr-property("border", $menu-list-border-left, false)
|
||||
margin: $menu-nested-list-margin
|
||||
+ltr-property("padding", $menu-nested-list-padding-left, false)
|
||||
|
||||
.menu-label
|
||||
color: $menu-label-color
|
||||
font-size: $menu-label-font-size
|
||||
letter-spacing: $menu-label-letter-spacing
|
||||
text-transform: uppercase
|
||||
&:not(:first-child)
|
||||
margin-top: $menu-label-spacing
|
||||
&:not(:last-child)
|
||||
margin-bottom: $menu-label-spacing
|
99
static/css/sass/bulma/components/message.sass
Normal file
99
static/css/sass/bulma/components/message.sass
Normal file
@ -0,0 +1,99 @@
|
||||
$message-background-color: $background !default
|
||||
$message-radius: $radius !default
|
||||
|
||||
$message-header-background-color: $text !default
|
||||
$message-header-color: $text-invert !default
|
||||
$message-header-weight: $weight-bold !default
|
||||
$message-header-padding: 0.75em 1em !default
|
||||
$message-header-radius: $radius !default
|
||||
|
||||
$message-body-border-color: $border !default
|
||||
$message-body-border-width: 0 0 0 4px !default
|
||||
$message-body-color: $text !default
|
||||
$message-body-padding: 1.25em 1.5em !default
|
||||
$message-body-radius: $radius !default
|
||||
|
||||
$message-body-pre-background-color: $scheme-main !default
|
||||
$message-body-pre-code-background-color: transparent !default
|
||||
|
||||
$message-header-body-border-width: 0 !default
|
||||
$message-colors: $colors !default
|
||||
|
||||
.message
|
||||
@extend %block
|
||||
background-color: $message-background-color
|
||||
border-radius: $message-radius
|
||||
font-size: $size-normal
|
||||
strong
|
||||
color: currentColor
|
||||
a:not(.button):not(.tag):not(.dropdown-item)
|
||||
color: currentColor
|
||||
text-decoration: underline
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
||||
// Colors
|
||||
@each $name, $components in $message-colors
|
||||
$color: nth($components, 1)
|
||||
$color-invert: nth($components, 2)
|
||||
$color-light: null
|
||||
$color-dark: null
|
||||
|
||||
@if length($components) >= 3
|
||||
$color-light: nth($components, 3)
|
||||
@if length($components) >= 4
|
||||
$color-dark: nth($components, 4)
|
||||
@else
|
||||
$color-luminance: colorLuminance($color)
|
||||
$darken-percentage: $color-luminance * 70%
|
||||
$desaturate-percentage: $color-luminance * 30%
|
||||
$color-dark: desaturate(darken($color, $darken-percentage), $desaturate-percentage)
|
||||
@else
|
||||
$color-lightning: max((100% - lightness($color)) - 2%, 0%)
|
||||
$color-light: lighten($color, $color-lightning)
|
||||
|
||||
&.is-#{$name}
|
||||
background-color: $color-light
|
||||
.message-header
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
.message-body
|
||||
border-color: $color
|
||||
color: $color-dark
|
||||
|
||||
.message-header
|
||||
align-items: center
|
||||
background-color: $message-header-background-color
|
||||
border-radius: $message-header-radius $message-header-radius 0 0
|
||||
color: $message-header-color
|
||||
display: flex
|
||||
font-weight: $message-header-weight
|
||||
justify-content: space-between
|
||||
line-height: 1.25
|
||||
padding: $message-header-padding
|
||||
position: relative
|
||||
.delete
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
+ltr-property("margin", 0.75em, false)
|
||||
& + .message-body
|
||||
border-width: $message-header-body-border-width
|
||||
border-top-left-radius: 0
|
||||
border-top-right-radius: 0
|
||||
|
||||
.message-body
|
||||
border-color: $message-body-border-color
|
||||
border-radius: $message-body-radius
|
||||
border-style: solid
|
||||
border-width: $message-body-border-width
|
||||
color: $message-body-color
|
||||
padding: $message-body-padding
|
||||
code,
|
||||
pre
|
||||
background-color: $message-body-pre-background-color
|
||||
pre code
|
||||
background-color: $message-body-pre-code-background-color
|
115
static/css/sass/bulma/components/modal.sass
Normal file
115
static/css/sass/bulma/components/modal.sass
Normal file
@ -0,0 +1,115 @@
|
||||
$modal-z: 40 !default
|
||||
|
||||
$modal-background-background-color: bulmaRgba($scheme-invert, 0.86) !default
|
||||
|
||||
$modal-content-width: 640px !default
|
||||
$modal-content-margin-mobile: 20px !default
|
||||
$modal-content-spacing-mobile: 160px !default
|
||||
$modal-content-spacing-tablet: 40px !default
|
||||
|
||||
$modal-close-dimensions: 40px !default
|
||||
$modal-close-right: 20px !default
|
||||
$modal-close-top: 20px !default
|
||||
|
||||
$modal-card-spacing: 40px !default
|
||||
|
||||
$modal-card-head-background-color: $background !default
|
||||
$modal-card-head-border-bottom: 1px solid $border !default
|
||||
$modal-card-head-padding: 20px !default
|
||||
$modal-card-head-radius: $radius-large !default
|
||||
|
||||
$modal-card-title-color: $text-strong !default
|
||||
$modal-card-title-line-height: 1 !default
|
||||
$modal-card-title-size: $size-4 !default
|
||||
|
||||
$modal-card-foot-radius: $radius-large !default
|
||||
$modal-card-foot-border-top: 1px solid $border !default
|
||||
|
||||
$modal-card-body-background-color: $scheme-main !default
|
||||
$modal-card-body-padding: 20px !default
|
||||
|
||||
$modal-breakpoint: $tablet !default
|
||||
|
||||
.modal
|
||||
@extend %overlay
|
||||
align-items: center
|
||||
display: none
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
overflow: hidden
|
||||
position: fixed
|
||||
z-index: $modal-z
|
||||
// Modifiers
|
||||
&.is-active
|
||||
display: flex
|
||||
|
||||
.modal-background
|
||||
@extend %overlay
|
||||
background-color: $modal-background-background-color
|
||||
|
||||
.modal-content,
|
||||
.modal-card
|
||||
margin: 0 $modal-content-margin-mobile
|
||||
max-height: calc(100vh - #{$modal-content-spacing-mobile})
|
||||
overflow: auto
|
||||
position: relative
|
||||
width: 100%
|
||||
// Responsiveness
|
||||
+from($modal-breakpoint)
|
||||
margin: 0 auto
|
||||
max-height: calc(100vh - #{$modal-content-spacing-tablet})
|
||||
width: $modal-content-width
|
||||
|
||||
.modal-close
|
||||
@extend %delete
|
||||
background: none
|
||||
height: $modal-close-dimensions
|
||||
position: fixed
|
||||
+ltr-position($modal-close-right)
|
||||
top: $modal-close-top
|
||||
width: $modal-close-dimensions
|
||||
|
||||
.modal-card
|
||||
display: flex
|
||||
flex-direction: column
|
||||
max-height: calc(100vh - #{$modal-card-spacing})
|
||||
overflow: hidden
|
||||
-ms-overflow-y: visible
|
||||
|
||||
.modal-card-head,
|
||||
.modal-card-foot
|
||||
align-items: center
|
||||
background-color: $modal-card-head-background-color
|
||||
display: flex
|
||||
flex-shrink: 0
|
||||
justify-content: flex-start
|
||||
padding: $modal-card-head-padding
|
||||
position: relative
|
||||
|
||||
.modal-card-head
|
||||
border-bottom: $modal-card-head-border-bottom
|
||||
border-top-left-radius: $modal-card-head-radius
|
||||
border-top-right-radius: $modal-card-head-radius
|
||||
|
||||
.modal-card-title
|
||||
color: $modal-card-title-color
|
||||
flex-grow: 1
|
||||
flex-shrink: 0
|
||||
font-size: $modal-card-title-size
|
||||
line-height: $modal-card-title-line-height
|
||||
|
||||
.modal-card-foot
|
||||
border-bottom-left-radius: $modal-card-foot-radius
|
||||
border-bottom-right-radius: $modal-card-foot-radius
|
||||
border-top: $modal-card-foot-border-top
|
||||
.button
|
||||
&:not(:last-child)
|
||||
+ltr-property("margin", 0.5em)
|
||||
|
||||
.modal-card-body
|
||||
+overflow-touch
|
||||
background-color: $modal-card-body-background-color
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
overflow: auto
|
||||
padding: $modal-card-body-padding
|
443
static/css/sass/bulma/components/navbar.sass
Normal file
443
static/css/sass/bulma/components/navbar.sass
Normal file
@ -0,0 +1,443 @@
|
||||
$navbar-background-color: $scheme-main !default
|
||||
$navbar-box-shadow-size: 0 2px 0 0 !default
|
||||
$navbar-box-shadow-color: $background !default
|
||||
$navbar-height: 3.25rem !default
|
||||
$navbar-padding-vertical: 1rem !default
|
||||
$navbar-padding-horizontal: 2rem !default
|
||||
$navbar-z: 30 !default
|
||||
$navbar-fixed-z: 30 !default
|
||||
|
||||
$navbar-item-color: $text !default
|
||||
$navbar-item-hover-color: $link !default
|
||||
$navbar-item-hover-background-color: $scheme-main-bis !default
|
||||
$navbar-item-active-color: $scheme-invert !default
|
||||
$navbar-item-active-background-color: transparent !default
|
||||
$navbar-item-img-max-height: 1.75rem !default
|
||||
|
||||
$navbar-burger-color: $navbar-item-color !default
|
||||
|
||||
$navbar-tab-hover-background-color: transparent !default
|
||||
$navbar-tab-hover-border-bottom-color: $link !default
|
||||
$navbar-tab-active-color: $link !default
|
||||
$navbar-tab-active-background-color: transparent !default
|
||||
$navbar-tab-active-border-bottom-color: $link !default
|
||||
$navbar-tab-active-border-bottom-style: solid !default
|
||||
$navbar-tab-active-border-bottom-width: 3px !default
|
||||
|
||||
$navbar-dropdown-background-color: $scheme-main !default
|
||||
$navbar-dropdown-border-top: 2px solid $border !default
|
||||
$navbar-dropdown-offset: -4px !default
|
||||
$navbar-dropdown-arrow: $link !default
|
||||
$navbar-dropdown-radius: $radius-large !default
|
||||
$navbar-dropdown-z: 20 !default
|
||||
|
||||
$navbar-dropdown-boxed-radius: $radius-large !default
|
||||
$navbar-dropdown-boxed-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1), 0 0 0 1px bulmaRgba($scheme-invert, 0.1) !default
|
||||
|
||||
$navbar-dropdown-item-hover-color: $scheme-invert !default
|
||||
$navbar-dropdown-item-hover-background-color: $background !default
|
||||
$navbar-dropdown-item-active-color: $link !default
|
||||
$navbar-dropdown-item-active-background-color: $background !default
|
||||
|
||||
$navbar-divider-background-color: $background !default
|
||||
$navbar-divider-height: 2px !default
|
||||
|
||||
$navbar-bottom-box-shadow-size: 0 -2px 0 0 !default
|
||||
|
||||
$navbar-breakpoint: $desktop !default
|
||||
|
||||
$navbar-colors: $colors !default
|
||||
|
||||
=navbar-fixed
|
||||
left: 0
|
||||
position: fixed
|
||||
right: 0
|
||||
z-index: $navbar-fixed-z
|
||||
|
||||
.navbar
|
||||
background-color: $navbar-background-color
|
||||
min-height: $navbar-height
|
||||
position: relative
|
||||
z-index: $navbar-z
|
||||
@each $name, $pair in $navbar-colors
|
||||
$color: nth($pair, 1)
|
||||
$color-invert: nth($pair, 2)
|
||||
&.is-#{$name}
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
.navbar-brand
|
||||
& > .navbar-item,
|
||||
.navbar-link
|
||||
color: $color-invert
|
||||
& > a.navbar-item,
|
||||
.navbar-link
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-active
|
||||
background-color: bulmaDarken($color, 5%)
|
||||
color: $color-invert
|
||||
.navbar-link
|
||||
&::after
|
||||
border-color: $color-invert
|
||||
.navbar-burger
|
||||
color: $color-invert
|
||||
+from($navbar-breakpoint)
|
||||
.navbar-start,
|
||||
.navbar-end
|
||||
& > .navbar-item,
|
||||
.navbar-link
|
||||
color: $color-invert
|
||||
& > a.navbar-item,
|
||||
.navbar-link
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-active
|
||||
background-color: bulmaDarken($color, 5%)
|
||||
color: $color-invert
|
||||
.navbar-link
|
||||
&::after
|
||||
border-color: $color-invert
|
||||
.navbar-item.has-dropdown:focus .navbar-link,
|
||||
.navbar-item.has-dropdown:hover .navbar-link,
|
||||
.navbar-item.has-dropdown.is-active .navbar-link
|
||||
background-color: bulmaDarken($color, 5%)
|
||||
color: $color-invert
|
||||
.navbar-dropdown
|
||||
a.navbar-item
|
||||
&.is-active
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
& > .container
|
||||
align-items: stretch
|
||||
display: flex
|
||||
min-height: $navbar-height
|
||||
width: 100%
|
||||
&.has-shadow
|
||||
box-shadow: $navbar-box-shadow-size $navbar-box-shadow-color
|
||||
&.is-fixed-bottom,
|
||||
&.is-fixed-top
|
||||
+navbar-fixed
|
||||
&.is-fixed-bottom
|
||||
bottom: 0
|
||||
&.has-shadow
|
||||
box-shadow: $navbar-bottom-box-shadow-size $navbar-box-shadow-color
|
||||
&.is-fixed-top
|
||||
top: 0
|
||||
|
||||
html,
|
||||
body
|
||||
&.has-navbar-fixed-top
|
||||
padding-top: $navbar-height
|
||||
&.has-navbar-fixed-bottom
|
||||
padding-bottom: $navbar-height
|
||||
|
||||
.navbar-brand,
|
||||
.navbar-tabs
|
||||
align-items: stretch
|
||||
display: flex
|
||||
flex-shrink: 0
|
||||
min-height: $navbar-height
|
||||
|
||||
.navbar-brand
|
||||
a.navbar-item
|
||||
&:focus,
|
||||
&:hover
|
||||
background-color: transparent
|
||||
|
||||
.navbar-tabs
|
||||
+overflow-touch
|
||||
max-width: 100vw
|
||||
overflow-x: auto
|
||||
overflow-y: hidden
|
||||
|
||||
.navbar-burger
|
||||
color: $navbar-burger-color
|
||||
+hamburger($navbar-height)
|
||||
+ltr-property("margin", auto, false)
|
||||
|
||||
.navbar-menu
|
||||
display: none
|
||||
|
||||
.navbar-item,
|
||||
.navbar-link
|
||||
color: $navbar-item-color
|
||||
display: block
|
||||
line-height: 1.5
|
||||
padding: 0.5rem 0.75rem
|
||||
position: relative
|
||||
.icon
|
||||
&:only-child
|
||||
margin-left: -0.25rem
|
||||
margin-right: -0.25rem
|
||||
|
||||
a.navbar-item,
|
||||
.navbar-link
|
||||
cursor: pointer
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&:hover,
|
||||
&.is-active
|
||||
background-color: $navbar-item-hover-background-color
|
||||
color: $navbar-item-hover-color
|
||||
|
||||
.navbar-item
|
||||
flex-grow: 0
|
||||
flex-shrink: 0
|
||||
img
|
||||
max-height: $navbar-item-img-max-height
|
||||
&.has-dropdown
|
||||
padding: 0
|
||||
&.is-expanded
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
&.is-tab
|
||||
border-bottom: 1px solid transparent
|
||||
min-height: $navbar-height
|
||||
padding-bottom: calc(0.5rem - 1px)
|
||||
&:focus,
|
||||
&:hover
|
||||
background-color: $navbar-tab-hover-background-color
|
||||
border-bottom-color: $navbar-tab-hover-border-bottom-color
|
||||
&.is-active
|
||||
background-color: $navbar-tab-active-background-color
|
||||
border-bottom-color: $navbar-tab-active-border-bottom-color
|
||||
border-bottom-style: $navbar-tab-active-border-bottom-style
|
||||
border-bottom-width: $navbar-tab-active-border-bottom-width
|
||||
color: $navbar-tab-active-color
|
||||
padding-bottom: calc(0.5rem - #{$navbar-tab-active-border-bottom-width})
|
||||
|
||||
.navbar-content
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
|
||||
.navbar-link:not(.is-arrowless)
|
||||
+ltr-property("padding", 2.5em)
|
||||
&::after
|
||||
@extend %arrow
|
||||
border-color: $navbar-dropdown-arrow
|
||||
margin-top: -0.375em
|
||||
+ltr-position(1.125em)
|
||||
|
||||
.navbar-dropdown
|
||||
font-size: 0.875rem
|
||||
padding-bottom: 0.5rem
|
||||
padding-top: 0.5rem
|
||||
.navbar-item
|
||||
padding-left: 1.5rem
|
||||
padding-right: 1.5rem
|
||||
|
||||
.navbar-divider
|
||||
background-color: $navbar-divider-background-color
|
||||
border: none
|
||||
display: none
|
||||
height: $navbar-divider-height
|
||||
margin: 0.5rem 0
|
||||
|
||||
+until($navbar-breakpoint)
|
||||
.navbar > .container
|
||||
display: block
|
||||
.navbar-brand,
|
||||
.navbar-tabs
|
||||
.navbar-item
|
||||
align-items: center
|
||||
display: flex
|
||||
.navbar-link
|
||||
&::after
|
||||
display: none
|
||||
.navbar-menu
|
||||
background-color: $navbar-background-color
|
||||
box-shadow: 0 8px 16px bulmaRgba($scheme-invert, 0.1)
|
||||
padding: 0.5rem 0
|
||||
&.is-active
|
||||
display: block
|
||||
// Fixed navbar
|
||||
.navbar
|
||||
&.is-fixed-bottom-touch,
|
||||
&.is-fixed-top-touch
|
||||
+navbar-fixed
|
||||
&.is-fixed-bottom-touch
|
||||
bottom: 0
|
||||
&.has-shadow
|
||||
box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1)
|
||||
&.is-fixed-top-touch
|
||||
top: 0
|
||||
&.is-fixed-top,
|
||||
&.is-fixed-top-touch
|
||||
.navbar-menu
|
||||
+overflow-touch
|
||||
max-height: calc(100vh - #{$navbar-height})
|
||||
overflow: auto
|
||||
html,
|
||||
body
|
||||
&.has-navbar-fixed-top-touch
|
||||
padding-top: $navbar-height
|
||||
&.has-navbar-fixed-bottom-touch
|
||||
padding-bottom: $navbar-height
|
||||
|
||||
+from($navbar-breakpoint)
|
||||
.navbar,
|
||||
.navbar-menu,
|
||||
.navbar-start,
|
||||
.navbar-end
|
||||
align-items: stretch
|
||||
display: flex
|
||||
.navbar
|
||||
min-height: $navbar-height
|
||||
&.is-spaced
|
||||
padding: $navbar-padding-vertical $navbar-padding-horizontal
|
||||
.navbar-start,
|
||||
.navbar-end
|
||||
align-items: center
|
||||
a.navbar-item,
|
||||
.navbar-link
|
||||
border-radius: $radius
|
||||
&.is-transparent
|
||||
a.navbar-item,
|
||||
.navbar-link
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-active
|
||||
background-color: transparent !important
|
||||
.navbar-item.has-dropdown
|
||||
&.is-active,
|
||||
&.is-hoverable:focus,
|
||||
&.is-hoverable:focus-within,
|
||||
&.is-hoverable:hover
|
||||
.navbar-link
|
||||
background-color: transparent !important
|
||||
.navbar-dropdown
|
||||
a.navbar-item
|
||||
&:focus,
|
||||
&:hover
|
||||
background-color: $navbar-dropdown-item-hover-background-color
|
||||
color: $navbar-dropdown-item-hover-color
|
||||
&.is-active
|
||||
background-color: $navbar-dropdown-item-active-background-color
|
||||
color: $navbar-dropdown-item-active-color
|
||||
.navbar-burger
|
||||
display: none
|
||||
.navbar-item,
|
||||
.navbar-link
|
||||
align-items: center
|
||||
display: flex
|
||||
.navbar-item
|
||||
&.has-dropdown
|
||||
align-items: stretch
|
||||
&.has-dropdown-up
|
||||
.navbar-link::after
|
||||
transform: rotate(135deg) translate(0.25em, -0.25em)
|
||||
.navbar-dropdown
|
||||
border-bottom: $navbar-dropdown-border-top
|
||||
border-radius: $navbar-dropdown-radius $navbar-dropdown-radius 0 0
|
||||
border-top: none
|
||||
bottom: 100%
|
||||
box-shadow: 0 -8px 8px bulmaRgba($scheme-invert, 0.1)
|
||||
top: auto
|
||||
&.is-active,
|
||||
&.is-hoverable:focus,
|
||||
&.is-hoverable:focus-within,
|
||||
&.is-hoverable:hover
|
||||
.navbar-dropdown
|
||||
display: block
|
||||
.navbar.is-spaced &,
|
||||
&.is-boxed
|
||||
opacity: 1
|
||||
pointer-events: auto
|
||||
transform: translateY(0)
|
||||
.navbar-menu
|
||||
flex-grow: 1
|
||||
flex-shrink: 0
|
||||
.navbar-start
|
||||
justify-content: flex-start
|
||||
+ltr-property("margin", auto)
|
||||
.navbar-end
|
||||
justify-content: flex-end
|
||||
+ltr-property("margin", auto, false)
|
||||
.navbar-dropdown
|
||||
background-color: $navbar-dropdown-background-color
|
||||
border-bottom-left-radius: $navbar-dropdown-radius
|
||||
border-bottom-right-radius: $navbar-dropdown-radius
|
||||
border-top: $navbar-dropdown-border-top
|
||||
box-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1)
|
||||
display: none
|
||||
font-size: 0.875rem
|
||||
+ltr-position(0, false)
|
||||
min-width: 100%
|
||||
position: absolute
|
||||
top: 100%
|
||||
z-index: $navbar-dropdown-z
|
||||
.navbar-item
|
||||
padding: 0.375rem 1rem
|
||||
white-space: nowrap
|
||||
a.navbar-item
|
||||
+ltr-property("padding", 3rem)
|
||||
&:focus,
|
||||
&:hover
|
||||
background-color: $navbar-dropdown-item-hover-background-color
|
||||
color: $navbar-dropdown-item-hover-color
|
||||
&.is-active
|
||||
background-color: $navbar-dropdown-item-active-background-color
|
||||
color: $navbar-dropdown-item-active-color
|
||||
.navbar.is-spaced &,
|
||||
&.is-boxed
|
||||
border-radius: $navbar-dropdown-boxed-radius
|
||||
border-top: none
|
||||
box-shadow: $navbar-dropdown-boxed-shadow
|
||||
display: block
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
top: calc(100% + (#{$navbar-dropdown-offset}))
|
||||
transform: translateY(-5px)
|
||||
transition-duration: $speed
|
||||
transition-property: opacity, transform
|
||||
&.is-right
|
||||
left: auto
|
||||
right: 0
|
||||
.navbar-divider
|
||||
display: block
|
||||
.navbar > .container,
|
||||
.container > .navbar
|
||||
.navbar-brand
|
||||
+ltr-property("margin", -.75rem, false)
|
||||
.navbar-menu
|
||||
+ltr-property("margin", -.75rem)
|
||||
// Fixed navbar
|
||||
.navbar
|
||||
&.is-fixed-bottom-desktop,
|
||||
&.is-fixed-top-desktop
|
||||
+navbar-fixed
|
||||
&.is-fixed-bottom-desktop
|
||||
bottom: 0
|
||||
&.has-shadow
|
||||
box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1)
|
||||
&.is-fixed-top-desktop
|
||||
top: 0
|
||||
html,
|
||||
body
|
||||
&.has-navbar-fixed-top-desktop
|
||||
padding-top: $navbar-height
|
||||
&.has-navbar-fixed-bottom-desktop
|
||||
padding-bottom: $navbar-height
|
||||
&.has-spaced-navbar-fixed-top
|
||||
padding-top: $navbar-height + ($navbar-padding-vertical * 2)
|
||||
&.has-spaced-navbar-fixed-bottom
|
||||
padding-bottom: $navbar-height + ($navbar-padding-vertical * 2)
|
||||
// Hover/Active states
|
||||
a.navbar-item,
|
||||
.navbar-link
|
||||
&.is-active
|
||||
color: $navbar-item-active-color
|
||||
&.is-active:not(:focus):not(:hover)
|
||||
background-color: $navbar-item-active-background-color
|
||||
.navbar-item.has-dropdown
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-active
|
||||
.navbar-link
|
||||
background-color: $navbar-item-hover-background-color
|
||||
|
||||
// Combination
|
||||
|
||||
.hero
|
||||
&.is-fullheight-with-navbar
|
||||
min-height: calc(100vh - #{$navbar-height})
|
150
static/css/sass/bulma/components/pagination.sass
Normal file
150
static/css/sass/bulma/components/pagination.sass
Normal file
@ -0,0 +1,150 @@
|
||||
$pagination-color: $text-strong !default
|
||||
$pagination-border-color: $border !default
|
||||
$pagination-margin: -0.25rem !default
|
||||
$pagination-min-width: $control-height !default
|
||||
|
||||
$pagination-item-font-size: 1em !default
|
||||
$pagination-item-margin: 0.25rem !default
|
||||
$pagination-item-padding-left: 0.5em !default
|
||||
$pagination-item-padding-right: 0.5em !default
|
||||
|
||||
$pagination-hover-color: $link-hover !default
|
||||
$pagination-hover-border-color: $link-hover-border !default
|
||||
|
||||
$pagination-focus-color: $link-focus !default
|
||||
$pagination-focus-border-color: $link-focus-border !default
|
||||
|
||||
$pagination-active-color: $link-active !default
|
||||
$pagination-active-border-color: $link-active-border !default
|
||||
|
||||
$pagination-disabled-color: $text-light !default
|
||||
$pagination-disabled-background-color: $border !default
|
||||
$pagination-disabled-border-color: $border !default
|
||||
|
||||
$pagination-current-color: $link-invert !default
|
||||
$pagination-current-background-color: $link !default
|
||||
$pagination-current-border-color: $link !default
|
||||
|
||||
$pagination-ellipsis-color: $grey-light !default
|
||||
|
||||
$pagination-shadow-inset: inset 0 1px 2px rgba($scheme-invert, 0.2)
|
||||
|
||||
.pagination
|
||||
@extend %block
|
||||
font-size: $size-normal
|
||||
margin: $pagination-margin
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
||||
&.is-rounded
|
||||
.pagination-previous,
|
||||
.pagination-next
|
||||
padding-left: 1em
|
||||
padding-right: 1em
|
||||
border-radius: $radius-rounded
|
||||
.pagination-link
|
||||
border-radius: $radius-rounded
|
||||
|
||||
.pagination,
|
||||
.pagination-list
|
||||
align-items: center
|
||||
display: flex
|
||||
justify-content: center
|
||||
text-align: center
|
||||
|
||||
.pagination-previous,
|
||||
.pagination-next,
|
||||
.pagination-link,
|
||||
.pagination-ellipsis
|
||||
@extend %control
|
||||
@extend %unselectable
|
||||
font-size: $pagination-item-font-size
|
||||
justify-content: center
|
||||
margin: $pagination-item-margin
|
||||
padding-left: $pagination-item-padding-left
|
||||
padding-right: $pagination-item-padding-right
|
||||
text-align: center
|
||||
|
||||
.pagination-previous,
|
||||
.pagination-next,
|
||||
.pagination-link
|
||||
border-color: $pagination-border-color
|
||||
color: $pagination-color
|
||||
min-width: $pagination-min-width
|
||||
&:hover
|
||||
border-color: $pagination-hover-border-color
|
||||
color: $pagination-hover-color
|
||||
&:focus
|
||||
border-color: $pagination-focus-border-color
|
||||
&:active
|
||||
box-shadow: $pagination-shadow-inset
|
||||
&[disabled]
|
||||
background-color: $pagination-disabled-background-color
|
||||
border-color: $pagination-disabled-border-color
|
||||
box-shadow: none
|
||||
color: $pagination-disabled-color
|
||||
opacity: 0.5
|
||||
|
||||
.pagination-previous,
|
||||
.pagination-next
|
||||
padding-left: 0.75em
|
||||
padding-right: 0.75em
|
||||
white-space: nowrap
|
||||
|
||||
.pagination-link
|
||||
&.is-current
|
||||
background-color: $pagination-current-background-color
|
||||
border-color: $pagination-current-border-color
|
||||
color: $pagination-current-color
|
||||
|
||||
.pagination-ellipsis
|
||||
color: $pagination-ellipsis-color
|
||||
pointer-events: none
|
||||
|
||||
.pagination-list
|
||||
flex-wrap: wrap
|
||||
|
||||
+mobile
|
||||
.pagination
|
||||
flex-wrap: wrap
|
||||
.pagination-previous,
|
||||
.pagination-next
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
.pagination-list
|
||||
li
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
|
||||
+tablet
|
||||
.pagination-list
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
justify-content: flex-start
|
||||
order: 1
|
||||
.pagination-previous
|
||||
order: 2
|
||||
.pagination-next
|
||||
order: 3
|
||||
.pagination
|
||||
justify-content: space-between
|
||||
&.is-centered
|
||||
.pagination-previous
|
||||
order: 1
|
||||
.pagination-list
|
||||
justify-content: center
|
||||
order: 2
|
||||
.pagination-next
|
||||
order: 3
|
||||
&.is-right
|
||||
.pagination-previous
|
||||
order: 1
|
||||
.pagination-next
|
||||
order: 2
|
||||
.pagination-list
|
||||
justify-content: flex-end
|
||||
order: 3
|
119
static/css/sass/bulma/components/panel.sass
Normal file
119
static/css/sass/bulma/components/panel.sass
Normal file
@ -0,0 +1,119 @@
|
||||
$panel-margin: $block-spacing !default
|
||||
$panel-item-border: 1px solid $border-light !default
|
||||
$panel-radius: $radius-large !default
|
||||
$panel-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default
|
||||
|
||||
$panel-heading-background-color: $border-light !default
|
||||
$panel-heading-color: $text-strong !default
|
||||
$panel-heading-line-height: 1.25 !default
|
||||
$panel-heading-padding: 0.75em 1em !default
|
||||
$panel-heading-radius: $radius !default
|
||||
$panel-heading-size: 1.25em !default
|
||||
$panel-heading-weight: $weight-bold !default
|
||||
|
||||
$panel-tabs-font-size: 0.875em !default
|
||||
$panel-tab-border-bottom: 1px solid $border !default
|
||||
$panel-tab-active-border-bottom-color: $link-active-border !default
|
||||
$panel-tab-active-color: $link-active !default
|
||||
|
||||
$panel-list-item-color: $text !default
|
||||
$panel-list-item-hover-color: $link !default
|
||||
|
||||
$panel-block-color: $text-strong !default
|
||||
$panel-block-hover-background-color: $background !default
|
||||
$panel-block-active-border-left-color: $link !default
|
||||
$panel-block-active-color: $link-active !default
|
||||
$panel-block-active-icon-color: $link !default
|
||||
|
||||
$panel-icon-color: $text-light !default
|
||||
$panel-colors: $colors !default
|
||||
|
||||
.panel
|
||||
border-radius: $panel-radius
|
||||
box-shadow: $panel-shadow
|
||||
font-size: $size-normal
|
||||
&:not(:last-child)
|
||||
margin-bottom: $panel-margin
|
||||
// Colors
|
||||
@each $name, $components in $panel-colors
|
||||
$color: nth($components, 1)
|
||||
$color-invert: nth($components, 2)
|
||||
&.is-#{$name}
|
||||
.panel-heading
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
.panel-tabs a.is-active
|
||||
border-bottom-color: $color
|
||||
.panel-block.is-active .panel-icon
|
||||
color: $color
|
||||
|
||||
.panel-tabs,
|
||||
.panel-block
|
||||
&:not(:last-child)
|
||||
border-bottom: $panel-item-border
|
||||
|
||||
.panel-heading
|
||||
background-color: $panel-heading-background-color
|
||||
border-radius: $panel-radius $panel-radius 0 0
|
||||
color: $panel-heading-color
|
||||
font-size: $panel-heading-size
|
||||
font-weight: $panel-heading-weight
|
||||
line-height: $panel-heading-line-height
|
||||
padding: $panel-heading-padding
|
||||
|
||||
.panel-tabs
|
||||
align-items: flex-end
|
||||
display: flex
|
||||
font-size: $panel-tabs-font-size
|
||||
justify-content: center
|
||||
a
|
||||
border-bottom: $panel-tab-border-bottom
|
||||
margin-bottom: -1px
|
||||
padding: 0.5em
|
||||
// Modifiers
|
||||
&.is-active
|
||||
border-bottom-color: $panel-tab-active-border-bottom-color
|
||||
color: $panel-tab-active-color
|
||||
|
||||
.panel-list
|
||||
a
|
||||
color: $panel-list-item-color
|
||||
&:hover
|
||||
color: $panel-list-item-hover-color
|
||||
|
||||
.panel-block
|
||||
align-items: center
|
||||
color: $panel-block-color
|
||||
display: flex
|
||||
justify-content: flex-start
|
||||
padding: 0.5em 0.75em
|
||||
input[type="checkbox"]
|
||||
+ltr-property("margin", 0.75em)
|
||||
& > .control
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
width: 100%
|
||||
&.is-wrapped
|
||||
flex-wrap: wrap
|
||||
&.is-active
|
||||
border-left-color: $panel-block-active-border-left-color
|
||||
color: $panel-block-active-color
|
||||
.panel-icon
|
||||
color: $panel-block-active-icon-color
|
||||
&:last-child
|
||||
border-bottom-left-radius: $panel-radius
|
||||
border-bottom-right-radius: $panel-radius
|
||||
|
||||
a.panel-block,
|
||||
label.panel-block
|
||||
cursor: pointer
|
||||
&:hover
|
||||
background-color: $panel-block-hover-background-color
|
||||
|
||||
.panel-icon
|
||||
+fa(14px, 1em)
|
||||
color: $panel-icon-color
|
||||
+ltr-property("margin", 0.75em)
|
||||
.fa
|
||||
font-size: inherit
|
||||
line-height: inherit
|
174
static/css/sass/bulma/components/tabs.sass
Normal file
174
static/css/sass/bulma/components/tabs.sass
Normal file
@ -0,0 +1,174 @@
|
||||
$tabs-border-bottom-color: $border !default
|
||||
$tabs-border-bottom-style: solid !default
|
||||
$tabs-border-bottom-width: 1px !default
|
||||
$tabs-link-color: $text !default
|
||||
$tabs-link-hover-border-bottom-color: $text-strong !default
|
||||
$tabs-link-hover-color: $text-strong !default
|
||||
$tabs-link-active-border-bottom-color: $link !default
|
||||
$tabs-link-active-color: $link !default
|
||||
$tabs-link-padding: 0.5em 1em !default
|
||||
|
||||
$tabs-boxed-link-radius: $radius !default
|
||||
$tabs-boxed-link-hover-background-color: $background !default
|
||||
$tabs-boxed-link-hover-border-bottom-color: $border !default
|
||||
|
||||
$tabs-boxed-link-active-background-color: $scheme-main !default
|
||||
$tabs-boxed-link-active-border-color: $border !default
|
||||
$tabs-boxed-link-active-border-bottom-color: transparent !default
|
||||
|
||||
$tabs-toggle-link-border-color: $border !default
|
||||
$tabs-toggle-link-border-style: solid !default
|
||||
$tabs-toggle-link-border-width: 1px !default
|
||||
$tabs-toggle-link-hover-background-color: $background !default
|
||||
$tabs-toggle-link-hover-border-color: $border-hover !default
|
||||
$tabs-toggle-link-radius: $radius !default
|
||||
$tabs-toggle-link-active-background-color: $link !default
|
||||
$tabs-toggle-link-active-border-color: $link !default
|
||||
$tabs-toggle-link-active-color: $link-invert !default
|
||||
|
||||
.tabs
|
||||
@extend %block
|
||||
+overflow-touch
|
||||
@extend %unselectable
|
||||
align-items: stretch
|
||||
display: flex
|
||||
font-size: $size-normal
|
||||
justify-content: space-between
|
||||
overflow: hidden
|
||||
overflow-x: auto
|
||||
white-space: nowrap
|
||||
a
|
||||
align-items: center
|
||||
border-bottom-color: $tabs-border-bottom-color
|
||||
border-bottom-style: $tabs-border-bottom-style
|
||||
border-bottom-width: $tabs-border-bottom-width
|
||||
color: $tabs-link-color
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-bottom: -#{$tabs-border-bottom-width}
|
||||
padding: $tabs-link-padding
|
||||
vertical-align: top
|
||||
&:hover
|
||||
border-bottom-color: $tabs-link-hover-border-bottom-color
|
||||
color: $tabs-link-hover-color
|
||||
li
|
||||
display: block
|
||||
&.is-active
|
||||
a
|
||||
border-bottom-color: $tabs-link-active-border-bottom-color
|
||||
color: $tabs-link-active-color
|
||||
ul
|
||||
align-items: center
|
||||
border-bottom-color: $tabs-border-bottom-color
|
||||
border-bottom-style: $tabs-border-bottom-style
|
||||
border-bottom-width: $tabs-border-bottom-width
|
||||
display: flex
|
||||
flex-grow: 1
|
||||
flex-shrink: 0
|
||||
justify-content: flex-start
|
||||
&.is-left
|
||||
padding-right: 0.75em
|
||||
&.is-center
|
||||
flex: none
|
||||
justify-content: center
|
||||
padding-left: 0.75em
|
||||
padding-right: 0.75em
|
||||
&.is-right
|
||||
justify-content: flex-end
|
||||
padding-left: 0.75em
|
||||
.icon
|
||||
&:first-child
|
||||
+ltr-property("margin", 0.5em)
|
||||
&:last-child
|
||||
+ltr-property("margin", 0.5em, false)
|
||||
// Alignment
|
||||
&.is-centered
|
||||
ul
|
||||
justify-content: center
|
||||
&.is-right
|
||||
ul
|
||||
justify-content: flex-end
|
||||
// Styles
|
||||
&.is-boxed
|
||||
a
|
||||
border: 1px solid transparent
|
||||
+ltr
|
||||
border-radius: $tabs-boxed-link-radius $tabs-boxed-link-radius 0 0
|
||||
+rtl
|
||||
border-radius: 0 0 $tabs-boxed-link-radius $tabs-boxed-link-radius
|
||||
&:hover
|
||||
background-color: $tabs-boxed-link-hover-background-color
|
||||
border-bottom-color: $tabs-boxed-link-hover-border-bottom-color
|
||||
li
|
||||
&.is-active
|
||||
a
|
||||
background-color: $tabs-boxed-link-active-background-color
|
||||
border-color: $tabs-boxed-link-active-border-color
|
||||
border-bottom-color: $tabs-boxed-link-active-border-bottom-color !important
|
||||
&.is-fullwidth
|
||||
li
|
||||
flex-grow: 1
|
||||
flex-shrink: 0
|
||||
&.is-toggle
|
||||
a
|
||||
border-color: $tabs-toggle-link-border-color
|
||||
border-style: $tabs-toggle-link-border-style
|
||||
border-width: $tabs-toggle-link-border-width
|
||||
margin-bottom: 0
|
||||
position: relative
|
||||
&:hover
|
||||
background-color: $tabs-toggle-link-hover-background-color
|
||||
border-color: $tabs-toggle-link-hover-border-color
|
||||
z-index: 2
|
||||
li
|
||||
& + li
|
||||
+ltr-property("margin", -#{$tabs-toggle-link-border-width}, false)
|
||||
&:first-child a
|
||||
+ltr
|
||||
border-top-left-radius: $tabs-toggle-link-radius
|
||||
border-bottom-left-radius: $tabs-toggle-link-radius
|
||||
+rtl
|
||||
border-top-right-radius: $tabs-toggle-link-radius
|
||||
border-bottom-right-radius: $tabs-toggle-link-radius
|
||||
&:last-child a
|
||||
+ltr
|
||||
border-top-right-radius: $tabs-toggle-link-radius
|
||||
border-bottom-right-radius: $tabs-toggle-link-radius
|
||||
+rtl
|
||||
border-top-left-radius: $tabs-toggle-link-radius
|
||||
border-bottom-left-radius: $tabs-toggle-link-radius
|
||||
&.is-active
|
||||
a
|
||||
background-color: $tabs-toggle-link-active-background-color
|
||||
border-color: $tabs-toggle-link-active-border-color
|
||||
color: $tabs-toggle-link-active-color
|
||||
z-index: 1
|
||||
ul
|
||||
border-bottom: none
|
||||
&.is-toggle-rounded
|
||||
li
|
||||
&:first-child a
|
||||
+ltr
|
||||
border-bottom-left-radius: $radius-rounded
|
||||
border-top-left-radius: $radius-rounded
|
||||
padding-left: 1.25em
|
||||
+rtl
|
||||
border-bottom-right-radius: $radius-rounded
|
||||
border-top-right-radius: $radius-rounded
|
||||
padding-right: 1.25em
|
||||
&:last-child a
|
||||
+ltr
|
||||
border-bottom-right-radius: $radius-rounded
|
||||
border-top-right-radius: $radius-rounded
|
||||
padding-right: 1.25em
|
||||
+rtl
|
||||
border-bottom-left-radius: $radius-rounded
|
||||
border-top-left-radius: $radius-rounded
|
||||
padding-left: 1.25em
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
16
static/css/sass/bulma/elements/_all.sass
Normal file
16
static/css/sass/bulma/elements/_all.sass
Normal file
@ -0,0 +1,16 @@
|
||||
/* Bulma Elements */
|
||||
@charset "utf-8"
|
||||
|
||||
@import "box.sass"
|
||||
@import "button.sass"
|
||||
@import "container.sass"
|
||||
@import "content.sass"
|
||||
@import "icon.sass"
|
||||
@import "image.sass"
|
||||
@import "notification.sass"
|
||||
@import "progress.sass"
|
||||
@import "table.sass"
|
||||
@import "tag.sass"
|
||||
@import "title.sass"
|
||||
|
||||
@import "other.sass"
|
24
static/css/sass/bulma/elements/box.sass
Normal file
24
static/css/sass/bulma/elements/box.sass
Normal file
@ -0,0 +1,24 @@
|
||||
$box-color: $text !default
|
||||
$box-background-color: $scheme-main !default
|
||||
$box-radius: $radius-large !default
|
||||
$box-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default
|
||||
$box-padding: 1.25rem !default
|
||||
|
||||
$box-link-hover-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0 0 1px $link !default
|
||||
$box-link-active-shadow: inset 0 1px 2px rgba($scheme-invert, 0.2), 0 0 0 1px $link !default
|
||||
|
||||
.box
|
||||
@extend %block
|
||||
background-color: $box-background-color
|
||||
border-radius: $box-radius
|
||||
box-shadow: $box-shadow
|
||||
color: $box-color
|
||||
display: block
|
||||
padding: $box-padding
|
||||
|
||||
a.box
|
||||
&:hover,
|
||||
&:focus
|
||||
box-shadow: $box-link-hover-shadow
|
||||
&:active
|
||||
box-shadow: $box-link-active-shadow
|
325
static/css/sass/bulma/elements/button.sass
Normal file
325
static/css/sass/bulma/elements/button.sass
Normal file
@ -0,0 +1,325 @@
|
||||
$button-color: $text-strong !default
|
||||
$button-background-color: $scheme-main !default
|
||||
$button-family: false !default
|
||||
|
||||
$button-border-color: $border !default
|
||||
$button-border-width: $control-border-width !default
|
||||
|
||||
$button-padding-vertical: calc(0.5em - #{$button-border-width}) !default
|
||||
$button-padding-horizontal: 1em !default
|
||||
|
||||
$button-hover-color: $link-hover !default
|
||||
$button-hover-border-color: $link-hover-border !default
|
||||
|
||||
$button-focus-color: $link-focus !default
|
||||
$button-focus-border-color: $link-focus-border !default
|
||||
$button-focus-box-shadow-size: 0 0 0 0.125em !default
|
||||
$button-focus-box-shadow-color: bulmaRgba($link, 0.25) !default
|
||||
|
||||
$button-active-color: $link-active !default
|
||||
$button-active-border-color: $link-active-border !default
|
||||
|
||||
$button-text-color: $text !default
|
||||
$button-text-decoration: underline !default
|
||||
$button-text-hover-background-color: $background !default
|
||||
$button-text-hover-color: $text-strong !default
|
||||
|
||||
$button-disabled-background-color: $scheme-main !default
|
||||
$button-disabled-border-color: $border !default
|
||||
$button-disabled-shadow: none !default
|
||||
$button-disabled-opacity: 0.5 !default
|
||||
|
||||
$button-static-color: $text-light !default
|
||||
$button-static-background-color: $scheme-main-ter !default
|
||||
$button-static-border-color: $border !default
|
||||
|
||||
$button-colors: $colors !default
|
||||
|
||||
// The button sizes use mixins so they can be used at different breakpoints
|
||||
=button-small
|
||||
border-radius: $radius-small
|
||||
font-size: $size-small
|
||||
=button-normal
|
||||
font-size: $size-normal
|
||||
=button-medium
|
||||
font-size: $size-medium
|
||||
=button-large
|
||||
font-size: $size-large
|
||||
|
||||
.button
|
||||
@extend %control
|
||||
@extend %unselectable
|
||||
background-color: $button-background-color
|
||||
border-color: $button-border-color
|
||||
border-width: $button-border-width
|
||||
color: $button-color
|
||||
cursor: pointer
|
||||
@if $button-family
|
||||
font-family: $button-family
|
||||
justify-content: center
|
||||
padding-bottom: $button-padding-vertical
|
||||
padding-left: $button-padding-horizontal
|
||||
padding-right: $button-padding-horizontal
|
||||
padding-top: $button-padding-vertical
|
||||
text-align: center
|
||||
white-space: nowrap
|
||||
strong
|
||||
color: inherit
|
||||
.icon
|
||||
&,
|
||||
&.is-small,
|
||||
&.is-medium,
|
||||
&.is-large
|
||||
height: 1.5em
|
||||
width: 1.5em
|
||||
&:first-child:not(:last-child)
|
||||
+ltr-property("margin", calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}), false)
|
||||
+ltr-property("margin", $button-padding-horizontal / 4)
|
||||
&:last-child:not(:first-child)
|
||||
+ltr-property("margin", $button-padding-horizontal / 4, false)
|
||||
+ltr-property("margin", calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}))
|
||||
&:first-child:last-child
|
||||
margin-left: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width})
|
||||
margin-right: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width})
|
||||
// States
|
||||
&:hover,
|
||||
&.is-hovered
|
||||
border-color: $button-hover-border-color
|
||||
color: $button-hover-color
|
||||
&:focus,
|
||||
&.is-focused
|
||||
border-color: $button-focus-border-color
|
||||
color: $button-focus-color
|
||||
&:not(:active)
|
||||
box-shadow: $button-focus-box-shadow-size $button-focus-box-shadow-color
|
||||
&:active,
|
||||
&.is-active
|
||||
border-color: $button-active-border-color
|
||||
color: $button-active-color
|
||||
// Colors
|
||||
&.is-text
|
||||
background-color: transparent
|
||||
border-color: transparent
|
||||
color: $button-text-color
|
||||
text-decoration: $button-text-decoration
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused
|
||||
background-color: $button-text-hover-background-color
|
||||
color: $button-text-hover-color
|
||||
&:active,
|
||||
&.is-active
|
||||
background-color: bulmaDarken($button-text-hover-background-color, 5%)
|
||||
color: $button-text-hover-color
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: transparent
|
||||
border-color: transparent
|
||||
box-shadow: none
|
||||
@each $name, $pair in $button-colors
|
||||
$color: nth($pair, 1)
|
||||
$color-invert: nth($pair, 2)
|
||||
&.is-#{$name}
|
||||
background-color: $color
|
||||
border-color: transparent
|
||||
color: $color-invert
|
||||
&:hover,
|
||||
&.is-hovered
|
||||
background-color: bulmaDarken($color, 2.5%)
|
||||
border-color: transparent
|
||||
color: $color-invert
|
||||
&:focus,
|
||||
&.is-focused
|
||||
border-color: transparent
|
||||
color: $color-invert
|
||||
&:not(:active)
|
||||
box-shadow: $button-focus-box-shadow-size bulmaRgba($color, 0.25)
|
||||
&:active,
|
||||
&.is-active
|
||||
background-color: bulmaDarken($color, 5%)
|
||||
border-color: transparent
|
||||
color: $color-invert
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: $color
|
||||
border-color: transparent
|
||||
box-shadow: none
|
||||
&.is-inverted
|
||||
background-color: $color-invert
|
||||
color: $color
|
||||
&:hover,
|
||||
&.is-hovered
|
||||
background-color: bulmaDarken($color-invert, 5%)
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: $color-invert
|
||||
border-color: transparent
|
||||
box-shadow: none
|
||||
color: $color
|
||||
&.is-loading
|
||||
&::after
|
||||
border-color: transparent transparent $color-invert $color-invert !important
|
||||
&.is-outlined
|
||||
background-color: transparent
|
||||
border-color: $color
|
||||
color: $color
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused
|
||||
background-color: $color
|
||||
border-color: $color
|
||||
color: $color-invert
|
||||
&.is-loading
|
||||
&::after
|
||||
border-color: transparent transparent $color $color !important
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused
|
||||
&::after
|
||||
border-color: transparent transparent $color-invert $color-invert !important
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: transparent
|
||||
border-color: $color
|
||||
box-shadow: none
|
||||
color: $color
|
||||
&.is-inverted.is-outlined
|
||||
background-color: transparent
|
||||
border-color: $color-invert
|
||||
color: $color-invert
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused
|
||||
background-color: $color-invert
|
||||
color: $color
|
||||
&.is-loading
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused
|
||||
&::after
|
||||
border-color: transparent transparent $color $color !important
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: transparent
|
||||
border-color: $color-invert
|
||||
box-shadow: none
|
||||
color: $color-invert
|
||||
// If light and dark colors are provided
|
||||
@if length($pair) >= 4
|
||||
$color-light: nth($pair, 3)
|
||||
$color-dark: nth($pair, 4)
|
||||
&.is-light
|
||||
background-color: $color-light
|
||||
color: $color-dark
|
||||
&:hover,
|
||||
&.is-hovered
|
||||
background-color: bulmaDarken($color-light, 2.5%)
|
||||
border-color: transparent
|
||||
color: $color-dark
|
||||
&:active,
|
||||
&.is-active
|
||||
background-color: bulmaDarken($color-light, 5%)
|
||||
border-color: transparent
|
||||
color: $color-dark
|
||||
// Sizes
|
||||
&.is-small
|
||||
+button-small
|
||||
&.is-normal
|
||||
+button-normal
|
||||
&.is-medium
|
||||
+button-medium
|
||||
&.is-large
|
||||
+button-large
|
||||
// Modifiers
|
||||
&[disabled],
|
||||
fieldset[disabled] &
|
||||
background-color: $button-disabled-background-color
|
||||
border-color: $button-disabled-border-color
|
||||
box-shadow: $button-disabled-shadow
|
||||
opacity: $button-disabled-opacity
|
||||
&.is-fullwidth
|
||||
display: flex
|
||||
width: 100%
|
||||
&.is-loading
|
||||
color: transparent !important
|
||||
pointer-events: none
|
||||
&::after
|
||||
@extend %loader
|
||||
+center(1em)
|
||||
position: absolute !important
|
||||
&.is-static
|
||||
background-color: $button-static-background-color
|
||||
border-color: $button-static-border-color
|
||||
color: $button-static-color
|
||||
box-shadow: none
|
||||
pointer-events: none
|
||||
&.is-rounded
|
||||
border-radius: $radius-rounded
|
||||
padding-left: calc(#{$button-padding-horizontal} + 0.25em)
|
||||
padding-right: calc(#{$button-padding-horizontal} + 0.25em)
|
||||
|
||||
.buttons
|
||||
align-items: center
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: flex-start
|
||||
.button
|
||||
margin-bottom: 0.5rem
|
||||
&:not(:last-child):not(.is-fullwidth)
|
||||
+ltr-property("margin", 0.5rem)
|
||||
&:last-child
|
||||
margin-bottom: -0.5rem
|
||||
&:not(:last-child)
|
||||
margin-bottom: 1rem
|
||||
// Sizes
|
||||
&.are-small
|
||||
.button:not(.is-normal):not(.is-medium):not(.is-large)
|
||||
+button-small
|
||||
&.are-medium
|
||||
.button:not(.is-small):not(.is-normal):not(.is-large)
|
||||
+button-medium
|
||||
&.are-large
|
||||
.button:not(.is-small):not(.is-normal):not(.is-medium)
|
||||
+button-large
|
||||
&.has-addons
|
||||
.button
|
||||
&:not(:first-child)
|
||||
border-bottom-left-radius: 0
|
||||
border-top-left-radius: 0
|
||||
&:not(:last-child)
|
||||
border-bottom-right-radius: 0
|
||||
border-top-right-radius: 0
|
||||
+ltr-property("margin", -1px)
|
||||
&:last-child
|
||||
+ltr-property("margin", 0)
|
||||
&:hover,
|
||||
&.is-hovered
|
||||
z-index: 2
|
||||
&:focus,
|
||||
&.is-focused,
|
||||
&:active,
|
||||
&.is-active,
|
||||
&.is-selected
|
||||
z-index: 3
|
||||
&:hover
|
||||
z-index: 4
|
||||
&.is-expanded
|
||||
flex-grow: 1
|
||||
flex-shrink: 1
|
||||
&.is-centered
|
||||
justify-content: center
|
||||
&:not(.has-addons)
|
||||
.button:not(.is-fullwidth)
|
||||
margin-left: 0.25rem
|
||||
margin-right: 0.25rem
|
||||
&.is-right
|
||||
justify-content: flex-end
|
||||
&:not(.has-addons)
|
||||
.button:not(.is-fullwidth)
|
||||
margin-left: 0.25rem
|
||||
margin-right: 0.25rem
|
27
static/css/sass/bulma/elements/container.sass
Normal file
27
static/css/sass/bulma/elements/container.sass
Normal file
@ -0,0 +1,27 @@
|
||||
$container-offset: (2 * $gap) !default
|
||||
$container-max-width: $fullhd !default
|
||||
|
||||
.container
|
||||
flex-grow: 1
|
||||
margin: 0 auto
|
||||
position: relative
|
||||
width: auto
|
||||
&.is-fluid
|
||||
max-width: none !important
|
||||
padding-left: $gap
|
||||
padding-right: $gap
|
||||
width: 100%
|
||||
+desktop
|
||||
max-width: $desktop - $container-offset
|
||||
+until-widescreen
|
||||
&.is-widescreen:not(.is-max-desktop)
|
||||
max-width: min($widescreen, $container-max-width) - $container-offset
|
||||
+until-fullhd
|
||||
&.is-fullhd:not(.is-max-desktop):not(.is-max-widescreen)
|
||||
max-width: min($fullhd, $container-max-width) - $container-offset
|
||||
+widescreen
|
||||
&:not(.is-max-desktop)
|
||||
max-width: min($widescreen, $container-max-width) - $container-offset
|
||||
+fullhd
|
||||
&:not(.is-max-desktop):not(.is-max-widescreen)
|
||||
max-width: min($fullhd, $container-max-width) - $container-offset
|
155
static/css/sass/bulma/elements/content.sass
Normal file
155
static/css/sass/bulma/elements/content.sass
Normal file
@ -0,0 +1,155 @@
|
||||
$content-heading-color: $text-strong !default
|
||||
$content-heading-weight: $weight-semibold !default
|
||||
$content-heading-line-height: 1.125 !default
|
||||
|
||||
$content-blockquote-background-color: $background !default
|
||||
$content-blockquote-border-left: 5px solid $border !default
|
||||
$content-blockquote-padding: 1.25em 1.5em !default
|
||||
|
||||
$content-pre-padding: 1.25em 1.5em !default
|
||||
|
||||
$content-table-cell-border: 1px solid $border !default
|
||||
$content-table-cell-border-width: 0 0 1px !default
|
||||
$content-table-cell-padding: 0.5em 0.75em !default
|
||||
$content-table-cell-heading-color: $text-strong !default
|
||||
$content-table-head-cell-border-width: 0 0 2px !default
|
||||
$content-table-head-cell-color: $text-strong !default
|
||||
$content-table-foot-cell-border-width: 2px 0 0 !default
|
||||
$content-table-foot-cell-color: $text-strong !default
|
||||
|
||||
.content
|
||||
@extend %block
|
||||
// Inline
|
||||
li + li
|
||||
margin-top: 0.25em
|
||||
// Block
|
||||
p,
|
||||
dl,
|
||||
ol,
|
||||
ul,
|
||||
blockquote,
|
||||
pre,
|
||||
table
|
||||
&:not(:last-child)
|
||||
margin-bottom: 1em
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6
|
||||
color: $content-heading-color
|
||||
font-weight: $content-heading-weight
|
||||
line-height: $content-heading-line-height
|
||||
h1
|
||||
font-size: 2em
|
||||
margin-bottom: 0.5em
|
||||
&:not(:first-child)
|
||||
margin-top: 1em
|
||||
h2
|
||||
font-size: 1.75em
|
||||
margin-bottom: 0.5714em
|
||||
&:not(:first-child)
|
||||
margin-top: 1.1428em
|
||||
h3
|
||||
font-size: 1.5em
|
||||
margin-bottom: 0.6666em
|
||||
&:not(:first-child)
|
||||
margin-top: 1.3333em
|
||||
h4
|
||||
font-size: 1.25em
|
||||
margin-bottom: 0.8em
|
||||
h5
|
||||
font-size: 1.125em
|
||||
margin-bottom: 0.8888em
|
||||
h6
|
||||
font-size: 1em
|
||||
margin-bottom: 1em
|
||||
blockquote
|
||||
background-color: $content-blockquote-background-color
|
||||
+ltr-property("border", $content-blockquote-border-left, false)
|
||||
padding: $content-blockquote-padding
|
||||
ol
|
||||
list-style-position: outside
|
||||
+ltr-property("margin", 2em, false)
|
||||
margin-top: 1em
|
||||
&:not([type])
|
||||
list-style-type: decimal
|
||||
&.is-lower-alpha
|
||||
list-style-type: lower-alpha
|
||||
&.is-lower-roman
|
||||
list-style-type: lower-roman
|
||||
&.is-upper-alpha
|
||||
list-style-type: upper-alpha
|
||||
&.is-upper-roman
|
||||
list-style-type: upper-roman
|
||||
ul
|
||||
list-style: disc outside
|
||||
+ltr-property("margin", 2em, false)
|
||||
margin-top: 1em
|
||||
ul
|
||||
list-style-type: circle
|
||||
margin-top: 0.5em
|
||||
ul
|
||||
list-style-type: square
|
||||
dd
|
||||
+ltr-property("margin", 2em, false)
|
||||
figure
|
||||
margin-left: 2em
|
||||
margin-right: 2em
|
||||
text-align: center
|
||||
&:not(:first-child)
|
||||
margin-top: 2em
|
||||
&:not(:last-child)
|
||||
margin-bottom: 2em
|
||||
img
|
||||
display: inline-block
|
||||
figcaption
|
||||
font-style: italic
|
||||
pre
|
||||
+overflow-touch
|
||||
overflow-x: auto
|
||||
padding: $content-pre-padding
|
||||
white-space: pre
|
||||
word-wrap: normal
|
||||
sup,
|
||||
sub
|
||||
font-size: 75%
|
||||
table
|
||||
width: 100%
|
||||
td,
|
||||
th
|
||||
border: $content-table-cell-border
|
||||
border-width: $content-table-cell-border-width
|
||||
padding: $content-table-cell-padding
|
||||
vertical-align: top
|
||||
th
|
||||
color: $content-table-cell-heading-color
|
||||
&:not([align])
|
||||
text-align: inherit
|
||||
thead
|
||||
td,
|
||||
th
|
||||
border-width: $content-table-head-cell-border-width
|
||||
color: $content-table-head-cell-color
|
||||
tfoot
|
||||
td,
|
||||
th
|
||||
border-width: $content-table-foot-cell-border-width
|
||||
color: $content-table-foot-cell-color
|
||||
tbody
|
||||
tr
|
||||
&:last-child
|
||||
td,
|
||||
th
|
||||
border-bottom-width: 0
|
||||
.tabs
|
||||
li + li
|
||||
margin-top: 0
|
||||
// Sizes
|
||||
&.is-small
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-medium
|
||||
&.is-large
|
||||
font-size: $size-large
|
1
static/css/sass/bulma/elements/form.sass
Normal file
1
static/css/sass/bulma/elements/form.sass
Normal file
@ -0,0 +1 @@
|
||||
@warn "The form.sass file is DEPRECATED. It has moved into its own /form folder. Please import sass/form/_all instead."
|
21
static/css/sass/bulma/elements/icon.sass
Normal file
21
static/css/sass/bulma/elements/icon.sass
Normal file
@ -0,0 +1,21 @@
|
||||
$icon-dimensions: 1.5rem !default
|
||||
$icon-dimensions-small: 1rem !default
|
||||
$icon-dimensions-medium: 2rem !default
|
||||
$icon-dimensions-large: 3rem !default
|
||||
|
||||
.icon
|
||||
align-items: center
|
||||
display: inline-flex
|
||||
justify-content: center
|
||||
height: $icon-dimensions
|
||||
width: $icon-dimensions
|
||||
// Sizes
|
||||
&.is-small
|
||||
height: $icon-dimensions-small
|
||||
width: $icon-dimensions-small
|
||||
&.is-medium
|
||||
height: $icon-dimensions-medium
|
||||
width: $icon-dimensions-medium
|
||||
&.is-large
|
||||
height: $icon-dimensions-large
|
||||
width: $icon-dimensions-large
|
71
static/css/sass/bulma/elements/image.sass
Normal file
71
static/css/sass/bulma/elements/image.sass
Normal file
@ -0,0 +1,71 @@
|
||||
$dimensions: 16 24 32 48 64 96 128 !default
|
||||
|
||||
.image
|
||||
display: block
|
||||
position: relative
|
||||
img
|
||||
display: block
|
||||
height: auto
|
||||
width: 100%
|
||||
&.is-rounded
|
||||
border-radius: $radius-rounded
|
||||
&.is-fullwidth
|
||||
width: 100%
|
||||
// Ratio
|
||||
&.is-square,
|
||||
&.is-1by1,
|
||||
&.is-5by4,
|
||||
&.is-4by3,
|
||||
&.is-3by2,
|
||||
&.is-5by3,
|
||||
&.is-16by9,
|
||||
&.is-2by1,
|
||||
&.is-3by1,
|
||||
&.is-4by5,
|
||||
&.is-3by4,
|
||||
&.is-2by3,
|
||||
&.is-3by5,
|
||||
&.is-9by16,
|
||||
&.is-1by2,
|
||||
&.is-1by3
|
||||
img,
|
||||
.has-ratio
|
||||
@extend %overlay
|
||||
height: 100%
|
||||
width: 100%
|
||||
&.is-square,
|
||||
&.is-1by1
|
||||
padding-top: 100%
|
||||
&.is-5by4
|
||||
padding-top: 80%
|
||||
&.is-4by3
|
||||
padding-top: 75%
|
||||
&.is-3by2
|
||||
padding-top: 66.6666%
|
||||
&.is-5by3
|
||||
padding-top: 60%
|
||||
&.is-16by9
|
||||
padding-top: 56.25%
|
||||
&.is-2by1
|
||||
padding-top: 50%
|
||||
&.is-3by1
|
||||
padding-top: 33.3333%
|
||||
&.is-4by5
|
||||
padding-top: 125%
|
||||
&.is-3by4
|
||||
padding-top: 133.3333%
|
||||
&.is-2by3
|
||||
padding-top: 150%
|
||||
&.is-3by5
|
||||
padding-top: 166.6666%
|
||||
&.is-9by16
|
||||
padding-top: 177.7777%
|
||||
&.is-1by2
|
||||
padding-top: 200%
|
||||
&.is-1by3
|
||||
padding-top: 300%
|
||||
// Sizes
|
||||
@each $dimension in $dimensions
|
||||
&.is-#{$dimension}x#{$dimension}
|
||||
height: $dimension * 1px
|
||||
width: $dimension * 1px
|
50
static/css/sass/bulma/elements/notification.sass
Normal file
50
static/css/sass/bulma/elements/notification.sass
Normal file
@ -0,0 +1,50 @@
|
||||
$notification-background-color: $background !default
|
||||
$notification-code-background-color: $scheme-main !default
|
||||
$notification-radius: $radius !default
|
||||
$notification-padding: 1.25rem 2.5rem 1.25rem 1.5rem !default
|
||||
$notification-padding-ltr: 1.25rem 2.5rem 1.25rem 1.5rem !default
|
||||
$notification-padding-rtl: 1.25rem 1.5rem 1.25rem 2.5rem !default
|
||||
|
||||
$notification-colors: $colors !default
|
||||
|
||||
.notification
|
||||
@extend %block
|
||||
background-color: $notification-background-color
|
||||
border-radius: $notification-radius
|
||||
position: relative
|
||||
+ltr
|
||||
padding: $notification-padding-ltr
|
||||
+rtl
|
||||
padding: $notification-padding-rtl
|
||||
a:not(.button):not(.dropdown-item)
|
||||
color: currentColor
|
||||
text-decoration: underline
|
||||
strong
|
||||
color: currentColor
|
||||
code,
|
||||
pre
|
||||
background: $notification-code-background-color
|
||||
pre code
|
||||
background: transparent
|
||||
& > .delete
|
||||
+ltr-position(0.5rem)
|
||||
position: absolute
|
||||
top: 0.5rem
|
||||
.title,
|
||||
.subtitle,
|
||||
.content
|
||||
color: currentColor
|
||||
// Colors
|
||||
@each $name, $pair in $notification-colors
|
||||
$color: nth($pair, 1)
|
||||
$color-invert: nth($pair, 2)
|
||||
&.is-#{$name}
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
// If light and dark colors are provided
|
||||
@if length($pair) >= 4
|
||||
$color-light: nth($pair, 3)
|
||||
$color-dark: nth($pair, 4)
|
||||
&.is-light
|
||||
background-color: $color-light
|
||||
color: $color-dark
|
39
static/css/sass/bulma/elements/other.sass
Normal file
39
static/css/sass/bulma/elements/other.sass
Normal file
@ -0,0 +1,39 @@
|
||||
.block
|
||||
@extend %block
|
||||
|
||||
.delete
|
||||
@extend %delete
|
||||
|
||||
.heading
|
||||
display: block
|
||||
font-size: 11px
|
||||
letter-spacing: 1px
|
||||
margin-bottom: 5px
|
||||
text-transform: uppercase
|
||||
|
||||
.highlight
|
||||
@extend %block
|
||||
font-weight: $weight-normal
|
||||
max-width: 100%
|
||||
overflow: hidden
|
||||
padding: 0
|
||||
pre
|
||||
overflow: auto
|
||||
max-width: 100%
|
||||
|
||||
.loader
|
||||
@extend %loader
|
||||
|
||||
.number
|
||||
align-items: center
|
||||
background-color: $background
|
||||
border-radius: $radius-rounded
|
||||
display: inline-flex
|
||||
font-size: $size-medium
|
||||
height: 2em
|
||||
justify-content: center
|
||||
margin-right: 1.5rem
|
||||
min-width: 2.5em
|
||||
padding: 0.25rem 0.5rem
|
||||
text-align: center
|
||||
vertical-align: top
|
71
static/css/sass/bulma/elements/progress.sass
Normal file
71
static/css/sass/bulma/elements/progress.sass
Normal file
@ -0,0 +1,71 @@
|
||||
$progress-bar-background-color: $border-light !default
|
||||
$progress-value-background-color: $text !default
|
||||
$progress-border-radius: $radius-rounded !default
|
||||
|
||||
$progress-indeterminate-duration: 1.5s !default
|
||||
|
||||
$progress-colors: $colors !default
|
||||
|
||||
.progress
|
||||
@extend %block
|
||||
-moz-appearance: none
|
||||
-webkit-appearance: none
|
||||
border: none
|
||||
border-radius: $progress-border-radius
|
||||
display: block
|
||||
height: $size-normal
|
||||
overflow: hidden
|
||||
padding: 0
|
||||
width: 100%
|
||||
&::-webkit-progress-bar
|
||||
background-color: $progress-bar-background-color
|
||||
&::-webkit-progress-value
|
||||
background-color: $progress-value-background-color
|
||||
&::-moz-progress-bar
|
||||
background-color: $progress-value-background-color
|
||||
&::-ms-fill
|
||||
background-color: $progress-value-background-color
|
||||
border: none
|
||||
// Colors
|
||||
@each $name, $pair in $progress-colors
|
||||
$color: nth($pair, 1)
|
||||
&.is-#{$name}
|
||||
&::-webkit-progress-value
|
||||
background-color: $color
|
||||
&::-moz-progress-bar
|
||||
background-color: $color
|
||||
&::-ms-fill
|
||||
background-color: $color
|
||||
&:indeterminate
|
||||
background-image: linear-gradient(to right, $color 30%, $progress-bar-background-color 30%)
|
||||
|
||||
&:indeterminate
|
||||
animation-duration: $progress-indeterminate-duration
|
||||
animation-iteration-count: infinite
|
||||
animation-name: moveIndeterminate
|
||||
animation-timing-function: linear
|
||||
background-color: $progress-bar-background-color
|
||||
background-image: linear-gradient(to right, $text 30%, $progress-bar-background-color 30%)
|
||||
background-position: top left
|
||||
background-repeat: no-repeat
|
||||
background-size: 150% 150%
|
||||
&::-webkit-progress-bar
|
||||
background-color: transparent
|
||||
&::-moz-progress-bar
|
||||
background-color: transparent
|
||||
&::-ms-fill
|
||||
animation-name: none
|
||||
|
||||
// Sizes
|
||||
&.is-small
|
||||
height: $size-small
|
||||
&.is-medium
|
||||
height: $size-medium
|
||||
&.is-large
|
||||
height: $size-large
|
||||
|
||||
@keyframes moveIndeterminate
|
||||
from
|
||||
background-position: 200% 0
|
||||
to
|
||||
background-position: -200% 0
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user