Compare commits
68 Commits
Author | SHA1 | Date | |
---|---|---|---|
70305a5e3c | |||
7ddd4b7fb7 | |||
b5a4b4192c | |||
a29104f172 | |||
fa63472d8c | |||
464f45cbb9 | |||
5705ab6919 | |||
e9b82016f3 | |||
7125cc0719 | |||
72f971162d | |||
58f21ca06e | |||
9a46b52904 | |||
0473c590a4 | |||
52b4ab5647 | |||
290830571d | |||
0da679957b | |||
3f81c6fb1a | |||
80ee9e0496 | |||
96ea3675f9 | |||
313e98d920 | |||
01d20f4641 | |||
1071acfc4c | |||
5ac50ed667 | |||
1e3ce054a9 | |||
ad8f68cdd8 | |||
194ffc754b | |||
4737702f3f | |||
8d244b584c | |||
3be1eed294 | |||
9c44ff3d57 | |||
0c56e734d8 | |||
f81d5ee548 | |||
2480b92905 | |||
2574fa13f7 | |||
f7c312b1e0 | |||
b831d5a8d5 | |||
11b2fe5942 | |||
0cdd3a16dc | |||
594651872c | |||
f20873ab0f | |||
fdec8f74c8 | |||
edd9ce7585 | |||
285a808ee4 | |||
76cf02a1f0 | |||
3df5f74ec3 | |||
b4edd48b72 | |||
9ef6c0657a | |||
3f7d06fc0f | |||
87685c4a62 | |||
9c122022f1 | |||
6a402ee64f | |||
c2e11b779e | |||
7d068991cf | |||
b2db4eb526 | |||
8115b908cf | |||
a72a6e7e62 | |||
136ec736e3 | |||
cfe4cf076a | |||
fce15c10a1 | |||
5f6a912f6b | |||
8f6ec52750 | |||
698e1dee50 | |||
01b534f027 | |||
0b50153774 | |||
b92e179584 | |||
c630472a75 | |||
a0ad65227a | |||
6672318765 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# 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,8 +1,14 @@
|
||||
.idea
|
||||
__pycache__/
|
||||
venv
|
||||
db/labertasche.db-shm
|
||||
db/labertasche.db-wal
|
||||
output
|
||||
/output/
|
||||
*.sql
|
||||
db/*.db
|
||||
db/*.db-shm
|
||||
db/*.db-wal
|
||||
*.old
|
||||
*.server
|
||||
/backup/
|
||||
labertasche.yaml
|
||||
/.secret
|
||||
/credentials.yaml
|
||||
*.bak
|
||||
smileys.yaml
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[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 Domeniko Gentner <code@tuxstash.de>
|
||||
Copyright (c) 2020-2077 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
|
||||
|
1
Pipfile
1
Pipfile
@ -15,6 +15,7 @@ flask-login = "*"
|
||||
sqlalchemy = "*"
|
||||
requests = "*"
|
||||
py3-validate-email = "*"
|
||||
validators = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
119
Pipfile.lock
generated
119
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "57134ef6f8a30aa46c1ab6263e62e14edbb27d6df2911fc6b2140dde8c49d27c"
|
||||
"sha256": "3a5cabc81d97143e23ec48af6c789d70ba16128d02104f8323100443b44e6b10"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -25,17 +25,17 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
|
||||
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
],
|
||||
"version": "==2020.11.8"
|
||||
"version": "==2020.12.5"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@ -44,6 +44,13 @@
|
||||
],
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
|
||||
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
|
||||
],
|
||||
"version": "==4.4.2"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7",
|
||||
@ -151,21 +158,23 @@
|
||||
},
|
||||
"py3-validate-email": {
|
||||
"hashes": [
|
||||
"sha256:3bbb264b49c0ae09afdb2738956f00b0e8dd7e079e2d079b2e9b6688de474d28"
|
||||
"sha256:e5815a929c064face7b6e775f290f157ab52c1c88d56d27d031a02a185b991e3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.10"
|
||||
"version": "==0.2.12"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
|
||||
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
|
||||
"sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
|
||||
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
|
||||
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
|
||||
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
|
||||
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
|
||||
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
|
||||
"sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
|
||||
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
|
||||
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
|
||||
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
|
||||
@ -175,11 +184,11 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.25.0"
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@ -190,47 +199,47 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"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"
|
||||
"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"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.20"
|
||||
"version": "==1.3.22"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@ -239,6 +248,14 @@
|
||||
],
|
||||
"version": "==1.26.2"
|
||||
},
|
||||
"validators": {
|
||||
"hashes": [
|
||||
"sha256:0143dcca8a386498edaf5780cbd5960da1a4c85e0719f3ee5c9b41249c4fefbd",
|
||||
"sha256:37cd9a9213278538ad09b5b9f9134266e7c226ab1fede1d500e29e0a8fbb9ea6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||
|
102
README.md
102
README.md
@ -14,13 +14,28 @@ A comment system for Hugo, written in Python (and Javascript).
|
||||
* EMail Blocklist
|
||||
* Only outputs JSON, so templates can be done independently, enhancing customization. Using the comments via a partial
|
||||
template in Hugo is the recommended way. See below for integration code.
|
||||
|
||||
* Antispam
|
||||
* Email Validation
|
||||
|
||||
## Requirements
|
||||
|
||||
* A public webserver capable of running Apache/NGINX and/or gunicorn. This server does not need to be the same as the
|
||||
* A public webserver capable of running Python, Apache/NGINX and/or gunicorn. This server does not need to be the same as the
|
||||
server running the site, but it must have access to your CI/CD chain. Same server is of course easier to implement.
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Python 3.8
|
||||
* flask
|
||||
* flask-cors
|
||||
* flask-sqlalchemy
|
||||
* flask-login
|
||||
* antispam
|
||||
* pyyaml
|
||||
* requests
|
||||
* py3-validate-email
|
||||
* Recommended OS: Ubuntu 20, Debian Buster
|
||||
* Recommended Server Software: Apache with libmodwsgi for Python 3
|
||||
* GoHugo, but the json can also be used by Javascript and other languages
|
||||
|
||||
## How does it work?
|
||||
|
||||
@ -34,7 +49,7 @@ template.
|
||||
|
||||
## Setup
|
||||
|
||||
Run `ssh://git@git.tuxstash.de:1235/gothseidank/labertasche.git` in the directory where you wish to host the comment
|
||||
Run `git clone ssh://git@git.tuxstash.de:1235/gothseidank/labertasche.git` in the directory where you wish to host the comment
|
||||
system. For example, `/var/www/html`, I also recommend making use of `/srv/` or `/opt/`. It depends on you.
|
||||
|
||||
When everything is downloaded, create the directory `/etc/labertasche`. In this directory, we need 2 files:
|
||||
@ -43,21 +58,21 @@ When everything is downloaded, create the directory `/etc/labertasche`. In this
|
||||
* mail_credentials.json - you can find an example in the root directory.
|
||||
|
||||
Copy these files from the root directory of this app to the folder `/etc/labertasche`. Make sure to set ownership for
|
||||
your user that runs your server later. I always do `chmown user:www-data`, so Apache has only group rights and enable read-only
|
||||
for the Apache user.
|
||||
your user that runs your server later. I always do `chown user:www-data`, so Apache has only group rights and enable read-only
|
||||
for the Apache user. I also recommend `chmod 700` for the directory and `chmod 600` for the files.
|
||||
|
||||
Make sure to read the config and replace the values as needed. The mail configuration should need no explanation,
|
||||
`labertasche.yaml` has comments. Feel free to bug about more documentation regarding this. Pay special attention to
|
||||
`labertasche.yaml` has comments. Feel free to bug me about more documentation regarding this. Pay special attention to
|
||||
secrets and passwords.
|
||||
|
||||
Now, for the server there are several options. I personally always host flask apps with Apache and mod_wsgi.
|
||||
Now, for the server there are several options. I personally always host flask apps with Apache and libmodwsgi.
|
||||
The config looks like this:
|
||||
|
||||
* [Apache](docs/apache-config.md)
|
||||
|
||||
Other options:
|
||||
|
||||
* [gunicorn](https://gunicorn.org/https://gunicorn.org/) + Apache/Nginx with Proxy Pass
|
||||
* [gunicorn](https://gunicorn.org/) + Apache/Nginx with Proxy Pass
|
||||
|
||||
Once you can see the administrative page, you can start integrating it into Hugo.
|
||||
|
||||
@ -65,12 +80,12 @@ Once you can see the administrative page, you can start integrating it into Hugo
|
||||
|
||||
### Javascript
|
||||
|
||||
In the project folder is a small javascript file. You will need to load this into Hugo, I suggest using Hugo's asset
|
||||
pipeline to integrate it into your site. One thing is important to know: this script only does the bare bones post request
|
||||
to your comment backend. Anything else must be done by yourself, but don't worry: The function is making use of a callback,
|
||||
so you can control what happens during the various stages.
|
||||
|
||||
TODO: Example using the javascript properly
|
||||
In the project folder is a small javascript file. You will need to add this to Hugo. I suggest using Hugo's asset
|
||||
pipeline to integrate it into your site and merge it with your current javascript.
|
||||
One thing is important to know: this script only does the bare bones post request to the comment backend.
|
||||
Any frontend work must be done by yourself, such as messages about minimum length etc.
|
||||
But don't worry: The function is making use of a callback, where you can receive various messages with error codes
|
||||
and act on them. See the javascript file for an example callback.
|
||||
|
||||
### Hugo templates
|
||||
|
||||
@ -78,14 +93,19 @@ Remember the `labertasche.yaml` file? It asked you where the data folder of Hugo
|
||||
various json files into that folder, in folders that describe your sections. So, for each category/section of your blog
|
||||
where comments can be placed, one folder will be made. And for each page within that section it generates a json file.
|
||||
|
||||
Now create a new partial called "comments.html" (or something else). Within that template the following structure is needed:
|
||||
Now create a new [partial](https://gohugo.io/templates/partials/) called "comments.html" (or something else).
|
||||
Within that template the following structure is needed:
|
||||
|
||||
```
|
||||
{{ $location := .Scratch.Get "location" }}
|
||||
{{ 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}}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
```
|
||||
@ -105,28 +125,70 @@ Of course you will also need a few inputs and a button that submits the data.
|
||||
Here is a base skeleton to start out:
|
||||
|
||||
```
|
||||
<div>
|
||||
<di id="labertasche-comment-section" data-remote="https://comments.example.com/comments/new">
|
||||
<input type="text" maxlength=100 placeholder="Enter Email" id="labertasche-mail">
|
||||
<textarea cols="10" rows="10" id="labertasche-text"></textarea>
|
||||
<input type="button" onclick="labertasche_post_comment(this, labertasche_callback);">
|
||||
</div>
|
||||
```
|
||||
|
||||
This is the recommended element on each top post if you want to utilize replies:
|
||||
|
||||
```
|
||||
<div class="">
|
||||
<a href="#labertasche-comment-section"
|
||||
onclick="labertasche_reply_to({{.comment_id}}, labertasche_reply_callback);">
|
||||
reply
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
Please take note of the `id` on each element, these are mandatory, as well as the function call for the `onclick` event.
|
||||
Again, style as needed and add more Javascript to your gusto.
|
||||
Again, style as needed and add more Javascript to your gusto. Make sure to implement the callback, otherwise the
|
||||
Javascript will crash. The `data-remote=` needs to have the URL where you host this program, as well as the path to
|
||||
the API endpoint.
|
||||
|
||||
|
||||
Inside your template `single.html`, or wherever you want to place comments, you qwill also need this:
|
||||
Inside your template `single.html`, or wherever you want to place comments, you will also need this:
|
||||
|
||||
```
|
||||
{{ $file := replaceRE "^(.*)[\\/]$" "data$1.json" .Page.RelPermalink }}
|
||||
{{ .Scratch.Set "location" $file }}
|
||||
{{ partial "partials/comments" . }}
|
||||
```
|
||||
There is no styling needed for this part!
|
||||
|
||||
After that and configuring labertasche correctly, the json files should be placed in your data folder and all you got
|
||||
to do after that, is to rebuild Hugo and the new comment should appear.
|
||||
|
||||
## Watching for changes via systemd
|
||||
|
||||
Hugo accepts the `--watch` command without the `server` option:
|
||||
`hugo --watch` is valid and it will watch the directory and rebuild the files, if something changes.
|
||||
Knowing that, we can build a systemd service from that, which could look like this:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Hugo
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/html/
|
||||
ExecStart=/usr/local/bin/hugo --watch --minify --noChmod --cleanDestinationDir --gc
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
`-noChmod` is a very important switch for this, because it stops Hugo from adjusting the file permissions.
|
||||
This comes in handy if you have a difference in user and group on your web server. `--cleanDestinationDir` and `--gc`
|
||||
will clean old files out, so you don't have to worry about synching the public directory with the current content of
|
||||
your static or assets dir. There will also be no old CSS files be lying around when using fingerprinting.
|
||||
|
||||
<!--suppress HtmlDeprecatedAttribute -->
|
||||
<p align="center">
|
||||
|
21
__implementation_example/README.md
Normal file
21
__implementation_example/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# 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.**
|
6
__implementation_example/archetypes/default.md
Normal file
6
__implementation_example/archetypes/default.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
4
__implementation_example/config.toml
Normal file
4
__implementation_example/config.toml
Normal file
@ -0,0 +1,4 @@
|
||||
baseURL = "http://dev.localhost/"
|
||||
languageCode = "en-us"
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
ignoreErrors = ["error-disable-taxonomy"]
|
94
__implementation_example/content/blog/readme.md
Normal file
94
__implementation_example/content/blog/readme.md
Normal file
@ -0,0 +1,94 @@
|
||||
---
|
||||
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.
|
61
__implementation_example/content/blog/stramine.md
Normal file
61
__implementation_example/content/blog/stramine.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
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
__implementation_example/data/blog/article-3.json
Normal file
1
__implementation_example/data/blog/article-3.json
Normal file
@ -0,0 +1 @@
|
||||
{"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"}]}
|
22
__implementation_example/data/blog/readme.json
Normal file
22
__implementation_example/data/blog/readme.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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
__implementation_example/data/blog/stramine.json
Normal file
1
__implementation_example/data/blog/stramine.json
Normal file
@ -0,0 +1 @@
|
||||
{"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"}]}
|
56
__implementation_example/layouts/_default/baseof.html
Normal file
56
__implementation_example/layouts/_default/baseof.html
Normal file
@ -0,0 +1,56 @@
|
||||
<!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>
|
@ -0,0 +1,7 @@
|
||||
{{ 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 }}
|
12
__implementation_example/layouts/_default/home.html
Normal file
12
__implementation_example/layouts/_default/home.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{ 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 }}
|
13
__implementation_example/layouts/_default/single.html
Normal file
13
__implementation_example/layouts/_default/single.html
Normal file
@ -0,0 +1,13 @@
|
||||
{{ 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 }}
|
111
__implementation_example/layouts/partials/comments.html
Normal file
111
__implementation_example/layouts/partials/comments.html
Normal file
@ -0,0 +1,111 @@
|
||||
{{ $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 }}
|
12
__implementation_example/static/css/labertasche.css
Normal file
12
__implementation_example/static/css/labertasche.css
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
169
__implementation_example/static/css/tuxstash.css.map
Normal file
169
__implementation_example/static/css/tuxstash.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
__implementation_example/static/images/default.jpg
Normal file
BIN
__implementation_example/static/images/default.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
122
__implementation_example/static/js/labertasche.js
Normal file
122
__implementation_example/static/js/labertasche.js
Normal file
@ -0,0 +1,122 @@
|
||||
//**********************************************************************************
|
||||
// * _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;
|
||||
}
|
156
__implementation_example/static/js/mysite.js
Normal file
156
__implementation_example/static/js/mysite.js
Normal file
@ -0,0 +1,156 @@
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
48
convert_settings.py
Executable file
48
convert_settings.py
Executable file
@ -0,0 +1,48 @@
|
||||
#!/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.
|
||||
""")
|
108
db/example-data.sql
Normal file
108
db/example-data.sql
Normal file
@ -0,0 +1,108 @@
|
||||
/**********************************************************************************
|
||||
* _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)
|
||||
;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
82
i18n/de-DE.json
Normal file
82
i18n/de-DE.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
82
i18n/en-US.json
Normal file
82
i18n/en-US.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"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."
|
||||
}
|
@ -6,17 +6,16 @@
|
||||
// *********************************************************************************/
|
||||
|
||||
/*
|
||||
Callback example.
|
||||
Possible messages:
|
||||
|
||||
post-min-length
|
||||
post-max-length
|
||||
post-invalid-json
|
||||
post-duplicate
|
||||
post-internal-server-error
|
||||
post-success
|
||||
post-before-fetch
|
||||
|
||||
//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
|
||||
// post-project-not-found
|
||||
function labertasche_callback(state)
|
||||
{
|
||||
if (state === "post-before-fetch"){
|
||||
@ -35,20 +34,63 @@ function labertasche_callback(state)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for initiating and cancelling replies.
|
||||
// Posstible message: 'on' and 'off'
|
||||
function labertasche_reply_callback(state)
|
||||
{
|
||||
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;
|
||||
let mail = document.getElementById('labertasche-mail').value;
|
||||
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) {
|
||||
btn.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return
|
||||
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');
|
||||
@ -65,7 +107,7 @@ function labertasche_post_comment(btn, callback)
|
||||
body: JSON.stringify({ "email": mail,
|
||||
"content": comment,
|
||||
"location": window.location.pathname,
|
||||
"replied_to": null // TODO: future feature: replies?
|
||||
"replied_to": reply_value
|
||||
})
|
||||
})
|
||||
.then(async function(response){
|
||||
@ -78,7 +120,5 @@ function labertasche_post_comment(btn, callback)
|
||||
})
|
||||
|
||||
// Don't reload the page
|
||||
if (btn) {
|
||||
btn.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
# /**********************************************************************************
|
||||
# * _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://dev.localhost:1314/" # Url where the comment system is served
|
||||
blog_url: "http://dev.localhost:1313/" # Url of your website
|
||||
cookie-domain: "dev.localhost" # Url where the comment system is served
|
||||
database_uri: "sqlite:///db/labertasche.db" # Database URI. See documentation. Default is sqlite.
|
||||
secret: "6Gxvb52bIJCm2vfDsmWKzShKp1omrzVG" # CHANGE ME! THIS IS IMPORTANT!
|
||||
output: "../../web/tuxstash.de/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: "../../web/tuxstash.de/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
|
||||
|
||||
# https://www.w3schools.com/charsets/ref_emoji_smileys.asp
|
||||
smileys:
|
||||
":)": "😀"
|
||||
":d": "😁"
|
||||
":D": "😁"
|
||||
";)": "😉"
|
||||
":p": "😋"
|
||||
":P": "😋"
|
||||
":8": "😎"
|
||||
"(:": "🙃"
|
||||
"$)": "🤑"
|
||||
":o": "😲"
|
||||
":O": "😲"
|
||||
|
@ -13,7 +13,8 @@ from labertasche import (
|
||||
blueprints,
|
||||
helper,
|
||||
mail,
|
||||
settings
|
||||
settings,
|
||||
language
|
||||
)
|
||||
|
||||
_all_ = [
|
||||
@ -22,5 +23,6 @@ _all_ = [
|
||||
blueprints,
|
||||
helper,
|
||||
mail,
|
||||
settings
|
||||
settings,
|
||||
language
|
||||
]
|
||||
|
@ -9,4 +9,5 @@
|
||||
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,30 +14,37 @@ 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
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.mail import Mail
|
||||
from labertasche.models import TComments, TLocation, TEmail, TProjects
|
||||
from labertasche.settings import Smileys
|
||||
from secrets import compare_digest
|
||||
|
||||
|
||||
# Blueprint
|
||||
bp_comments = Blueprint("bp_comments", __name__, url_prefix='/comments')
|
||||
|
||||
|
||||
# Route for adding new comments
|
||||
@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()
|
||||
@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()
|
||||
|
||||
# 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
|
||||
@ -48,9 +55,8 @@ def check_and_insert_new_comment():
|
||||
|
||||
# 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:
|
||||
print("too short", file=stderr)
|
||||
len(new_comment['content']) < 40 or \
|
||||
len(new_comment['email']) < 5:
|
||||
return make_response(jsonify(status='post-invalid-json'), 400)
|
||||
|
||||
# Strip any HTML from message body
|
||||
@ -59,16 +65,26 @@ def check_and_insert_new_comment():
|
||||
content = re.sub(tags, '', new_comment['content']).strip()
|
||||
content = re.sub(special, '', content).strip()
|
||||
|
||||
# Convert smileys
|
||||
if addons['smileys']:
|
||||
for key, value in smileys.items():
|
||||
# Convert smileys if enabled
|
||||
if project.addon_smileys:
|
||||
for key, value in smileys.smileys.items():
|
||||
content = content.replace(key, value)
|
||||
|
||||
# Validate replied_to field is integer
|
||||
replied_to = new_comment['replied_to']
|
||||
try:
|
||||
if replied_to:
|
||||
replied_to = int(replied_to)
|
||||
|
||||
# not a valid id at all
|
||||
except ValueError:
|
||||
return make_response(jsonify(status="bad-reply"), 400)
|
||||
|
||||
# Update values
|
||||
new_comment.update({"content": content})
|
||||
new_comment.update({"email": new_comment['email'].strip()})
|
||||
new_comment.update({"location": location})
|
||||
new_comment.update({"replied_to": None}) # Not (yet?) implemented
|
||||
new_comment.update({"replied_to": replied_to})
|
||||
|
||||
# Check mail
|
||||
if not sender.validate(new_comment['email']):
|
||||
@ -92,21 +108,25 @@ def check_and_insert_new_comment():
|
||||
if not email.is_allowed:
|
||||
is_spam = True
|
||||
if email.is_allowed:
|
||||
# This forces the comment to be not spam if the address is in the allowed list,
|
||||
# but the commenter will still need to confirm it to avoid brute
|
||||
# force attacks against this feature
|
||||
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():
|
||||
# Set existing location id
|
||||
# Location exists, set existing location id
|
||||
new_comment.update({'location_id': loc_query.first().id_location})
|
||||
# TComments does not have this field
|
||||
new_comment.pop("location")
|
||||
else:
|
||||
# Insert new location
|
||||
loc_table = {
|
||||
'location': new_comment['location']
|
||||
'location': new_comment['location'],
|
||||
'project_id': project.id_project
|
||||
}
|
||||
new_loc = TLocation(**loc_table)
|
||||
db.session.add(new_loc)
|
||||
@ -118,12 +138,17 @@ def check_and_insert_new_comment():
|
||||
new_comment.pop("location")
|
||||
|
||||
# insert comment
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
new_comment.update({"is_published": False})
|
||||
if project.sendotp:
|
||||
new_comment.update({"is_published": False})
|
||||
else:
|
||||
new_comment.update({"is_published": True})
|
||||
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'])})
|
||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'], project.name)})
|
||||
new_comment.update({"project_id": project.id_project})
|
||||
t_comment = TComments(**new_comment)
|
||||
db.session.add(t_comment)
|
||||
db.session.commit()
|
||||
@ -131,31 +156,33 @@ def check_and_insert_new_comment():
|
||||
db.session.refresh(t_comment)
|
||||
|
||||
# Send confirmation link and store returned value
|
||||
hashes = sender.send_confirmation_link(new_comment['email'])
|
||||
setattr(t_comment, "confirmation", hashes[0])
|
||||
setattr(t_comment, "deletion", hashes[1])
|
||||
db.session.commit()
|
||||
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()
|
||||
|
||||
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 as e: # must be at bottom
|
||||
# mail(f"check_and_insert_new_comment has thrown an error: {e}", )
|
||||
print(e, file=stderr)
|
||||
except Exception: # must be at bottom
|
||||
return make_response(jsonify(status="post-internal-server-error"), 400)
|
||||
|
||||
export_location(location)
|
||||
return make_response(jsonify(status="post-success", comment_id=t_comment.comments_id), 200)
|
||||
export_location(t_comment.location_id)
|
||||
return make_response(jsonify(status="post-success",
|
||||
comment_id=t_comment.comments_id,
|
||||
sendotp=project.sendotp), 200)
|
||||
|
||||
|
||||
# Route for confirming comments
|
||||
@bp_comments.route("/confirm/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("/<name>/confirm/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_confirmation_link(email_hash):
|
||||
settings = Settings()
|
||||
def check_confirmation_link(name, email_hash):
|
||||
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):
|
||||
@ -163,27 +190,27 @@ def check_confirmation_link(email_hash):
|
||||
if not comment.is_spam:
|
||||
setattr(comment, "is_published", True)
|
||||
db.session.commit()
|
||||
url = f"{settings.system['blog_url']}{location.location}#comment_{comment.comments_id}"
|
||||
export_location(location.location)
|
||||
url = f"{project.blogurl}{location.location}#comment_{comment.comments_id}"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
||||
|
||||
|
||||
# Route for deleting comments
|
||||
@bp_comments.route("/delete/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("<name>/delete/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_deletion_link(email_hash):
|
||||
settings = Settings()
|
||||
query = db.session.query(TComments).filter(TComments.deletion == email_hash)
|
||||
comment = query.first()
|
||||
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()
|
||||
|
||||
if comment:
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||
if compare_digest(comment.deletion, email_hash):
|
||||
query.delete()
|
||||
db.session.delete(comment)
|
||||
db.session.commit()
|
||||
url = f"{settings.system['blog_url']}?deleted=true"
|
||||
export_location(location.location)
|
||||
url = f"{project.blogurl}?deleted=true"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
@ -1,201 +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, 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
|
||||
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>', methods=["POST", "GET"])
|
||||
@login_required
|
||||
def dashboard_review_spam(location=None):
|
||||
all_locations = db.session.query(TLocation).all()
|
||||
|
||||
# Check post
|
||||
if request.method == "POST":
|
||||
location = request.form.get('selected_location')
|
||||
|
||||
# no parameters found
|
||||
if location is None:
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location)
|
||||
|
||||
try:
|
||||
if int(location) >= 1:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.location_id == location)\
|
||||
.filter(TComments.is_spam == True)
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location,
|
||||
spam_comments=spam_comments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return render_template("review-spam.html", locations=all_locations, selected=location)
|
||||
|
||||
|
||||
@bp_dashboard.route('/manage-comments/', methods=["POST", "GET"])
|
||||
@bp_dashboard.route('/manage-comments/<int:location>', methods=["POST", "GET"])
|
||||
@login_required
|
||||
def dashboard_manage_regular_comments(location=None):
|
||||
all_locations = db.session.query(TLocation).all()
|
||||
|
||||
# Check post
|
||||
if request.method == "POST":
|
||||
location = request.form.get('selected_location')
|
||||
|
||||
# no parameters found
|
||||
if location is None:
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location)
|
||||
|
||||
try:
|
||||
if int(location) >= 1:
|
||||
spam_comments = db.session.query(TComments).filter(TComments.location_id == location) \
|
||||
.filter(TComments.is_spam == False)
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location,
|
||||
spam_comments=spam_comments)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return render_template("manage-comments.html", locations=all_locations, selected=location)
|
||||
|
||||
|
||||
@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]
|
||||
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]
|
||||
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]
|
||||
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]
|
||||
return redirect(f"{url}/{location_id}")
|
18
labertasche/blueprints/bp_dashboard/__init__.py
Normal file
18
labertasche/blueprints/bp_dashboard/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/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
|
68
labertasche/blueprints/bp_dashboard/comments.py
Normal file
68
labertasche/blueprints/bp_dashboard/comments.py
Normal file
@ -0,0 +1,68 @@
|
||||
#!/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")
|
||||
|
30
labertasche/blueprints/bp_dashboard/mail.py
Normal file
30
labertasche/blueprints/bp_dashboard/mail.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/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)
|
109
labertasche/blueprints/bp_dashboard/projects.py
Normal file
109
labertasche/blueprints/bp_dashboard/projects.py
Normal file
@ -0,0 +1,109 @@
|
||||
#!/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)
|
||||
|
72
labertasche/blueprints/bp_dashboard/spam.py
Normal file
72
labertasche/blueprints/bp_dashboard/spam.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/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")
|
||||
|
18
labertasche/blueprints/bp_jsconnector/__init__.py
Normal file
18
labertasche/blueprints/bp_jsconnector/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/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
|
130
labertasche/blueprints/bp_jsconnector/comments.py
Normal file
130
labertasche/blueprints/bp_jsconnector/comments.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/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)
|
21
labertasche/blueprints/bp_jsconnector/language.py
Normal file
21
labertasche/blueprints/bp_jsconnector/language.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/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)
|
37
labertasche/blueprints/bp_jsconnector/mail.py
Normal file
37
labertasche/blueprints/bp_jsconnector/mail.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/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)
|
||||
|
||||
|
191
labertasche/blueprints/bp_jsconnector/projects.py
Normal file
191
labertasche/blueprints/bp_jsconnector/projects.py
Normal file
@ -0,0 +1,191 @@
|
||||
#!/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,7 +8,9 @@
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.helper import check_auth, User
|
||||
from labertasche.helper import User
|
||||
from labertasche.settings import Credentials
|
||||
from secrets import compare_digest
|
||||
from flask_login import login_user, current_user, logout_user
|
||||
|
||||
# Blueprint
|
||||
@ -19,7 +21,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_index'))
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@ -30,9 +32,11 @@ def login():
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
if check_auth(username, password):
|
||||
credentials = Credentials()
|
||||
if compare_digest(username.encode('utf8'), credentials.username.encode('utf8')) and \
|
||||
credentials.compare_password(password):
|
||||
login_user(User(0), remember=True)
|
||||
return redirect(url_for('bp_dashboard.dashboard_index'))
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
|
||||
# Redirect get request to the login page
|
||||
return redirect(url_for('bp_login.show_login'))
|
14
labertasche/blueprints/bp_upgrades/__init__.py
Normal file
14
labertasche/blueprints/bp_upgrades/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
#!/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
|
235
labertasche/blueprints/bp_upgrades/db_v2.py
Normal file
235
labertasche/blueprints/bp_upgrades/db_v2.py
Normal file
@ -0,0 +1,235 @@
|
||||
#!/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,6 +7,20 @@
|
||||
# * _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()
|
||||
labertasche_db = SQLAlchemy(metadata=metadata, engine_options={
|
||||
'poolclass': NullPool
|
||||
})
|
||||
|
@ -8,21 +8,19 @@
|
||||
# *********************************************************************************/
|
||||
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
|
||||
import requests
|
||||
from labertasche.models import TLocation, TComments, TProjects
|
||||
from labertasche.database import labertasche_db as db
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
"""
|
||||
Class for flask-login, which represents a user
|
||||
"""
|
||||
def __init__(self, user_id):
|
||||
self.id = user_id
|
||||
|
||||
@ -37,7 +35,7 @@ def is_valid_json(j):
|
||||
try:
|
||||
json.dumps(j)
|
||||
return True
|
||||
except json.JSONDecodeError as e:
|
||||
except json.JSONDecodeError:
|
||||
print("not valid json")
|
||||
return False
|
||||
|
||||
@ -77,97 +75,81 @@ def alchemy_query_to_dict(obj):
|
||||
|
||||
# Come on, it's a mail hash, don't complain
|
||||
# noinspection InsecureHash
|
||||
def check_gravatar(email: str):
|
||||
def check_gravatar(email: str, name: 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
|
||||
"""
|
||||
settings = Settings()
|
||||
options = settings.gravatar
|
||||
from requests import get
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
|
||||
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"{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)
|
||||
|
||||
if project.gravatar_cache:
|
||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={project.gravatar_size}"
|
||||
response = 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)
|
||||
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: str) -> bool:
|
||||
def export_location(location_id: int) -> bool:
|
||||
"""
|
||||
Exports the comments for the location after the comment was accepted
|
||||
:param location: relative url of the hugo page
|
||||
:param location_id: The id of the store location to export
|
||||
"""
|
||||
try:
|
||||
# Query
|
||||
loc_query = db.session.query(TLocation).filter(TLocation.location == location).first()
|
||||
# flush before query
|
||||
db.session.flush()
|
||||
|
||||
if loc_query:
|
||||
# Query
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||
|
||||
if location:
|
||||
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
|
||||
.filter(TComments.is_published == True) \
|
||||
.filter(TComments.location_id == loc_query.id_location) \
|
||||
.filter(TComments.replied_to == None)
|
||||
.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
|
||||
|
||||
bundle = {
|
||||
"comments": []
|
||||
"comments": [],
|
||||
"replies": []
|
||||
}
|
||||
for comment in comments:
|
||||
if comment.replied_to is not None:
|
||||
bundle["replies"].append(alchemy_query_to_dict(comment))
|
||||
continue
|
||||
bundle['comments'].append(alchemy_query_to_dict(comment))
|
||||
|
||||
path_loc = re_match(".*(?=/)", loc_query.location)[0]
|
||||
|
||||
system = Settings().system
|
||||
out = Path(f"{system['output']}/{path_loc}.json")
|
||||
out = out.absolute()
|
||||
print(out)
|
||||
folder = out.parents[0]
|
||||
# Create folder if not exists and write file
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
with out.open('w') as fp:
|
||||
with jsonfile.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
|
||||
|
||||
@ -188,3 +170,17 @@ 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
|
||||
|
48
labertasche/language/__init__.py
Normal file
48
labertasche/language/__init__.py
Normal file
@ -0,0 +1,48 @@
|
||||
#!/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,12 +13,16 @@ 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
|
||||
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")
|
||||
@ -61,40 +65,49 @@ class mail:
|
||||
except SMTPException as e:
|
||||
print(f"SMTPException: {e}")
|
||||
|
||||
def send_confirmation_link(self, email):
|
||||
def send_confirmation_link(self, email: str, name: str) -> tuple:
|
||||
"""
|
||||
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.system['web_url']}/comments/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.system['web_url']}/comments/delete/{delete_digest}"
|
||||
confirm_url = f"{settings.weburl}/comments/{project.name}/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.weburl}/comments/{project.name}/delete/{delete_digest}"
|
||||
|
||||
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}"
|
||||
html_tpl = f"mail/comment_confirmation_{language.browser_language}.html"
|
||||
txt_tpl = f"mail/comment_confirmation_{language.browser_language}_txt.html"
|
||||
|
||||
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."
|
||||
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)
|
||||
|
||||
self.send(txt_what, html_what, email)
|
||||
|
||||
return confirm_digest, delete_digest
|
||||
|
||||
def validate(self, addr):
|
||||
# validate email
|
||||
is_valid = validate_email(email_address=addr,
|
||||
check_regex=True,
|
||||
check_mx=False,
|
||||
dns_timeout=10,
|
||||
use_blacklist=True,
|
||||
debug=False)
|
||||
is_valid = validate_email_or_fail(email_address=addr, check_regex=True, check_mx=False,
|
||||
dns_timeout=10, use_blacklist=True, debug=False)
|
||||
return is_valid
|
||||
|
@ -9,3 +9,5 @@
|
||||
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,10 +16,10 @@ class TComments(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
comments_id = db.Column(db.Integer, primary_key=True)
|
||||
comments_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# foreign keys
|
||||
location_id = db.Column(db.Text, ForeignKey('t_location.id_location'), nullable=False)
|
||||
location_id = db.Column(db.Integer, ForeignKey('t_location.id_location'), nullable=False)
|
||||
|
||||
# data
|
||||
email = db.Column(db.Text, nullable=False)
|
||||
@ -28,7 +28,8 @@ 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.Boolean, nullable=True)
|
||||
replied_to = db.Column(db.Integer, ForeignKey('t_comments.comments_id'), nullable=True, default=None)
|
||||
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,6 +7,7 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
from sqlalchemy import ForeignKey
|
||||
|
||||
|
||||
class TEmail(db.Model):
|
||||
@ -15,7 +16,7 @@ class TEmail(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_email = db.Column(db.Integer, primary_key=True)
|
||||
id_email = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# data
|
||||
email = db.Column(db.Integer, unique=True)
|
||||
|
@ -7,6 +7,7 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from labertasche.database import labertasche_db as db
|
||||
from sqlalchemy import ForeignKey, UniqueConstraint
|
||||
|
||||
|
||||
class TLocation(db.Model):
|
||||
@ -15,7 +16,11 @@ class TLocation(db.Model):
|
||||
__table_args__ = {'useexisting': True}
|
||||
|
||||
# primary key
|
||||
id_location = db.Column(db.Integer, primary_key=True)
|
||||
id_location = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# data
|
||||
location = db.Column(db.Text, nullable=False, unique=True)
|
||||
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")
|
||||
|
30
labertasche/models/t_projects.py
Normal file
30
labertasche/models/t_projects.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/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)
|
21
labertasche/models/t_version.py
Normal file
21
labertasche/models/t_version.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/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,12 +9,29 @@
|
||||
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:
|
||||
"""
|
||||
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
file = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
@ -23,8 +40,139 @@ class Settings:
|
||||
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):
|
||||
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']
|
||||
self.smileys = conf['smileys']
|
||||
|
||||
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'])
|
||||
|
77
server.py
77
server.py
@ -6,55 +6,74 @@
|
||||
# * _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 labertasche.settings import Settings
|
||||
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.database import labertasche_db
|
||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard
|
||||
from labertasche.language import Language
|
||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades
|
||||
from labertasche.helper import User
|
||||
from flask_login import LoginManager
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
# Load settings
|
||||
settings = Settings()
|
||||
secret = Secret()
|
||||
|
||||
# Flask App
|
||||
laberflask = Flask(__name__)
|
||||
laberflask.config.update(dict(
|
||||
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
|
||||
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
|
||||
))
|
||||
|
||||
# CORS
|
||||
CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']}})
|
||||
# Mark secret for deletion
|
||||
del secret
|
||||
|
||||
# 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 = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
# Initialize ORM
|
||||
labertasche_db.init_app(laberflask)
|
||||
with laberflask.app_context():
|
||||
labertasche_db.create_all()
|
||||
log = getLogger('werkzeug')
|
||||
log.setLevel(LOGGING_ERROR)
|
||||
|
||||
# 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":
|
||||
@ -62,14 +81,26 @@ 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.system["database_uri"][0:6] == 'sqlite':
|
||||
if settings.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,11 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# """
|
||||
# /**********************************************************************************
|
||||
# * _author : Domeniko Gentner
|
||||
# * _mail : code@tuxstash.de
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/piradio
|
||||
# * _license : This project is under GPL.v2
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
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
BIN
static/css/materialdesignicons-webfont.woff2
Normal file
BIN
static/css/materialdesignicons-webfont.woff2
Normal file
Binary file not shown.
1
static/css/materialdesignicons.min.css
vendored
Normal file
1
static/css/materialdesignicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/sass/bulma
Submodule
1
static/css/sass/bulma
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5432d0625b34ebfdc17518baed80897a82018d2d
|
@ -1,5 +0,0 @@
|
||||
/* Bulma Base */
|
||||
@charset "utf-8"
|
||||
|
||||
@import "minireset.sass"
|
||||
@import "generic.sass"
|
@ -1,143 +0,0 @@
|
||||
$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 +0,0 @@
|
||||
@warn "The helpers.sass file is DEPRECATED. It has moved into its own /helpers folder. Please import sass/helpers/_all instead."
|
@ -1,79 +0,0 @@
|
||||
/*! 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
|
@ -1,15 +0,0 @@
|
||||
/* 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"
|
@ -1,75 +0,0 @@
|
||||
$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"
|
@ -1,83 +0,0 @@
|
||||
$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
|
@ -1,81 +0,0 @@
|
||||
$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
|
@ -1,77 +0,0 @@
|
||||
$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
|
@ -1,52 +0,0 @@
|
||||
$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
|
@ -1,57 +0,0 @@
|
||||
$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
|
@ -1,99 +0,0 @@
|
||||
$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
|
@ -1,115 +0,0 @@
|
||||
$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
|
@ -1,443 +0,0 @@
|
||||
$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})
|
@ -1,150 +0,0 @@
|
||||
$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
|
@ -1,119 +0,0 @@
|
||||
$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
|
@ -1,174 +0,0 @@
|
||||
$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
|
@ -1,16 +0,0 @@
|
||||
/* 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"
|
@ -1,24 +0,0 @@
|
||||
$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
|
@ -1,325 +0,0 @@
|
||||
$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
|
@ -1,27 +0,0 @@
|
||||
$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
|
@ -1,155 +0,0 @@
|
||||
$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 +0,0 @@
|
||||
@warn "The form.sass file is DEPRECATED. It has moved into its own /form folder. Please import sass/form/_all instead."
|
@ -1,21 +0,0 @@
|
||||
$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
|
@ -1,71 +0,0 @@
|
||||
$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
|
@ -1,50 +0,0 @@
|
||||
$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
|
@ -1,39 +0,0 @@
|
||||
.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
|
@ -1,71 +0,0 @@
|
||||
$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
|
@ -1,131 +0,0 @@
|
||||
$table-color: $text-strong !default
|
||||
$table-background-color: $scheme-main !default
|
||||
|
||||
$table-cell-border: 1px solid $border !default
|
||||
$table-cell-border-width: 0 0 1px !default
|
||||
$table-cell-padding: 0.5em 0.75em !default
|
||||
$table-cell-heading-color: $text-strong !default
|
||||
|
||||
$table-head-cell-border-width: 0 0 2px !default
|
||||
$table-head-cell-color: $text-strong !default
|
||||
$table-foot-cell-border-width: 2px 0 0 !default
|
||||
$table-foot-cell-color: $text-strong !default
|
||||
|
||||
$table-head-background-color: transparent !default
|
||||
$table-body-background-color: transparent !default
|
||||
$table-foot-background-color: transparent !default
|
||||
|
||||
$table-row-hover-background-color: $scheme-main-bis !default
|
||||
|
||||
$table-row-active-background-color: $primary !default
|
||||
$table-row-active-color: $primary-invert !default
|
||||
|
||||
$table-striped-row-even-background-color: $scheme-main-bis !default
|
||||
$table-striped-row-even-hover-background-color: $scheme-main-ter !default
|
||||
|
||||
$table-colors: $colors !default
|
||||
|
||||
.table
|
||||
@extend %block
|
||||
background-color: $table-background-color
|
||||
color: $table-color
|
||||
td,
|
||||
th
|
||||
border: $table-cell-border
|
||||
border-width: $table-cell-border-width
|
||||
padding: $table-cell-padding
|
||||
vertical-align: top
|
||||
// Colors
|
||||
@each $name, $pair in $table-colors
|
||||
$color: nth($pair, 1)
|
||||
$color-invert: nth($pair, 2)
|
||||
&.is-#{$name}
|
||||
background-color: $color
|
||||
border-color: $color
|
||||
color: $color-invert
|
||||
// Modifiers
|
||||
&.is-narrow
|
||||
white-space: nowrap
|
||||
width: 1%
|
||||
&.is-selected
|
||||
background-color: $table-row-active-background-color
|
||||
color: $table-row-active-color
|
||||
a,
|
||||
strong
|
||||
color: currentColor
|
||||
&.is-vcentered
|
||||
vertical-align: middle
|
||||
th
|
||||
color: $table-cell-heading-color
|
||||
&:not([align])
|
||||
text-align: inherit
|
||||
tr
|
||||
&.is-selected
|
||||
background-color: $table-row-active-background-color
|
||||
color: $table-row-active-color
|
||||
a,
|
||||
strong
|
||||
color: currentColor
|
||||
td,
|
||||
th
|
||||
border-color: $table-row-active-color
|
||||
color: currentColor
|
||||
thead
|
||||
background-color: $table-head-background-color
|
||||
td,
|
||||
th
|
||||
border-width: $table-head-cell-border-width
|
||||
color: $table-head-cell-color
|
||||
tfoot
|
||||
background-color: $table-foot-background-color
|
||||
td,
|
||||
th
|
||||
border-width: $table-foot-cell-border-width
|
||||
color: $table-foot-cell-color
|
||||
tbody
|
||||
background-color: $table-body-background-color
|
||||
tr
|
||||
&:last-child
|
||||
td,
|
||||
th
|
||||
border-bottom-width: 0
|
||||
// Modifiers
|
||||
&.is-bordered
|
||||
td,
|
||||
th
|
||||
border-width: 1px
|
||||
tr
|
||||
&:last-child
|
||||
td,
|
||||
th
|
||||
border-bottom-width: 1px
|
||||
&.is-fullwidth
|
||||
width: 100%
|
||||
&.is-hoverable
|
||||
tbody
|
||||
tr:not(.is-selected)
|
||||
&:hover
|
||||
background-color: $table-row-hover-background-color
|
||||
&.is-striped
|
||||
tbody
|
||||
tr:not(.is-selected)
|
||||
&:hover
|
||||
background-color: $table-row-hover-background-color
|
||||
&:nth-child(even)
|
||||
background-color: $table-striped-row-even-hover-background-color
|
||||
&.is-narrow
|
||||
td,
|
||||
th
|
||||
padding: 0.25em 0.5em
|
||||
&.is-striped
|
||||
tbody
|
||||
tr:not(.is-selected)
|
||||
&:nth-child(even)
|
||||
background-color: $table-striped-row-even-background-color
|
||||
|
||||
.table-container
|
||||
@extend %block
|
||||
+overflow-touch
|
||||
overflow: auto
|
||||
overflow-y: hidden
|
||||
max-width: 100%
|
@ -1,138 +0,0 @@
|
||||
$tag-background-color: $background !default
|
||||
$tag-color: $text !default
|
||||
$tag-radius: $radius !default
|
||||
$tag-delete-margin: 1px !default
|
||||
|
||||
$tag-colors: $colors !default
|
||||
|
||||
.tags
|
||||
align-items: center
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: flex-start
|
||||
.tag
|
||||
margin-bottom: 0.5rem
|
||||
&:not(:last-child)
|
||||
+ltr-property("margin", 0.5rem)
|
||||
&:last-child
|
||||
margin-bottom: -0.5rem
|
||||
&:not(:last-child)
|
||||
margin-bottom: 1rem
|
||||
// Sizes
|
||||
&.are-medium
|
||||
.tag:not(.is-normal):not(.is-large)
|
||||
font-size: $size-normal
|
||||
&.are-large
|
||||
.tag:not(.is-normal):not(.is-medium)
|
||||
font-size: $size-medium
|
||||
&.is-centered
|
||||
justify-content: center
|
||||
.tag
|
||||
margin-right: 0.25rem
|
||||
margin-left: 0.25rem
|
||||
&.is-right
|
||||
justify-content: flex-end
|
||||
.tag
|
||||
&:not(:first-child)
|
||||
margin-left: 0.5rem
|
||||
&:not(:last-child)
|
||||
margin-right: 0
|
||||
&.has-addons
|
||||
.tag
|
||||
+ltr-property("margin", 0)
|
||||
&:not(:first-child)
|
||||
+ltr-property("margin", 0, false)
|
||||
+ltr
|
||||
border-top-left-radius: 0
|
||||
border-bottom-left-radius: 0
|
||||
+rtl
|
||||
border-top-right-radius: 0
|
||||
border-bottom-right-radius: 0
|
||||
&:not(:last-child)
|
||||
+ltr
|
||||
border-top-right-radius: 0
|
||||
border-bottom-right-radius: 0
|
||||
+rtl
|
||||
border-top-left-radius: 0
|
||||
border-bottom-left-radius: 0
|
||||
|
||||
.tag:not(body)
|
||||
align-items: center
|
||||
background-color: $tag-background-color
|
||||
border-radius: $tag-radius
|
||||
color: $tag-color
|
||||
display: inline-flex
|
||||
font-size: $size-small
|
||||
height: 2em
|
||||
justify-content: center
|
||||
line-height: 1.5
|
||||
padding-left: 0.75em
|
||||
padding-right: 0.75em
|
||||
white-space: nowrap
|
||||
.delete
|
||||
+ltr-property("margin", 0.25rem, false)
|
||||
+ltr-property("margin", -0.375rem)
|
||||
// Colors
|
||||
@each $name, $pair in $tag-colors
|
||||
$color: nth($pair, 1)
|
||||
$color-invert: nth($pair, 2)
|
||||
&.is-#{$name}
|
||||
background-color: $color
|
||||
color: $color-invert
|
||||
// If a light and dark colors are provided
|
||||
@if length($pair) > 3
|
||||
$color-light: nth($pair, 3)
|
||||
$color-dark: nth($pair, 4)
|
||||
&.is-light
|
||||
background-color: $color-light
|
||||
color: $color-dark
|
||||
// Sizes
|
||||
&.is-normal
|
||||
font-size: $size-small
|
||||
&.is-medium
|
||||
font-size: $size-normal
|
||||
&.is-large
|
||||
font-size: $size-medium
|
||||
.icon
|
||||
&:first-child:not(:last-child)
|
||||
+ltr-property("margin", -0.375em, false)
|
||||
+ltr-property("margin", 0.1875em)
|
||||
&:last-child:not(:first-child)
|
||||
+ltr-property("margin", 0.1875em, false)
|
||||
+ltr-property("margin", -0.375em)
|
||||
&:first-child:last-child
|
||||
+ltr-property("margin", -0.375em, false)
|
||||
+ltr-property("margin", -0.375em)
|
||||
// Modifiers
|
||||
&.is-delete
|
||||
+ltr-property("margin", $tag-delete-margin, false)
|
||||
padding: 0
|
||||
position: relative
|
||||
width: 2em
|
||||
&::before,
|
||||
&::after
|
||||
background-color: currentColor
|
||||
content: ""
|
||||
display: block
|
||||
left: 50%
|
||||
position: absolute
|
||||
top: 50%
|
||||
transform: translateX(-50%) translateY(-50%) rotate(45deg)
|
||||
transform-origin: center center
|
||||
&::before
|
||||
height: 1px
|
||||
width: 50%
|
||||
&::after
|
||||
height: 50%
|
||||
width: 1px
|
||||
&:hover,
|
||||
&:focus
|
||||
background-color: darken($tag-background-color, 5%)
|
||||
&:active
|
||||
background-color: darken($tag-background-color, 10%)
|
||||
&.is-rounded
|
||||
border-radius: $radius-rounded
|
||||
|
||||
a.tag
|
||||
&:hover
|
||||
text-decoration: underline
|
@ -1,70 +0,0 @@
|
||||
$title-color: $text-strong !default
|
||||
$title-family: false !default
|
||||
$title-size: $size-3 !default
|
||||
$title-weight: $weight-semibold !default
|
||||
$title-line-height: 1.125 !default
|
||||
$title-strong-color: inherit !default
|
||||
$title-strong-weight: inherit !default
|
||||
$title-sub-size: 0.75em !default
|
||||
$title-sup-size: 0.75em !default
|
||||
|
||||
$subtitle-color: $text !default
|
||||
$subtitle-family: false !default
|
||||
$subtitle-size: $size-5 !default
|
||||
$subtitle-weight: $weight-normal !default
|
||||
$subtitle-line-height: 1.25 !default
|
||||
$subtitle-strong-color: $text-strong !default
|
||||
$subtitle-strong-weight: $weight-semibold !default
|
||||
$subtitle-negative-margin: -1.25rem !default
|
||||
|
||||
.title,
|
||||
.subtitle
|
||||
@extend %block
|
||||
word-break: break-word
|
||||
em,
|
||||
span
|
||||
font-weight: inherit
|
||||
sub
|
||||
font-size: $title-sub-size
|
||||
sup
|
||||
font-size: $title-sup-size
|
||||
.tag
|
||||
vertical-align: middle
|
||||
|
||||
.title
|
||||
color: $title-color
|
||||
@if $title-family
|
||||
font-family: $title-family
|
||||
font-size: $title-size
|
||||
font-weight: $title-weight
|
||||
line-height: $title-line-height
|
||||
strong
|
||||
color: $title-strong-color
|
||||
font-weight: $title-strong-weight
|
||||
& + .highlight
|
||||
margin-top: -0.75rem
|
||||
&:not(.is-spaced) + .subtitle
|
||||
margin-top: $subtitle-negative-margin
|
||||
// Sizes
|
||||
@each $size in $sizes
|
||||
$i: index($sizes, $size)
|
||||
&.is-#{$i}
|
||||
font-size: $size
|
||||
|
||||
.subtitle
|
||||
color: $subtitle-color
|
||||
@if $subtitle-family
|
||||
font-family: $subtitle-family
|
||||
font-size: $subtitle-size
|
||||
font-weight: $subtitle-weight
|
||||
line-height: $subtitle-line-height
|
||||
strong
|
||||
color: $subtitle-strong-color
|
||||
font-weight: $subtitle-strong-weight
|
||||
&:not(.is-spaced) + .title
|
||||
margin-top: $subtitle-negative-margin
|
||||
// Sizes
|
||||
@each $size in $sizes
|
||||
$i: index($sizes, $size)
|
||||
&.is-#{$i}
|
||||
font-size: $size
|
@ -1,9 +0,0 @@
|
||||
/* Bulma Form */
|
||||
@charset "utf-8"
|
||||
|
||||
@import "shared.sass"
|
||||
@import "input-textarea.sass"
|
||||
@import "checkbox-radio.sass"
|
||||
@import "select.sass"
|
||||
@import "file.sass"
|
||||
@import "tools.sass"
|
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