Compare commits
306 commits
feature-12
...
master
Author | SHA1 | Date | |
---|---|---|---|
Midgard | 3cde7764c4 | ||
73671bd8f1 | |||
45b4913657 | |||
7b78e7d8ff | |||
a29d3a33be | |||
7fad75fc08 | |||
5a82354b78 | |||
fbb69c843a | |||
30626e457a | |||
0aea3f6d34 | |||
cdca5646ef | |||
8b1b3f482a | |||
2d6aea10fb | |||
6e79fc50ed | |||
ba29ecbc73 | |||
1ffcdc3ec1 | |||
6bb11e49a3 | |||
aab522eef9 | |||
e86fce0a7e | |||
02afba70a9 | |||
1bc6a5931e | |||
c991cd7882 | |||
a29ade4773 | |||
6f7aff15cc | |||
7b12c266b3 | |||
7d122cf6e9 | |||
202d5d3e7a | |||
28fa1b7592 | |||
bf8eb94117 | |||
b14671413c | |||
29afc8db7a | |||
1dcd723bd4 | |||
c0f44ab037 | |||
4e8799eca5 | |||
e302da0335 | |||
c839fce270 | |||
687d389fa2 | |||
9c4361ab1b | |||
754eae4a50 | |||
f3911b377d | |||
3bc2ad83ea | |||
0661016236 | |||
10327941d2 | |||
5d204a4012 | |||
2bdd07c9af | |||
978b432d7e | |||
426357f00d | |||
Midgard | 5306561ddd | ||
01b5c72e7b | |||
Midgard | 4a353ec17e | ||
8f3750060b | |||
bb49fb2795 | |||
28a6dc5422 | |||
Midgard | 453cacebd9 | ||
Midgard | 44feb1a4ff | ||
c04d9bbd44 | |||
4d9d43b0f0 | |||
0a0d13c0dc | |||
2c4a288d4e | |||
da1a708e28 | |||
d6d9d61f27 | |||
a077a8038a | |||
1c0d78f2ee | |||
bbb38aa825 | |||
c43efa4b10 | |||
8a2b9247e1 | |||
25e2757461 | |||
ab47c0a882 | |||
f87f3c5446 | |||
a33c76f84b | |||
98214f8b84 | |||
7702fdecbe | |||
0e0771bae1 | |||
749012140b | |||
Midgard | b5202a9de6 | ||
Midgard | 7b16a3b6c5 | ||
Midgard | 03f1e56161 | ||
Midgard | cf27a7de8a | ||
Midgard | a568103a60 | ||
Midgard | 85d8892176 | ||
Midgard | c35d107502 | ||
dfbf1de5a1 | |||
fec9d660c3 | |||
9c00fcc0cf | |||
2271b0427c | |||
Midgard | 5a9d9c1d31 | ||
Midgard | ced04acb2e | ||
Midgard | 09e2d704cd | ||
Midgard | fc630e9061 | ||
841c3d5fb8 | |||
da88d807d1 | |||
cc0c271a22 | |||
0ace54a8fd | |||
ae77adc54e | |||
e93460743a | |||
fb3e7b95f1 | |||
d59ad9abba | |||
492d1ca91c | |||
461664f629 | |||
781e4cd45b | |||
5e29f2a5f7 | |||
1cdd22c1c0 | |||
33d2fe3b52 | |||
2123d7d1a3 | |||
ba1b37f5fe | |||
8350e57cc8 | |||
70491f3e5b | |||
c5e9067d59 | |||
ee65544031 | |||
b573841e49 | |||
4924f22b48 | |||
1f7ddcdb33 | |||
36b55c5171 | |||
c5da63b23e | |||
9e3578425f | |||
Midgard | 4cbc4473de | ||
Midgard | 326b5d6b5f | ||
Midgard | 02a82621b7 | ||
Midgard | adcc0dd69e | ||
Midgard | 4a96c0c286 | ||
Midgard | e82742eb62 | ||
Midgard | 16bd9243d4 | ||
Midgard | 1ff8ceb521 | ||
Midgard | 58d270e582 | ||
Midgard | 00cdc587b0 | ||
90ea53d20e | |||
Midgard | cc9343ccbc | ||
Midgard | 65d6de21fa | ||
ca2b682dec | |||
fad23fbeda | |||
ec7c8ed40b | |||
f5a8303362 | |||
Midgard | 787669823c | ||
Midgard | 695331dd1a | ||
Midgard | 342e94a7bf | ||
Midgard | d3e6c1b08c | ||
Midgard | c23a11bfdb | ||
Midgard | 8cefb48768 | ||
Midgard | 17b3bc1c7a | ||
Midgard | 837dc682bc | ||
Midgard | 6a052fae73 | ||
Midgard | 9641435b64 | ||
Midgard | 0ab0824913 | ||
Midgard | 7856302028 | ||
Midgard | a841be3c48 | ||
Midgard | d1fcea7391 | ||
Midgard | 0b93307f1f | ||
Midgard | 6651a8d513 | ||
Midgard | 5a24b934fd | ||
Midgard | 8ccc508747 | ||
Midgard | f516ffe8c6 | ||
Midgard | 1bea3d2fcc | ||
Midgard | 48d67cbd89 | ||
Midgard | f49952b4f2 | ||
Midgard | 63ce1c5551 | ||
Midgard | 65ed818875 | ||
Midgard | b25705250d | ||
Midgard | 0ee1a0ea12 | ||
Midgard | 52f85c420f | ||
b1f3d786e8 | |||
Midgard | cd67eab29b | ||
Midgard | 7464ee4ea4 | ||
Midgard | f6753172ec | ||
Midgard | aa1cd97773 | ||
Midgard | c61b323f8c | ||
Midgard | 84957f3175 | ||
Midgard | 65fa6441b1 | ||
Midgard | 67df1cebff | ||
Midgard | 5e944755ee | ||
Midgard | 2d3ae10f90 | ||
Midgard | 8a01e74323 | ||
Midgard | dc1596ee71 | ||
Midgard | ff57afca9a | ||
Midgard | 4f7ffc0e3d | ||
Midgard | 588ffdadfb | ||
Midgard | 42ac9031e6 | ||
Midgard | fd5b432837 | ||
Midgard | d904f3c562 | ||
Midgard | 6282eed349 | ||
Midgard | 5d6db78e6e | ||
Midgard | 323a24ece6 | ||
Midgard | 1fa38d08c6 | ||
Midgard | 289b36b918 | ||
Midgard | aba8301758 | ||
Midgard | 0f9a816eea | ||
Midgard | 54dc1f23bf | ||
Midgard | b8eb40e448 | ||
Midgard | f60c1d180c | ||
Midgard | e434aad3b8 | ||
Midgard | 648f8da63b | ||
Midgard | c52ba3b6d9 | ||
Midgard | a860998858 | ||
Midgard | 79c0056ff0 | ||
Midgard | 45a110db23 | ||
Midgard | f2112ec26d | ||
Midgard | 076fb9880e | ||
Midgard | 21f4903bfd | ||
Midgard | e013127ed7 | ||
Midgard | 13097b8156 | ||
Midgard | 04a1f0acb1 | ||
Midgard | b44d34003d | ||
Midgard | 172d5d1e08 | ||
Midgard | 8aa50d1bd9 | ||
Midgard | 649b3d4ac5 | ||
Midgard | aa331b6843 | ||
Midgard | b7552ce080 | ||
Midgard | 11fc7093f3 | ||
Midgard | 8e414afe45 | ||
Midgard | bba3a0cd3a | ||
Midgard | cb8d640502 | ||
Midgard | 2b4b706e94 | ||
Midgard | d9685b00d9 | ||
Midgard | 729dab2ba4 | ||
Midgard | e15b085006 | ||
Midgard | ccb034ef7f | ||
Midgard | 02cea73a6c | ||
Midgard | 2892a7dc25 | ||
Midgard | 36bdee3689 | ||
Midgard | 1246284f9b | ||
Midgard | 0bdb17277f | ||
Midgard | 646858aac3 | ||
Midgard | a62e2bd9f7 | ||
Midgard | d911cb573b | ||
Midgard | 38e10257d2 | ||
Midgard | 57d65661de | ||
ea06fe72e2 | |||
825521867e | |||
Midgard | 5c08ecca1b | ||
midgard | aa139d0d21 | ||
Midgard | 841f98bd51 | ||
Midgard | e54a89e12c | ||
5c20226b59 | |||
b9fc1f2754 | |||
Midgard | 0bfcf658db | ||
Midgard | 2b962e99d4 | ||
Midgard | 6495184af2 | ||
Midgard | b8ac141310 | ||
Midgard | 94d4a403cb | ||
Midgard | 13560b04cd | ||
Midgard | 1025ade758 | ||
Midgard | 513e495665 | ||
Midgard | f900c85931 | ||
Midgard | 91e76c2b45 | ||
Midgard | 04fb06d187 | ||
Midgard | 85f36a8fe4 | ||
Midgard | 5049382bf1 | ||
Midgard | 2db02b9326 | ||
Midgard | 6768a35808 | ||
Midgard | 0e779e59b9 | ||
Midgard | aa63023383 | ||
Midgard | 8ed38f178c | ||
Midgard | 6f24b52855 | ||
Midgard | 0e7675cec6 | ||
Midgard | 161a5031e5 | ||
Midgard | 3380d790bc | ||
Midgard | b7cc2048d6 | ||
Midgard | ad9b55fa4b | ||
Midgard | fb9b0d7960 | ||
Midgard | b1d0c3e004 | ||
Midgard | 40170046c5 | ||
Midgard | 7e476c39dc | ||
Midgard | 285d4223e2 | ||
Midgard | 4b99eff7c8 | ||
Midgard | 2313f0bcb5 | ||
Midgard | ecaff42ba4 | ||
Midgard | 6d7baa31ea | ||
Midgard | d5bb14d3a5 | ||
Midgard | 238c841239 | ||
Midgard | 9494886965 | ||
Midgard | a346220896 | ||
Midgard | fe593fece6 | ||
Midgard | 798e08d74b | ||
Midgard | 9588116ba7 | ||
Midgard | e46d5e622e | ||
Midgard | ecb0550fdd | ||
Midgard | d564808417 | ||
Midgard | f2c6ad9c89 | ||
Midgard | 996444e1b0 | ||
Midgard | 22aa5a0fb0 | ||
Midgard | 5fcac5f937 | ||
Midgard | 1d9e7cc170 | ||
Midgard | 37e3799985 | ||
Midgard | edb1677523 | ||
Midgard | 1bfbbfde6d | ||
Midgard | 5f7d36f0fa | ||
Midgard | b459dbc9b7 | ||
Midgard | 230e336b2b | ||
Midgard | f77a61cf87 | ||
Midgard | 303349e376 | ||
Midgard | e6b707e3d7 | ||
Midgard | 8d1e95e5b4 | ||
Midgard | ca264b90e1 | ||
Midgard | 50d9b624d9 | ||
Midgard | f62b105abd | ||
Midgard | a2847eab5d | ||
fe88e456a7 | |||
Midgard | 18897af4e3 | ||
Midgard | 9f2edb2f81 | ||
Midgard | a3af7476c7 | ||
Midgard | 894f9f0016 | ||
Midgard | 5fc39416cc | ||
Midgard | ce16d068bc | ||
Midgard | eaad17b2b0 | ||
Midgard | 2e6ea44095 | ||
Midgard | 0d7b43705b | ||
midgard | ca199f4a3b |
10
.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# Include source, config and scripts
|
||||||
|
!app
|
||||||
|
!etc
|
||||||
|
!*.md
|
||||||
|
!*.sh
|
||||||
|
!*.txt
|
||||||
|
!LICENSE
|
20
.editorconfig
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# https://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.{py,py.*}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.el]
|
||||||
|
indent_style = space
|
5
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
||||||
|
menus/
|
||||||
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
@ -65,6 +68,7 @@ app/config.py
|
||||||
|
|
||||||
# Do not add database file
|
# Do not add database file
|
||||||
*.db
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
|
@ -72,4 +76,5 @@ tmp/
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
# Do not add scss cache
|
# Do not add scss cache
|
||||||
|
.sass-cache
|
||||||
*.sass-cache
|
*.sass-cache
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
# run arbitrary code.
|
# run arbitrary code.
|
||||||
extension-pkg-whitelist=
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
|
fail-under=9.58
|
||||||
|
|
||||||
# Add files or directories to the blacklist. They should be base names, not
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
# paths.
|
# paths.
|
||||||
ignore=CVS
|
ignore=CVS
|
||||||
|
@ -28,7 +30,7 @@ limit-inference-results=100
|
||||||
|
|
||||||
# List of plugins (as comma separated values of python modules names) to load,
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
# usually to register additional checkers.
|
# usually to register additional checkers.
|
||||||
load-plugins=
|
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||||
|
|
||||||
# Pickle collected data for later comparisons.
|
# Pickle collected data for later comparisons.
|
||||||
persistent=yes
|
persistent=yes
|
||||||
|
@ -60,7 +62,7 @@ confidence=
|
||||||
# --enable=similarities". If you want to run only the classes checker, but have
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||||
# --disable=W".
|
# --disable=W".
|
||||||
disable=E0401,E0611,C0103,W0511,W0611
|
disable=E0401,E0611,C0103,W0511,W0611,C0415
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3.5.3
|
3.9.2
|
||||||
|
|
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
python 3.9.2
|
26
Dockerfile
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM python:3.9.2-slim AS development
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
RUN pip install pymysql
|
||||||
|
|
||||||
|
ADD https://git.zeus.gent/haldis/menus/-/archive/master/menus-master.tar /tmp
|
||||||
|
RUN mkdir menus && \
|
||||||
|
tar --directory=menus --extract --strip-components=1 --file=/tmp/menus-master.tar
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
WORKDIR /src/app
|
||||||
|
CMD python app.py db upgrade && \
|
||||||
|
python app.py runserver -h 0.0.0.0 -p 8000
|
||||||
|
|
||||||
|
FROM development AS production
|
||||||
|
|
||||||
|
RUN pip install waitress
|
||||||
|
|
||||||
|
CMD python app.py db upgrade && \
|
||||||
|
python waitress_wsgi.py
|
673
LICENSE
|
@ -1,3 +1,676 @@
|
||||||
|
This software is licensed under the GNU Affero Public License.
|
||||||
|
Previous versions were licensed under the MIT license, which you can find at the bottom of this
|
||||||
|
document.
|
||||||
|
|
||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
A previous version of Haldis, upon which this one is based, is available under the MIT license.
|
||||||
|
Portions of this codebase remain in the current Haldis version.
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Zeus WPI
|
Copyright (c) 2015 Zeus WPI
|
||||||
|
|
12
README.md
|
@ -14,6 +14,7 @@ Be lazier today!
|
||||||
## Local setup
|
## Local setup
|
||||||
|
|
||||||
There is a special script to get started with the project. Just run it in the root of the project.
|
There is a special script to get started with the project. Just run it in the root of the project.
|
||||||
|
Note: this script might require you to install a certain python version, you can do this using your favorite tool e.g. [pyenv](https://github.com/pyenv/pyenv#simple-python-version-management-pyenv)
|
||||||
|
|
||||||
./first-setup.sh
|
./first-setup.sh
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ Afterwards upgrade the database to the latest version using
|
||||||
cd app
|
cd app
|
||||||
python3 app.py db upgrade
|
python3 app.py db upgrade
|
||||||
|
|
||||||
You can now still seed the database by running
|
You can now still seed the database by running, note that you might want to put your name in the `HALDIS_ADMINS` in `app/config.py`
|
||||||
|
|
||||||
./populate-db.sh
|
./populate-db.sh
|
||||||
|
|
||||||
|
@ -61,6 +62,11 @@ Run `pip-compile --upgrade`
|
||||||
|
|
||||||
For more information about managing the dependencies see [jazzband/pip-tools: A set of tools to keep your pinned Python dependencies fresh.](https://github.com/jazzband/pip-tools)
|
For more information about managing the dependencies see [jazzband/pip-tools: A set of tools to keep your pinned Python dependencies fresh.](https://github.com/jazzband/pip-tools)
|
||||||
|
|
||||||
## Authors
|
## Production
|
||||||
|
To prepare the application in a production environment, follow the same steps as for *Local setup* up to and including `./populate-db.sh`.
|
||||||
|
|
||||||
* **Feliciaan De Palmenaer** - *Initial work* - [Github](https://github.com/feliciaan)
|
Set DEBUG to False in `app/config.py`.
|
||||||
|
|
||||||
|
See [Flask's deployment documentation](https://flask.palletsprojects.com/en/1.1.x/deploying/#self-hosted-options).
|
||||||
|
|
||||||
|
Set the server's Python interpreter to `/path/to/haldis/venv/bin/python`. Doing `source venv/bin/activate` is not necessary when that binary is used.
|
||||||
|
|
0
app/__init__.py
Normal file
14
app/add_admins.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""Script for adding users as admin to Haldis."""
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from models import User
|
||||||
|
from config import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
def add() -> None:
|
||||||
|
"""Add users as admin."""
|
||||||
|
for username in Configuration.HALDIS_ADMINS:
|
||||||
|
user = User()
|
||||||
|
user.configure(username, True, 0, associations=["zeus"])
|
||||||
|
db.session.add(user)
|
77
app/admin.py
|
@ -1,52 +1,75 @@
|
||||||
"Haldis admin related views and models"
|
"Module for everything related to Admin users"
|
||||||
|
|
||||||
import flask_login as login
|
import flask_login as login
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_admin import Admin
|
from flask_admin import Admin
|
||||||
from flask_admin.contrib.sqla import ModelView
|
from flask_admin.contrib.sqla import ModelView
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from models import Order, OrderItem, OrderItemChoice, User
|
||||||
from models import Location, Order, OrderItem, Product, User
|
|
||||||
|
|
||||||
|
|
||||||
class ModelBaseView(ModelView):
|
class ModelBaseView(ModelView):
|
||||||
"Base model for admin related things"
|
"Class for the base view of the model"
|
||||||
# pylint: disable=R0201, R0903
|
# pylint: disable=too-few-public-methods, no-self-use
|
||||||
def is_accessible(self) -> bool:
|
def is_accessible(self) -> bool:
|
||||||
"Check if the user has admin permission"
|
"Function to check if the logged in user is an admin"
|
||||||
if login.current_user.is_anonymous():
|
|
||||||
return False
|
|
||||||
return login.current_user.is_admin()
|
return login.current_user.is_admin()
|
||||||
|
|
||||||
|
|
||||||
class UserAdminModel(ModelBaseView):
|
class UserAdminModel(ModelBaseView):
|
||||||
"Model for user admin"
|
"Class for the model of a UserAdmin"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=too-few-public-methods
|
||||||
column_searchable_list = ("username",)
|
column_searchable_list = ("username",)
|
||||||
|
column_editable_list = ("username",)
|
||||||
|
column_default_sort = "username"
|
||||||
inline_models = None
|
inline_models = None
|
||||||
|
|
||||||
|
|
||||||
class ProductAdminModel(ModelBaseView):
|
class OrderAdminModel(ModelBaseView):
|
||||||
"Model for product admin"
|
"Class for the model of a OrderAdmin"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=too-few-public-methods
|
||||||
column_searchable_list = ("name",)
|
column_default_sort = ("starttime", True)
|
||||||
inline_models = None
|
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier", "association"]
|
||||||
|
column_labels = {
|
||||||
|
"starttime": "Start Time",
|
||||||
|
"stoptime": "Closing Time",
|
||||||
|
"location_id": "HLDS Location ID",
|
||||||
|
"association": "Association",
|
||||||
|
}
|
||||||
|
form_excluded_columns = ["items", "courier_id"]
|
||||||
|
can_delete = False
|
||||||
|
|
||||||
|
|
||||||
class LocationAdminModel(ModelBaseView):
|
class OrderItemAdminModel(ModelBaseView):
|
||||||
"Model for location admin"
|
"Class for the model of a OrderItemAdmin"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=too-few-public-methods
|
||||||
column_searchable_list = ("name", "address", "website")
|
column_default_sort = ("order_id", True)
|
||||||
inline_models = None
|
column_list = [
|
||||||
form_columns = ("name", "address", "website", "telephone")
|
"order_id",
|
||||||
|
"slug",
|
||||||
|
"order.location_name",
|
||||||
|
"user_name",
|
||||||
|
"user",
|
||||||
|
"dish_name",
|
||||||
|
"dish_id",
|
||||||
|
"comment",
|
||||||
|
"price",
|
||||||
|
"paid",
|
||||||
|
"hlds_data_version",
|
||||||
|
]
|
||||||
|
column_labels = {
|
||||||
|
"order_id": "Order",
|
||||||
|
"order.location_name": "Order's Location",
|
||||||
|
"user_name": "Anon. User",
|
||||||
|
"user_id": "Registered User",
|
||||||
|
"hlds_data_version": "HLDS Data Version",
|
||||||
|
"dish_id": "HLDS Dish ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def init_admin(app: Flask, database: SQLAlchemy) -> None:
|
def init_admin(app: Flask, database: SQLAlchemy) -> None:
|
||||||
"Initialize the admin related things in the app."
|
"Register admin views with Flask app."
|
||||||
admin = Admin(app, name="Haldis", url="/admin", template_mode="bootstrap3")
|
admin = Admin(app, name="Haldis", url="/admin", template_mode="bootstrap3")
|
||||||
|
|
||||||
admin.add_view(UserAdminModel(User, database.session))
|
admin.add_view(UserAdminModel(User, database.session))
|
||||||
admin.add_view(LocationAdminModel(Location, database.session))
|
admin.add_view(OrderAdminModel(Order, database.session))
|
||||||
admin.add_view(ProductAdminModel(Product, database.session))
|
admin.add_view(OrderItemAdminModel(OrderItem, database.session))
|
||||||
admin.add_view(ModelBaseView(Order, database.session))
|
|
||||||
admin.add_view(ModelBaseView(OrderItem, database.session))
|
|
||||||
|
|
164
app/app.py
Normal file → Executable file
|
@ -1,46 +1,36 @@
|
||||||
"Main Haldis script"
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Main Haldis script"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
import sentry_sdk
|
||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from airbrake import Airbrake, AirbrakeHandler
|
from admin import init_admin
|
||||||
from flask import Flask, render_template
|
from config import Configuration
|
||||||
|
from flask import Flask, render_template, Response
|
||||||
from flask_bootstrap import Bootstrap, StaticCDN
|
from flask_bootstrap import Bootstrap, StaticCDN
|
||||||
from flask_debugtoolbar import DebugToolbarExtension
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_migrate import Migrate, MigrateCommand
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
from flask_oauthlib.client import OAuth, OAuthException
|
|
||||||
from flask_script import Manager, Server
|
from flask_script import Manager, Server
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
from admin import init_admin
|
from admin import init_admin
|
||||||
from login import init_login
|
from auth.login import init_login
|
||||||
|
from auth.zeus import init_oauth
|
||||||
|
from config import Configuration
|
||||||
from models import db
|
from models import db
|
||||||
from models.anonymous_user import AnonymouseUser
|
from models.anonymous_user import AnonymouseUser
|
||||||
from utils import euro_string
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
from zeus import init_oauth
|
from utils import euro_string, price_range_string, ignore_none
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> Manager:
|
|
||||||
"Create the Haldis application"
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Load the config file
|
|
||||||
app.config.from_object("config.Configuration")
|
|
||||||
|
|
||||||
app_manager = register_plugins(app)
|
|
||||||
add_handlers(app)
|
|
||||||
add_routes(app)
|
|
||||||
add_template_filters(app)
|
|
||||||
|
|
||||||
# TODO do we need to return and then run the manager?
|
|
||||||
return app_manager
|
|
||||||
|
|
||||||
|
|
||||||
def register_plugins(app: Flask) -> Manager:
|
def register_plugins(app: Flask) -> Manager:
|
||||||
"Register all the plugins to Haldis"
|
"""Register the plugins to the app"""
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
# Register Airbrake and enable the logrotation
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
timedFileHandler = TimedRotatingFileHandler(
|
timedFileHandler = TimedRotatingFileHandler(
|
||||||
app.config["LOGFILE"], when="midnight", backupCount=100
|
app.config["LOGFILE"], when="midnight", backupCount=100
|
||||||
|
@ -52,19 +42,6 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
loglogger.addHandler(timedFileHandler)
|
loglogger.addHandler(timedFileHandler)
|
||||||
app.logger.addHandler(timedFileHandler)
|
app.logger.addHandler(timedFileHandler)
|
||||||
|
|
||||||
airbrakelogger = logging.getLogger("airbrake")
|
|
||||||
|
|
||||||
# Airbrake
|
|
||||||
airbrake = Airbrake(project_id=app.config["AIRBRAKE_ID"],
|
|
||||||
api_key=app.config["AIRBRAKE_KEY"])
|
|
||||||
# ugly hack to make this work for out errbit
|
|
||||||
airbrake._api_url = "http://errbit.awesomepeople.tv/api/v3/projects/{}/notices".format( # pylint: disable=W0212
|
|
||||||
airbrake.project_id
|
|
||||||
)
|
|
||||||
|
|
||||||
airbrakelogger.addHandler(AirbrakeHandler(airbrake=airbrake))
|
|
||||||
app.logger.addHandler(AirbrakeHandler(airbrake=airbrake))
|
|
||||||
|
|
||||||
# Initialize SQLAlchemy
|
# Initialize SQLAlchemy
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
@ -73,7 +50,6 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
app_manager = Manager(app)
|
app_manager = Manager(app)
|
||||||
app_manager.add_command("db", MigrateCommand)
|
app_manager.add_command("db", MigrateCommand)
|
||||||
app_manager.add_command("runserver", Server(port=8000))
|
app_manager.add_command("runserver", Server(port=8000))
|
||||||
# Add admin interface
|
|
||||||
init_admin(app, db)
|
init_admin(app, db)
|
||||||
|
|
||||||
# Init login manager
|
# Init login manager
|
||||||
|
@ -99,7 +75,7 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
# Make cookies more secure
|
# Make cookies more secure
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
SESSION_COOKIE_SAMESITE="Lax",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
|
@ -109,7 +85,8 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
|
|
||||||
|
|
||||||
def add_handlers(app: Flask) -> None:
|
def add_handlers(app: Flask) -> None:
|
||||||
"Add handlers for 4xx error codes"
|
"""Add handlers for 4xx error codes"""
|
||||||
|
|
||||||
# pylint: disable=W0612,W0613
|
# pylint: disable=W0612,W0613
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def handle404(e) -> typing.Tuple[str, int]:
|
def handle404(e) -> typing.Tuple[str, int]:
|
||||||
|
@ -121,56 +98,103 @@ def add_handlers(app: Flask) -> None:
|
||||||
|
|
||||||
|
|
||||||
def add_routes(application: Flask) -> None:
|
def add_routes(application: Flask) -> None:
|
||||||
"Add all routes to Haldis"
|
"""Add all routes to Haldis"""
|
||||||
# import views # TODO convert to blueprint
|
# import views # TODO convert to blueprint
|
||||||
# import views.stats # TODO convert to blueprint
|
# import views.stats # TODO convert to blueprint
|
||||||
|
|
||||||
from views.order import order_bp
|
from auth.login import auth_bp
|
||||||
from views.general import general_bp
|
from auth.microsoft import auth_microsoft_bp
|
||||||
from views.stats import stats_blueprint
|
from auth.zeus import auth_zeus_bp
|
||||||
from views.debug import debug_bp
|
from views.debug import debug_bp
|
||||||
from login import auth_bp
|
from views.general import general_bp
|
||||||
from zeus import oauth_bp
|
from views.order import order_bp
|
||||||
|
from views.stats import stats_blueprint
|
||||||
|
|
||||||
application.register_blueprint(general_bp, url_prefix="/")
|
application.register_blueprint(general_bp, url_prefix="/")
|
||||||
application.register_blueprint(order_bp, url_prefix="/order")
|
application.register_blueprint(order_bp, url_prefix="/order")
|
||||||
application.register_blueprint(stats_blueprint, url_prefix="/stats")
|
application.register_blueprint(stats_blueprint, url_prefix="/stats")
|
||||||
application.register_blueprint(auth_bp, url_prefix="/")
|
application.register_blueprint(auth_bp, url_prefix="/")
|
||||||
application.register_blueprint(oauth_bp, url_prefix="/")
|
if Configuration.ENABLE_MICROSOFT_AUTH:
|
||||||
|
application.register_blueprint(auth_microsoft_bp,
|
||||||
|
url_prefix="/users/auth/microsoft_graph_auth") # "/auth/microsoft")
|
||||||
|
application.register_blueprint(auth_zeus_bp, url_prefix="/auth/zeus")
|
||||||
|
|
||||||
if application.debug:
|
if application.debug:
|
||||||
application.register_blueprint(debug_bp, url_prefix="/debug")
|
application.register_blueprint(debug_bp, url_prefix="/debug")
|
||||||
|
|
||||||
|
|
||||||
def add_template_filters(app: Flask) -> None:
|
def add_template_filters(app: Flask) -> None:
|
||||||
"Add functions which can be used in the templates"
|
"""Add functions which can be used in the templates"""
|
||||||
|
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
@app.template_filter("countdown")
|
@app.template_filter("countdown")
|
||||||
def countdown(value, only_positive: bool = True,
|
def countdown(
|
||||||
show_text: bool = True) -> str:
|
value, only_positive: bool = True, show_text: bool = True, reload: bool = True
|
||||||
"A function which returns time until the order is done"
|
) -> str:
|
||||||
delta = value - datetime.now()
|
delta = int(value.timestamp() - datetime.now().timestamp())
|
||||||
if delta.total_seconds() < 0 and only_positive:
|
if delta < 0 and only_positive:
|
||||||
return "closed"
|
text = "closed"
|
||||||
hours, remainder = divmod(delta.seconds, 3600)
|
else:
|
||||||
minutes, seconds = divmod(remainder, 60)
|
carry, seconds = divmod(delta, 60)
|
||||||
time = "%02d:%02d:%02d" % (hours, minutes, seconds)
|
carry, minutes = divmod(carry, 60)
|
||||||
if show_text:
|
days, hours = divmod(carry, 24)
|
||||||
return f"{time} left"
|
|
||||||
return time
|
days_text = f"{days} days, " if days else ""
|
||||||
|
|
||||||
|
appendix = " left" if show_text else ""
|
||||||
|
text = f"{days_text}{hours:02d}:{minutes:02d}:{seconds:02d}{appendix}"
|
||||||
|
|
||||||
|
reload_str = "yes" if reload else "no"
|
||||||
|
|
||||||
|
return Markup(
|
||||||
|
f"<span class='time' data-seconds='{delta}' data-reload='{reload_str}'>"
|
||||||
|
+ text
|
||||||
|
+ "</span>"
|
||||||
|
)
|
||||||
|
|
||||||
@app.template_filter("year")
|
@app.template_filter("year")
|
||||||
def current_year(value: typing.Any) -> str: # pylint: disable=W0613
|
def current_year(_value: typing.Any) -> str:
|
||||||
"A function which returns the current year"
|
|
||||||
return str(datetime.now().year)
|
return str(datetime.now().year)
|
||||||
|
|
||||||
@app.template_filter("euro")
|
app.template_filter("euro")(euro_string)
|
||||||
def euro(value: int) -> str:
|
app.template_filter("price_range")(price_range_string)
|
||||||
"A function which converts a value to its euro_string"
|
app.template_filter("any")(any)
|
||||||
return euro_string(value)
|
app.template_filter("all")(all)
|
||||||
|
app.template_filter("ignore_none")(ignore_none)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
"""Initializer for the Flask app object"""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/robots.txt')
|
||||||
|
def noindex():
|
||||||
|
r = Response(response="User-Agent: *\nDisallow: /\n", status=200, mimetype="text/plain")
|
||||||
|
r.headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
return r
|
||||||
|
|
||||||
|
# Load the config file
|
||||||
|
app.config.from_object("config.Configuration")
|
||||||
|
|
||||||
|
app_manager = register_plugins(app)
|
||||||
|
add_handlers(app)
|
||||||
|
add_routes(app)
|
||||||
|
add_template_filters(app)
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_config():
|
||||||
|
return dict(configuration=Configuration)
|
||||||
|
|
||||||
|
return app, app_manager
|
||||||
|
|
||||||
|
|
||||||
# For usage when you directly call the script with python
|
# For usage when you directly call the script with python
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
manager = create_app()
|
if Configuration.SENTRY_DSN:
|
||||||
manager.run()
|
sentry_sdk.init(
|
||||||
|
dsn=Configuration.SENTRY_DSN,
|
||||||
|
integrations=[FlaskIntegration()]
|
||||||
|
)
|
||||||
|
|
||||||
|
app, app_mgr = create_app()
|
||||||
|
app_mgr.run()
|
||||||
|
|
|
@ -1,32 +1,25 @@
|
||||||
"Script for everything related to logging in and out"
|
"""Script for everything related to logging in and out"""
|
||||||
from flask import Blueprint, abort, redirect, session, url_for
|
from flask import Blueprint, abort, redirect, session, url_for
|
||||||
from flask_login import current_user, logout_user
|
from flask_login import current_user, logout_user
|
||||||
from werkzeug.wrappers import Response
|
|
||||||
|
|
||||||
from models import User
|
from models import User
|
||||||
from zeus import zeus_login
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
auth_bp = Blueprint("auth_bp", __name__)
|
auth_bp = Blueprint("auth_bp", __name__)
|
||||||
|
|
||||||
|
|
||||||
def init_login(app) -> None:
|
def init_login(app) -> None:
|
||||||
"Initialize the login"
|
"""Initialize the login"""
|
||||||
|
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
@app.login_manager.user_loader
|
@app.login_manager.user_loader
|
||||||
def load_user(userid) -> User:
|
def load_user(userid) -> User:
|
||||||
"Load the user"
|
"""Load the user"""
|
||||||
return User.query.filter_by(id=userid).first()
|
return User.query.filter_by(id=userid).first()
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/login")
|
|
||||||
def login():
|
|
||||||
"Function to handle a user trying to log in"
|
|
||||||
return zeus_login()
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout")
|
||||||
def logout() -> Response:
|
def logout() -> Response:
|
||||||
"Function to handle a user trying to log out"
|
"""Function to handle a user trying to log out"""
|
||||||
if "zeus_token" in session:
|
if "zeus_token" in session:
|
||||||
session.pop("zeus_token", None)
|
session.pop("zeus_token", None)
|
||||||
logout_user()
|
logout_user()
|
||||||
|
@ -34,6 +27,6 @@ def logout() -> Response:
|
||||||
|
|
||||||
|
|
||||||
def before_request() -> None:
|
def before_request() -> None:
|
||||||
"Function for what has to be done before a request"
|
"""Function for what has to be done before a request"""
|
||||||
if current_user.is_anonymous() or not current_user.is_allowed():
|
if current_user.is_anonymous() or not current_user.is_allowed():
|
||||||
abort(401)
|
abort(401)
|
77
app/auth/microsoft.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from flask import Blueprint, url_for, request, redirect, flash, Response
|
||||||
|
from flask_login import login_user
|
||||||
|
from microsoftgraph.client import Client
|
||||||
|
|
||||||
|
from config import Configuration
|
||||||
|
from models import User, db
|
||||||
|
|
||||||
|
auth_microsoft_bp = Blueprint("auth_microsoft_bp", __name__)
|
||||||
|
|
||||||
|
client = Client(Configuration.MICROSOFT_AUTH_ID,
|
||||||
|
Configuration.MICROSOFT_AUTH_SECRET,
|
||||||
|
account_type="ugentbe.onmicrosoft.com")
|
||||||
|
|
||||||
|
|
||||||
|
def microsoft_login():
|
||||||
|
"""Log in using Microsoft"""
|
||||||
|
scope = ["openid", "profile", "User.Read", "User.Read.All"]
|
||||||
|
url = client.authorization_url(url_for("auth_microsoft_bp.authorized", _external=True), scope, state=None)
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_microsoft_bp.route("/login")
|
||||||
|
def login():
|
||||||
|
"""Function to handle a user trying to log in"""
|
||||||
|
return microsoft_login()
|
||||||
|
|
||||||
|
|
||||||
|
@auth_microsoft_bp.route("callback") # "/authorized")
|
||||||
|
def authorized() -> typing.Any:
|
||||||
|
# type is 'typing.Union[str, Response]', but this errors due to
|
||||||
|
# https://github.com/python/mypy/issues/7187
|
||||||
|
"""Check authorized status"""
|
||||||
|
|
||||||
|
oauth_code = request.args['code']
|
||||||
|
|
||||||
|
resp = client.exchange_code(url_for("auth_microsoft_bp.authorized", _external=True), oauth_code)
|
||||||
|
client.set_token(resp.data)
|
||||||
|
|
||||||
|
resp = client.users.get_me()
|
||||||
|
microsoft_uuid = resp.data['id']
|
||||||
|
username = resp.data['userPrincipalName']
|
||||||
|
|
||||||
|
# Fail if fields are not populated
|
||||||
|
if not microsoft_uuid or not username:
|
||||||
|
flash("You're not allowed to enter, please contact a system administrator")
|
||||||
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
# Find existing user by Microsoft UUID (userPrincipalName can change)
|
||||||
|
user = User.query.filter_by(microsoft_uuid=microsoft_uuid).first()
|
||||||
|
if user:
|
||||||
|
return login_and_redirect_user(user)
|
||||||
|
|
||||||
|
# Find existing user by username (pre-existing account)
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user:
|
||||||
|
return login_and_redirect_user(user)
|
||||||
|
|
||||||
|
# No user found, create a new one
|
||||||
|
user = create_user(username, microsoft_uuid=microsoft_uuid)
|
||||||
|
return login_and_redirect_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
def login_and_redirect_user(user) -> Response:
|
||||||
|
"""Log in the user and then redirect them"""
|
||||||
|
login_user(user)
|
||||||
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username, *, microsoft_uuid) -> User:
|
||||||
|
"""Create a temporary user if it is needed"""
|
||||||
|
user = User()
|
||||||
|
user.configure(username, False, 1, microsoft_uuid=microsoft_uuid)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
|
@ -4,32 +4,34 @@ import typing
|
||||||
from flask import (Blueprint, current_app, flash, redirect, request, session,
|
from flask import (Blueprint, current_app, flash, redirect, request, session,
|
||||||
url_for)
|
url_for)
|
||||||
from flask_login import login_user
|
from flask_login import login_user
|
||||||
from flask_oauthlib.client import OAuth, OAuthException
|
from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp
|
||||||
|
from models import User, db
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
from models import User, db
|
auth_zeus_bp = Blueprint("auth_zeus_bp", __name__)
|
||||||
|
|
||||||
oauth_bp = Blueprint("oauth_bp", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
def zeus_login():
|
def zeus_login():
|
||||||
"Log in using ZeusWPI"
|
"""Log in using ZeusWPI"""
|
||||||
return current_app.zeus.authorize(
|
return current_app.zeus.authorize(
|
||||||
callback=url_for("oauth_bp.authorized", _external=True)
|
callback=url_for("auth_zeus_bp.authorized", _external=True))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@oauth_bp.route("/login/zeus/authorized")
|
@auth_zeus_bp.route("/login")
|
||||||
|
def login():
|
||||||
|
"""Function to handle a user trying to log in"""
|
||||||
|
return zeus_login()
|
||||||
|
|
||||||
|
|
||||||
|
@auth_zeus_bp.route("/authorized")
|
||||||
def authorized() -> typing.Any:
|
def authorized() -> typing.Any:
|
||||||
# type is 'typing.Union[str, Response]', but this errors due to
|
# type is 'typing.Union[str, Response]', but this errors due to
|
||||||
# https://github.com/python/mypy/issues/7187
|
# https://github.com/python/mypy/issues/7187
|
||||||
"Check authorized status"
|
"""Check authorized status"""
|
||||||
resp = current_app.zeus.authorized_response()
|
resp = current_app.zeus.authorized_response()
|
||||||
if resp is None:
|
if resp is None:
|
||||||
return "Access denied: reason=%s error=%s" % (
|
# pylint: disable=C0301
|
||||||
request.args["error"],
|
return f"Access denied: reason={request.args['error']} error={request.args['error_description']}"
|
||||||
request.args["error_description"],
|
|
||||||
)
|
|
||||||
if isinstance(resp, OAuthException):
|
if isinstance(resp, OAuthException):
|
||||||
return f"Access denied: {resp.message}<br>{resp.data}"
|
return f"Access denied: {resp.message}<br>{resp.data}"
|
||||||
|
|
||||||
|
@ -49,8 +51,8 @@ def authorized() -> typing.Any:
|
||||||
return redirect(url_for("general_bp.home"))
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
|
||||||
def init_oauth(app):
|
def init_oauth(app) -> OAuthRemoteApp:
|
||||||
"Initialize the OAuth for ZeusWPI"
|
"""Initialize the OAuth for ZeusWPI"""
|
||||||
oauth = OAuth(app)
|
oauth = OAuth(app)
|
||||||
|
|
||||||
zeus = oauth.remote_app(
|
zeus = oauth.remote_app(
|
||||||
|
@ -73,15 +75,15 @@ def init_oauth(app):
|
||||||
|
|
||||||
|
|
||||||
def login_and_redirect_user(user) -> Response:
|
def login_and_redirect_user(user) -> Response:
|
||||||
"Log in the user and then redirect them"
|
"""Log in the user and then redirect them"""
|
||||||
login_user(user)
|
login_user(user)
|
||||||
return redirect(url_for("general_bp.home"))
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
|
||||||
def create_user(username) -> User:
|
def create_user(username) -> User:
|
||||||
"Create a temporary user if it is needed"
|
"""Create a temporary user if it is needed"""
|
||||||
user = User()
|
user = User()
|
||||||
user.configure(username, False, 1)
|
user.configure(username, False, 1, associations=["zeus"])
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
|
@ -1,17 +1,26 @@
|
||||||
"An example for a Haldis config"
|
"""An example for a Haldis config"""
|
||||||
# config
|
# import os
|
||||||
|
|
||||||
|
|
||||||
class Configuration():
|
class Configuration:
|
||||||
"Haldis configuration object"
|
"Haldis configuration object"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=too-few-public-methods
|
||||||
SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.db"
|
SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.db"
|
||||||
|
# MARIADB_HOST = os.environ.get("MARIADB_HOST")
|
||||||
|
# MARIADB_DB = os.environ.get("MARIADB_DATABASE")
|
||||||
|
# MARIADB_USER = os.environ.get("MARIADB_USER")
|
||||||
|
# MARIADB_PASS = os.environ.get("MARIADB_PASSWORD")
|
||||||
|
# SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MARIADB_USER}:{MARIADB_PASS}@{MARIADB_HOST}/{MARIADB_DB}"
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
HALDIS_ADMINS = []
|
||||||
SECRET_KEY = "<change>"
|
SECRET_KEY = "<change>"
|
||||||
SLACK_WEBHOOK = None
|
SLACK_WEBHOOK = None
|
||||||
LOGFILE = "haldis.log"
|
LOGFILE = "haldis.log"
|
||||||
|
SENTRY_DSN = None
|
||||||
ZEUS_KEY = "tomtest"
|
ZEUS_KEY = "tomtest"
|
||||||
ZEUS_SECRET = "blargh"
|
ZEUS_SECRET = "blargh"
|
||||||
AIRBRAKE_ID = ""
|
|
||||||
AIRBRAKE_KEY = ""
|
ENABLE_MICROSOFT_AUTH = False
|
||||||
|
MICROSOFT_AUTH_ID = ""
|
||||||
|
MICROSOFT_AUTH_SECRET = ""
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
"Script for interaction and changes to the database"
|
"""Script for interaction and changes to the database"""
|
||||||
|
|
||||||
import add_admins
|
import add_admins
|
||||||
import add_fitchen
|
|
||||||
import add_oceans_garden
|
from app import create_app, db
|
||||||
import add_primadonna
|
|
||||||
import add_simpizza
|
app, app_manager = create_app()
|
||||||
import add_testlocation
|
|
||||||
from app import db, create_app
|
|
||||||
|
|
||||||
entry_sets = {
|
entry_sets = {
|
||||||
"Admins": add_admins.add,
|
"admins": add_admins.add,
|
||||||
"Testlocation": add_testlocation.add,
|
|
||||||
"Ocean's Garden": add_oceans_garden.add,
|
|
||||||
"SimPizza": add_simpizza.add,
|
|
||||||
"Primadonna": add_primadonna.add,
|
|
||||||
"Fitchen": add_fitchen.add,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yes = ["yes", "y"]
|
yes = ["yes", "y"]
|
||||||
|
@ -21,13 +15,13 @@ no = ["no", "n"]
|
||||||
|
|
||||||
|
|
||||||
def commit() -> None:
|
def commit() -> None:
|
||||||
"Commit all the things to the database"
|
"""Commit all the things to the database"""
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Committing successful")
|
print("Committing successful")
|
||||||
|
|
||||||
|
|
||||||
def check_if_overwrite() -> bool:
|
def check_if_overwrite() -> bool:
|
||||||
"Check if the user wants to overwrite the previous database"
|
"""Check if the user wants to overwrite the previous database"""
|
||||||
answer = input("Do you want to overwrite the previous database? (y/N) ")
|
answer = input("Do you want to overwrite the previous database? (y/N) ")
|
||||||
return answer.lower() in yes
|
return answer.lower() in yes
|
||||||
|
|
||||||
|
@ -35,36 +29,32 @@ def check_if_overwrite() -> bool:
|
||||||
def add_all() -> None:
|
def add_all() -> None:
|
||||||
"Add all possible entries in the entry_sets to the database"
|
"Add all possible entries in the entry_sets to the database"
|
||||||
for entry_set, function in entry_sets.items():
|
for entry_set, function in entry_sets.items():
|
||||||
print("Adding {}.".format(entry_set))
|
print(f"Adding {entry_set}.")
|
||||||
function()
|
function()
|
||||||
|
|
||||||
|
|
||||||
def recreate_from_scratch() -> None:
|
def recreate_from_scratch() -> None:
|
||||||
"Recreate a completely new database"
|
"""Recreate a completely new database"""
|
||||||
confirmation = "Are you very very sure? (Will delete previous entries!) (y/N) "
|
|
||||||
if input(confirmation) in yes:
|
|
||||||
print("Resetting the database!")
|
print("Resetting the database!")
|
||||||
db.drop_all()
|
db.drop_all()
|
||||||
db.create_all()
|
db.create_all()
|
||||||
add_to_current()
|
add_to_current()
|
||||||
else:
|
|
||||||
print("You cancelled.")
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_current() -> None:
|
def add_to_current() -> None:
|
||||||
"Add things to the current database"
|
"""Add things to the current database"""
|
||||||
available = [entry_set for entry_set in entry_sets]
|
available = list(entry_sets)
|
||||||
|
|
||||||
def add_numbers() -> str:
|
def add_numbers() -> str:
|
||||||
return " ".join(
|
return " ".join(
|
||||||
["{}({}), ".format(loc, i) for i, loc in enumerate(available)]
|
[f"{loc}({i}), " for i, loc in enumerate(available)]
|
||||||
).rstrip(", ")
|
).rstrip(", ")
|
||||||
|
|
||||||
while input("Do you still want to add something? (Y/n) ").lower() not in no:
|
while input("Do you still want to add something? (Y/n) ").lower() not in no:
|
||||||
print(
|
print(
|
||||||
"What do you want to add? (Use numbers, or A for all, or C for cancel) "
|
"What do you want to add? (Use numbers, or A for all, or C for cancel) "
|
||||||
)
|
)
|
||||||
answer = input("Available: {} : ".format(add_numbers()))
|
answer = input(f"Available: {add_numbers()} : ")
|
||||||
if answer.lower() == "a":
|
if answer.lower() == "a":
|
||||||
add_all()
|
add_all()
|
||||||
available = []
|
available = []
|
||||||
|
@ -72,25 +62,24 @@ def add_to_current() -> None:
|
||||||
pass
|
pass
|
||||||
elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]:
|
elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]:
|
||||||
answer_index = int(answer)
|
answer_index = int(answer)
|
||||||
print("Adding {}.".format(available[answer_index]))
|
print(f"Adding {available[answer_index]}.")
|
||||||
entry_sets[str(available[answer_index])]()
|
entry_sets[str(available[answer_index])]()
|
||||||
del available[answer_index]
|
del available[answer_index]
|
||||||
else:
|
else:
|
||||||
print("Not a valid answer.")
|
print("Not a valid answer.")
|
||||||
print("Thank you for adding, come again!")
|
print("Thank you for adding, come again!")
|
||||||
|
|
||||||
manager = create_app()
|
|
||||||
|
|
||||||
@manager.command
|
@app_manager.command
|
||||||
def setup_database(): # type: None
|
def setup_database(): # type: None
|
||||||
"Start the database interaction script"
|
"""Start the database interaction script"""
|
||||||
print("Database modification script!")
|
print("Database modification script!")
|
||||||
print("=============================\n\n")
|
print("=============================\n\n")
|
||||||
if check_if_overwrite():
|
if (not db.engine.table_names()) or check_if_overwrite():
|
||||||
recreate_from_scratch()
|
recreate_from_scratch()
|
||||||
else:
|
else:
|
||||||
add_to_current()
|
add_to_current()
|
||||||
commit()
|
commit()
|
||||||
|
|
||||||
|
|
||||||
manager.run()
|
app_manager.run()
|
|
@ -1,24 +0,0 @@
|
||||||
"Script for adding users as admin to Haldis."
|
|
||||||
from app import db
|
|
||||||
from models import User
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add users as admin."
|
|
||||||
feli = User()
|
|
||||||
feli.configure("feliciaan", True, 0)
|
|
||||||
db.session.add(feli)
|
|
||||||
|
|
||||||
destro = User()
|
|
||||||
destro.configure("destro", True, 0)
|
|
||||||
db.session.add(destro)
|
|
||||||
|
|
||||||
iepoev = User()
|
|
||||||
iepoev.configure("iepoev", True, 1)
|
|
||||||
db.session.add(iepoev)
|
|
||||||
|
|
||||||
flynn = User()
|
|
||||||
flynn.configure("flynn", True, 0)
|
|
||||||
db.session.add(flynn)
|
|
||||||
|
|
||||||
# To future developers, add yourself here
|
|
|
@ -1,38 +0,0 @@
|
||||||
"Script to add Fitchen to Haldis"
|
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
menuitems = [
|
|
||||||
"Spicy Chicken",
|
|
||||||
"Advocado Chick",
|
|
||||||
"Indian Summer",
|
|
||||||
"Olive Garden",
|
|
||||||
"Advocado Spring",
|
|
||||||
"Spicy Mexican",
|
|
||||||
"Beefcake",
|
|
||||||
"Iron Man",
|
|
||||||
"Fitalian",
|
|
||||||
"Captain",
|
|
||||||
"Sea Breeze",
|
|
||||||
"Vegan Market",
|
|
||||||
"Sunset Beach",
|
|
||||||
"Hot Tofu",
|
|
||||||
"Vegan Advocado Spring",
|
|
||||||
]
|
|
||||||
|
|
||||||
pricedict = {"Small": 799, "Medium": 999, "Large": 1199}
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add Fitchen to the database"
|
|
||||||
fitchen = Location()
|
|
||||||
fitchen.configure("Fitchen", "?", "?", "https://www.fitchen.be/")
|
|
||||||
db.session.add(fitchen)
|
|
||||||
|
|
||||||
for menuitem in menuitems:
|
|
||||||
for size, price in pricedict.items():
|
|
||||||
for container in ["bowl", "wrap"]:
|
|
||||||
name = "%s %s in %s" % (size, menuitem, container)
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(fitchen, name, price)
|
|
||||||
db.session.add(entry)
|
|
|
@ -1,56 +0,0 @@
|
||||||
"Script to add Ocean Garden to Haldis"
|
|
||||||
from itertools import product
|
|
||||||
|
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
zetmelen = ["Nasi", "Bami"]
|
|
||||||
vlezen = ["Rundsvlees", "Varkensvlees", "Kippenstukkjes"]
|
|
||||||
sauzen = [
|
|
||||||
"Balisaus",
|
|
||||||
"Yu siang saus",
|
|
||||||
"Gon boa saus",
|
|
||||||
"Curry saus",
|
|
||||||
"Oestersaus",
|
|
||||||
"Zwarte pepersaus",
|
|
||||||
"Champignons",
|
|
||||||
"Chinese champignons",
|
|
||||||
"A la Maleisïe",
|
|
||||||
]
|
|
||||||
|
|
||||||
specials = [
|
|
||||||
"Nasi Kippenbolletjes Zoetzuur",
|
|
||||||
"Bami Kippenbolletjes Zoetzuur",
|
|
||||||
"Nasi Varkenbolletjes Zoetzuur",
|
|
||||||
"Bami Varkenbolletjes Zoetzuur",
|
|
||||||
"Nasi Babi Pangang",
|
|
||||||
"Bami Babi Pangang",
|
|
||||||
"Diverse Groenten met Bami",
|
|
||||||
"Diverse Groenten met Nasi",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add Ocean Garden to the database"
|
|
||||||
chinees = Location()
|
|
||||||
chinees.configure(
|
|
||||||
"Oceans's Garden",
|
|
||||||
"Zwijnaardsesteenweg 399 9000 Gent",
|
|
||||||
"tel: 09/222.72.74",
|
|
||||||
"http://oceangarden.byethost3.com/studentenmenus.html",
|
|
||||||
)
|
|
||||||
db.session.add(chinees)
|
|
||||||
|
|
||||||
def chinees_create_entry(name) -> None:
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(chinees, name, 550)
|
|
||||||
db.session.add(entry)
|
|
||||||
|
|
||||||
def chinees_create_regulat(zetmeel, vlees="", saus="") -> None:
|
|
||||||
chinees_create_entry("{} {} {}".format(zetmeel, vlees, saus).rstrip())
|
|
||||||
|
|
||||||
for z, v, s in product(zetmelen, vlezen, sauzen):
|
|
||||||
chinees_create_regulat(z, v, s)
|
|
||||||
|
|
||||||
for special in specials:
|
|
||||||
chinees_create_entry(special)
|
|
|
@ -1,121 +0,0 @@
|
||||||
"Script to add Primadonna to Haldis"
|
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
|
|
||||||
def add():
|
|
||||||
"Add Primadonna to the database"
|
|
||||||
addTA()
|
|
||||||
addAfhalen()
|
|
||||||
|
|
||||||
|
|
||||||
pizzasTA = {
|
|
||||||
"Peperoni": 750,
|
|
||||||
"Basis pizza (extra garneringen zie site)": 600,
|
|
||||||
"Parma": 750,
|
|
||||||
"Margharita": 600,
|
|
||||||
"Funghi": 715,
|
|
||||||
"Mamma mia": 715,
|
|
||||||
"Napoletana": 750,
|
|
||||||
"Exotic": 750,
|
|
||||||
"Siciliana": 750,
|
|
||||||
"Michelangelo": 750,
|
|
||||||
"Roma": 750,
|
|
||||||
"Torno": 750,
|
|
||||||
"Bolognese": 780,
|
|
||||||
"Hawai": 910,
|
|
||||||
"Cipolla": 910,
|
|
||||||
"Dolce vita": 910,
|
|
||||||
"Valentino": 910,
|
|
||||||
"Vegateriana": 1000,
|
|
||||||
"La donna": 1000,
|
|
||||||
"Tropical": 1000,
|
|
||||||
"Quattro Stagioni": 1000,
|
|
||||||
"Romana": 1000,
|
|
||||||
"Diabolo": 1000,
|
|
||||||
"Turkish": 1000,
|
|
||||||
"Cesar": 1000,
|
|
||||||
"Calzone": 1040,
|
|
||||||
"Calzone Vegetariana": 1040,
|
|
||||||
"Quattro Formaggi": 1040,
|
|
||||||
"Frutti di mare": 1040,
|
|
||||||
"Gerookte ham en rucola": 1040,
|
|
||||||
"Van de chef": 1170,
|
|
||||||
"Milano": 1170,
|
|
||||||
"Soronto": 1260,
|
|
||||||
"Primma Donna": 1260,
|
|
||||||
"Pasta (zie site voor opties)": 900,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def addTA() -> None:
|
|
||||||
"Add Primadonna on takeaway.com to the database"
|
|
||||||
primadonna_takeaway = Location()
|
|
||||||
primadonna_takeaway.configure(
|
|
||||||
"Primadonna (takeaway laten bezorgen)",
|
|
||||||
"Overpoortstraat 46 9000 Gent",
|
|
||||||
"tel: 0475 40 13 00",
|
|
||||||
"https://www.takeaway.com/be-en/prima-donna",
|
|
||||||
)
|
|
||||||
db.session.add(primadonna_takeaway)
|
|
||||||
|
|
||||||
for pizza, price in pizzasTA.items():
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(primadonna_takeaway, pizza, price)
|
|
||||||
db.session.add(entry)
|
|
||||||
|
|
||||||
|
|
||||||
pizzasAfhalen = {
|
|
||||||
"Peperoni": 575,
|
|
||||||
"Basis pizza (extra garneringen zie site)": 450,
|
|
||||||
"Parma": 575,
|
|
||||||
"Margharita": 450,
|
|
||||||
"Funghi": 550,
|
|
||||||
"Mamma mia": 550,
|
|
||||||
"Napoletana": 575,
|
|
||||||
"Exotic": 575,
|
|
||||||
"Siciliana": 575,
|
|
||||||
"Michelangelo": 575,
|
|
||||||
"Roma": 575,
|
|
||||||
"Torno": 575,
|
|
||||||
"Bolognese": 600,
|
|
||||||
"Hawai": 700,
|
|
||||||
"Cipolla": 700,
|
|
||||||
"Dolce vita": 700,
|
|
||||||
"Valentino": 700,
|
|
||||||
"Vegateriana": 770,
|
|
||||||
"La donna": 770,
|
|
||||||
"Tropical": 770,
|
|
||||||
"Quattro Stagioni": 770,
|
|
||||||
"Romana": 770,
|
|
||||||
"Diabolo": 770,
|
|
||||||
"Turkish": 770,
|
|
||||||
"Cesar": 770,
|
|
||||||
"Calzone": 800,
|
|
||||||
"Calzone Vegetariana": 800,
|
|
||||||
"Quattro Formaggi": 800,
|
|
||||||
"Frutti di mare": 800,
|
|
||||||
"Gerookte ham en rucola": 800,
|
|
||||||
"Van de chef": 900,
|
|
||||||
"Milano": 900,
|
|
||||||
"Soronto": 970,
|
|
||||||
"Primma Donna": 970,
|
|
||||||
"Pasta (zie site voor opties)": 700,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def addAfhalen() -> None:
|
|
||||||
"Add Primadonna to takeaway to the database"
|
|
||||||
primadonna_afhalen = Location()
|
|
||||||
primadonna_afhalen.configure(
|
|
||||||
"Primadonna (bellen en afhalen)",
|
|
||||||
"Overpoortstraat 46 9000 Gent",
|
|
||||||
"tel: 0475 40 13 00",
|
|
||||||
"http://primadonnagent.be/Menu.html",
|
|
||||||
)
|
|
||||||
db.session.add(primadonna_afhalen)
|
|
||||||
|
|
||||||
for pizza, price in pizzasAfhalen.items():
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(primadonna_afhalen, pizza, price)
|
|
||||||
db.session.add(entry)
|
|
|
@ -1,51 +0,0 @@
|
||||||
"Script to add SimPizza to Haldis"
|
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
pizzas = [
|
|
||||||
"Bolognese de luxe",
|
|
||||||
"Hawaï",
|
|
||||||
"Popeye",
|
|
||||||
"Pepperoni",
|
|
||||||
"Seafood",
|
|
||||||
"Hot pizzaaah!!!",
|
|
||||||
"Salmon delight",
|
|
||||||
"Full option",
|
|
||||||
"Pitza kebab",
|
|
||||||
"Multi cheese",
|
|
||||||
"4 Seasons",
|
|
||||||
"Mega fish",
|
|
||||||
"Creamy multi cheese",
|
|
||||||
"Green fiësta",
|
|
||||||
"Chicken bbq",
|
|
||||||
"Funky chicken",
|
|
||||||
"Veggie",
|
|
||||||
"Meat lovers",
|
|
||||||
"Scampi mampi",
|
|
||||||
"Tabasco",
|
|
||||||
"Chicken time",
|
|
||||||
"Meatballs",
|
|
||||||
"Tuna",
|
|
||||||
"Anchovy",
|
|
||||||
"Calzone",
|
|
||||||
"Bbq meatballs",
|
|
||||||
"Creamy chicken",
|
|
||||||
"Hot bolognese",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add Simpizza to the database"
|
|
||||||
simpizza = Location()
|
|
||||||
simpizza.configure(
|
|
||||||
"Sim-pizza",
|
|
||||||
"De Pintelaan 252 9000 Gent",
|
|
||||||
"tel: 09/321.02.00",
|
|
||||||
"http://simpizza.be",
|
|
||||||
)
|
|
||||||
db.session.add(simpizza)
|
|
||||||
|
|
||||||
for pizza in pizzas:
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(simpizza, pizza, 1195)
|
|
||||||
db.session.add(entry)
|
|
|
@ -1,139 +0,0 @@
|
||||||
"Script to add Stefanos to Haldis"
|
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
bickies = {
|
|
||||||
"Bicky Burger Original": 330,
|
|
||||||
"Bicky Burger": 300,
|
|
||||||
"Bicky Glenniei": 330,
|
|
||||||
"Bicky Capoentje": 330,
|
|
||||||
"Bicky Chicken": 350,
|
|
||||||
"Bicky Fish": 350,
|
|
||||||
"Bicky Veggie": 350,
|
|
||||||
}
|
|
||||||
|
|
||||||
sauskes = {
|
|
||||||
"american": 70,
|
|
||||||
"andalouse": 70,
|
|
||||||
"bicky saus": 70,
|
|
||||||
"cocktail": 70,
|
|
||||||
"curryketchu": 70,
|
|
||||||
"gele curry saus": 70,
|
|
||||||
"hannibal": 70,
|
|
||||||
"jamballa": 70,
|
|
||||||
"joppie": 70,
|
|
||||||
"loempiasaus": 70,
|
|
||||||
"looksaus": 70,
|
|
||||||
"mammout saus": 70,
|
|
||||||
"mayo": 70,
|
|
||||||
"mosterd": 70,
|
|
||||||
"pepersaus": 70,
|
|
||||||
"pickles": 70,
|
|
||||||
"pili-pili saus": 70,
|
|
||||||
"samurai": 70,
|
|
||||||
"tartare": 70,
|
|
||||||
"ketchup": 70,
|
|
||||||
"toscanse saus": 70,
|
|
||||||
"zoete mayo": 70,
|
|
||||||
"stoverijsaus": 130,
|
|
||||||
"special op vlees": 80,
|
|
||||||
"speciaal op friet": 160,
|
|
||||||
}
|
|
||||||
|
|
||||||
special_bickies = {
|
|
||||||
"Bicky Yellow": 400,
|
|
||||||
"Bicky Hermes": 500,
|
|
||||||
"Bicky Grand Cru": 530,
|
|
||||||
"Bicky Royal": 600,
|
|
||||||
"Bicky Wrap": 400,
|
|
||||||
"Bicky Rib": 450,
|
|
||||||
"Lloydje/Plankske": 600,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
specials = {
|
|
||||||
"Julientje": 650,
|
|
||||||
"Julientje Dubbel": 800,
|
|
||||||
"Veggie Julientje": 700,
|
|
||||||
"Veggie Julientje Dubbel": 800,
|
|
||||||
"Rombautje": 700,
|
|
||||||
"Rombautje Dubbel": 900,
|
|
||||||
"Bolleke": 650,
|
|
||||||
"Bolleke Dubbel": 800,
|
|
||||||
"Hendrik": 700,
|
|
||||||
"Hendrik Dubbel": 900,
|
|
||||||
"Lieveke": 700,
|
|
||||||
"Molleke": 850,
|
|
||||||
"Molleke Dubbel": 1200,
|
|
||||||
"Stefano": 650,
|
|
||||||
"Stefano Dubbel": 800,
|
|
||||||
"Picasso": 1350,
|
|
||||||
}
|
|
||||||
|
|
||||||
vlezekes = {
|
|
||||||
"Ardeense sate": 350,
|
|
||||||
"Bamischijf": 200,
|
|
||||||
"5 Bitterballen": 150,
|
|
||||||
"Jagerworst": 300,
|
|
||||||
"Boulet": 200,
|
|
||||||
"Chixfingers": 350,
|
|
||||||
"Chicken Nuggets": 350,
|
|
||||||
"Crizzly Pikant": 350,
|
|
||||||
"Frikandel": 100,
|
|
||||||
"Garnaalballetjes": 350,
|
|
||||||
"Garnaalkroket": 300,
|
|
||||||
"Kaasballetjes": 250,
|
|
||||||
"Kaaskroket": 100,
|
|
||||||
"Kipcorn": 200,
|
|
||||||
"Kipsate": 400,
|
|
||||||
"Loempia met kip": 350,
|
|
||||||
"Lookworst": 300,
|
|
||||||
"Merguez": 350,
|
|
||||||
"Mexicano": 200,
|
|
||||||
"Mini Loempia's met saus": 300,
|
|
||||||
"Mini Lucifers": 300,
|
|
||||||
"Ragouzi": 250,
|
|
||||||
"stoofvlees": 450,
|
|
||||||
}
|
|
||||||
|
|
||||||
friet = {"Klein pak": 200, "Midden pak": 250, "Groot pak": 300}
|
|
||||||
|
|
||||||
data = [special_bickies, specials, vlezekes, friet]
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add Stefanos to the database"
|
|
||||||
stefanos = Location()
|
|
||||||
stefanos.configure(
|
|
||||||
"Stefano's Place",
|
|
||||||
"Overpoortstraat 12 9000 Gent",
|
|
||||||
"tel: geen",
|
|
||||||
"https://www.facebook.com/pages/category/Fast-Food-Restaurant/Stefanos-Place-370774480004139/", # pylint: disable=C0301
|
|
||||||
)
|
|
||||||
db.session.add(stefanos)
|
|
||||||
|
|
||||||
# sommige bickies kunde met een schel kaas bestellen
|
|
||||||
for name, price in bickies.items():
|
|
||||||
bicky = Product()
|
|
||||||
bicky.configure(stefanos, name, price)
|
|
||||||
db.session.add(bicky)
|
|
||||||
|
|
||||||
bicky_cheese = Product()
|
|
||||||
bicky_cheese.configure(stefanos, name + " cheese", price + 30)
|
|
||||||
db.session.add(bicky_cheese)
|
|
||||||
|
|
||||||
for dictionary in data:
|
|
||||||
for name, price in dictionary.items():
|
|
||||||
item = Product()
|
|
||||||
item.configure(stefanos, name, price)
|
|
||||||
db.session.add(item)
|
|
||||||
|
|
||||||
# saus in een potteke bestellen is 10 cent extra
|
|
||||||
for name, price in sauskes.items():
|
|
||||||
saus = Product()
|
|
||||||
saus.configure(stefanos, name, price)
|
|
||||||
db.session.add(saus)
|
|
||||||
|
|
||||||
saus_apart = Product()
|
|
||||||
saus_apart.configure(stefanos, name + " apart", price + 10)
|
|
||||||
db.session.add(saus_apart)
|
|
|
@ -1,24 +0,0 @@
|
||||||
from app import db
|
|
||||||
from models import Location, Product
|
|
||||||
|
|
||||||
STUFFS = [
|
|
||||||
("Broodje zever", 540),
|
|
||||||
("Broodje aap", 0),
|
|
||||||
("Broodje goud", 500000),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
testlocation = Location()
|
|
||||||
testlocation.configure(
|
|
||||||
"Testlocation",
|
|
||||||
"Please ignore!",
|
|
||||||
"0469 69 69 69",
|
|
||||||
"http://localhost:8000/",
|
|
||||||
)
|
|
||||||
db.session.add(testlocation)
|
|
||||||
|
|
||||||
for stuff in STUFFS:
|
|
||||||
entry = Product()
|
|
||||||
entry.configure(testlocation, *stuff)
|
|
||||||
db.session.add(entry)
|
|
|
@ -1,19 +0,0 @@
|
||||||
ORDERS
|
|
||||||
=======
|
|
||||||
OrderID | User | LocatieID | Starttijdstip | Eindtijstip | Comment
|
|
||||||
|
|
||||||
Item
|
|
||||||
=====
|
|
||||||
ItemID | OrderID | User | FoodID
|
|
||||||
|
|
||||||
Locaties
|
|
||||||
======
|
|
||||||
LocatieID | Naam | Adres | Coordinaten(2.0) | Website
|
|
||||||
|
|
||||||
LocatieMetaData
|
|
||||||
===============
|
|
||||||
LocatieID | Key | Value
|
|
||||||
|
|
||||||
Food
|
|
||||||
=====
|
|
||||||
FoodID | LocatieID | Naam | Prijs
|
|
38
app/database/muhscheme.txt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
This is just a description of the database schema. It's not generated automatically, nor is it used
|
||||||
|
to automatically generate anything. For the latest version, check the files in app/models/
|
||||||
|
|
||||||
|
|
||||||
|
user
|
||||||
|
username
|
||||||
|
is_admin
|
||||||
|
bias
|
||||||
|
|
||||||
|
order
|
||||||
|
id
|
||||||
|
slug secret used in URL
|
||||||
|
courier_id
|
||||||
|
location_id HLDS identifier
|
||||||
|
location_name this allows historical orders to keep the same location name
|
||||||
|
starttime
|
||||||
|
stoptime
|
||||||
|
public
|
||||||
|
|
||||||
|
order_item
|
||||||
|
id
|
||||||
|
order_id
|
||||||
|
user_id
|
||||||
|
user_name for users who are not logged in
|
||||||
|
dish_id HLDS identifier
|
||||||
|
dish_name ) this allows historical orders to keep their correct name and price
|
||||||
|
price )
|
||||||
|
paid
|
||||||
|
comment
|
||||||
|
hlds_data_version Git commit hash to identify HLDS data version
|
||||||
|
|
||||||
|
order_item_choice
|
||||||
|
id
|
||||||
|
order_item_id
|
||||||
|
choice_id HLDS identifier
|
||||||
|
kind single_choice/multi_choice
|
||||||
|
name
|
||||||
|
value just a textual description of the chosen values
|
|
@ -1,25 +1,42 @@
|
||||||
|
"Module used for everything related to the fat versions of models"
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from hlds.definitions import location_definitions
|
||||||
|
from hlds.models import Dish, Location
|
||||||
|
from models import Order, OrderItem, User
|
||||||
from sqlalchemy.sql import desc, func
|
from sqlalchemy.sql import desc, func
|
||||||
|
|
||||||
from models import Location, Order, OrderItem, Product, User
|
|
||||||
|
|
||||||
|
|
||||||
class FatModel:
|
class FatModel:
|
||||||
|
"General class for the fat version of models"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
|
"Function to query all"
|
||||||
|
# pylint: disable=E1101
|
||||||
return cls.query.all()
|
return cls.query.all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def amount(cls):
|
def amount(cls):
|
||||||
|
"Function to query the amount"
|
||||||
|
# pylint: disable=E1101
|
||||||
return cls.query.count()
|
return cls.query.count()
|
||||||
|
|
||||||
|
|
||||||
class FatLocation(Location, FatModel):
|
class FatLocation(Location, FatModel):
|
||||||
pass
|
"Fat version of the Location model"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls):
|
||||||
|
return location_definitions
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def amount(cls):
|
||||||
|
return len(location_definitions)
|
||||||
|
|
||||||
|
|
||||||
class FatOrder(Order, FatModel):
|
class FatOrder(Order, FatModel):
|
||||||
|
"Fat version of the Order model"
|
||||||
|
|
||||||
# It's hard to add the unique user constraint,
|
# It's hard to add the unique user constraint,
|
||||||
# as DISTINCT seems to apply after a GROUP BY and aggregate
|
# as DISTINCT seems to apply after a GROUP BY and aggregate
|
||||||
|
@ -27,34 +44,15 @@ class FatOrder(Order, FatModel):
|
||||||
# even if they get reduced by the disctinct afterwards.
|
# even if they get reduced by the disctinct afterwards.
|
||||||
@classmethod
|
@classmethod
|
||||||
def items_per_order(cls):
|
def items_per_order(cls):
|
||||||
return (
|
"Function to get the total of all items per order"
|
||||||
Order.query.join(OrderItem).group_by(Order.id)
|
return (Order.query.join(OrderItem).group_by(Order.id).with_entities(
|
||||||
.with_entities(Order.id,
|
Order.id,
|
||||||
func.count(OrderItem.user_id).label("total"))
|
func.count(OrderItem.user_id).label("total")))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FatUser(User, FatModel):
|
class FatUser(User, FatModel):
|
||||||
pass
|
"Fat version of the User model"
|
||||||
|
|
||||||
|
|
||||||
class FatOrderItem(OrderItem, FatModel):
|
class FatOrderItem(OrderItem, FatModel):
|
||||||
pass
|
"Fat version of the OrderItem model"
|
||||||
|
|
||||||
|
|
||||||
class FatProduct(Product, FatModel):
|
|
||||||
@classmethod
|
|
||||||
def top4(cls) -> None:
|
|
||||||
top4 = (
|
|
||||||
OrderItem.query.join(Product)
|
|
||||||
.join(Location)
|
|
||||||
.group_by(Product.id)
|
|
||||||
.with_entities(
|
|
||||||
Product.name, Location.name, func.count(
|
|
||||||
Product.id).label("count")
|
|
||||||
)
|
|
||||||
.order_by(desc("count"))
|
|
||||||
.limit(4)
|
|
||||||
)
|
|
||||||
for top in top4:
|
|
||||||
print(top)
|
|
||||||
|
|
68
app/forms.py
|
@ -1,60 +1,64 @@
|
||||||
"Script for everything form related in Haldis"
|
"Script for everything form related in Haldis"
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import session
|
from flask import request, session
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_wtf import FlaskForm as Form
|
from flask_wtf import FlaskForm as Form
|
||||||
from wtforms import (DateTimeField, SelectField, StringField, SubmitField,
|
from hlds.definitions import location_definitions
|
||||||
validators)
|
from hlds.models import Choice, Dish, Location
|
||||||
|
from models import User
|
||||||
from models import Location, User
|
from utils import euro_string, price_range_string
|
||||||
from utils import euro_string
|
from wtforms import (DateTimeField, FieldList, SelectField,
|
||||||
|
SelectMultipleField, StringField, SubmitField, validators)
|
||||||
|
|
||||||
|
|
||||||
class OrderForm(Form):
|
class OrderForm(Form):
|
||||||
"Class which defines the form for a new Order"
|
"Class which defines the form for a new Order"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=R0903
|
||||||
courrier_id = SelectField("Courrier", coerce=int)
|
courier_id = SelectField("Courier", coerce=int)
|
||||||
location_id = SelectField(
|
location_id = SelectField(
|
||||||
"Location", coerce=int, validators=[validators.required()]
|
"Location", coerce=str, validators=[validators.required()]
|
||||||
)
|
)
|
||||||
starttime = DateTimeField(
|
starttime = DateTimeField(
|
||||||
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
|
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
|
||||||
)
|
)
|
||||||
stoptime = DateTimeField("Stoptime", format="%d-%m-%Y %H:%M")
|
stoptime = DateTimeField("Stoptime", format="%d-%m-%Y %H:%M")
|
||||||
|
association = SelectField("Association", coerce=str, validators=[validators.required()])
|
||||||
submit_button = SubmitField("Submit")
|
submit_button = SubmitField("Submit")
|
||||||
|
|
||||||
def populate(self) -> None:
|
def populate(self) -> None:
|
||||||
"Fill in the options for courrier for an Order"
|
"Fill in the options for courier for an Order"
|
||||||
if current_user.is_admin():
|
if current_user.is_admin():
|
||||||
self.courrier_id.choices = [(0, None)] + [
|
self.courier_id.choices = [
|
||||||
(u.id, u.username) for u in User.query.order_by("username")
|
(0, None),
|
||||||
|
(current_user.id, current_user.username),
|
||||||
|
] + [
|
||||||
|
(u.id, u.username) for u in User.query.order_by("username") if u.id != current_user.id
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
self.courrier_id.choices = [
|
self.courier_id.choices = [
|
||||||
(0, None),
|
(0, None),
|
||||||
(current_user.id, current_user.username),
|
(current_user.id, current_user.username),
|
||||||
]
|
]
|
||||||
self.location_id.choices = [
|
self.location_id.choices = [(l.id, l.name) for l in location_definitions]
|
||||||
(l.id, l.name) for l in Location.query.order_by("name")
|
self.association.choices = current_user.association_list()
|
||||||
]
|
|
||||||
if self.stoptime.data is None:
|
if self.stoptime.data is None:
|
||||||
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
class OrderItemForm(Form):
|
class OrderItemForm(Form):
|
||||||
"Class which defines the form for a new Item in an Order"
|
"New Item in an Order"
|
||||||
# pylint: disable=R0903
|
# pylint: disable=R0903
|
||||||
product_id = SelectField("Item", coerce=int)
|
dish_id = SelectField("Dish")
|
||||||
extra = StringField("Extra")
|
comment = StringField("Comment")
|
||||||
submit_button = SubmitField("Submit")
|
submit_button = SubmitField("Submit")
|
||||||
|
|
||||||
def populate(self, location: Location) -> None:
|
def populate(self, location: Location) -> None:
|
||||||
"Fill in all the product options from the location"
|
"Populate the order item form"
|
||||||
self.product_id.choices = [
|
self.dish_id.choices = [(dish.id, dish.name) for dish in location.dishes]
|
||||||
(i.id, (i.name + ": " + euro_string(i.price)))
|
if not self.is_submitted() and self.comment.data is None:
|
||||||
for i in location.products
|
self.comment.data = request.args.get("comment")
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AnonOrderItemForm(OrderItemForm):
|
class AnonOrderItemForm(OrderItemForm):
|
||||||
|
@ -62,26 +66,30 @@ class AnonOrderItemForm(OrderItemForm):
|
||||||
Class which defines the form for a new Item in an Order
|
Class which defines the form for a new Item in an Order
|
||||||
For Users who aren't logged in
|
For Users who aren't logged in
|
||||||
"""
|
"""
|
||||||
name = StringField("Name", validators=[validators.required()])
|
|
||||||
|
user_name = StringField("Name", validators=[validators.required()])
|
||||||
|
|
||||||
def populate(self, location: Location) -> None:
|
def populate(self, location: Location) -> None:
|
||||||
"""
|
"""
|
||||||
Fill in all the product options from the location and
|
Fill in all the dish options from the location and
|
||||||
the name of the anon user
|
the name of the anon user
|
||||||
"""
|
"""
|
||||||
OrderItemForm.populate(self, location)
|
OrderItemForm.populate(self, location)
|
||||||
if self.name.data is None:
|
if not self.is_submitted():
|
||||||
self.name.data = session.get("anon_name", None)
|
if self.user_name.data is None:
|
||||||
|
self.user_name.data = request.args.get("user_name")
|
||||||
|
if self.user_name.data is None:
|
||||||
|
self.user_name.data = session.get("anon_name", None)
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
"Check if the provided anon_name is not already taken"
|
"""Check if the provided anon_name is not already taken"""
|
||||||
rv = OrderForm.validate(self)
|
rv = OrderForm.validate(self)
|
||||||
if not rv:
|
if not rv:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check if we have a user with this name
|
# check if we have a user with this name
|
||||||
user = User.query.filter_by(username=self.name.data).first()
|
user = User.query.filter_by(username=self.user_name.data).first()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.name.errors.append("Name already in use")
|
self.user_name.errors.append("Name already in use")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
9
app/hlds/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"""
|
||||||
|
If you want to access the definitions, then just do
|
||||||
|
>>> from hlds.definitions import location_definitions
|
||||||
|
|
||||||
|
These are not imported in this module's init, to avoid opening the definition files and running the
|
||||||
|
parser on them when testing other code in this module, or when testing the parser on other files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .models import Choice, Location, Option
|
24
app/hlds/definitions.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Import this class to load the standard HLDS definitions
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .models import Location
|
||||||
|
from .parser import parse_all_directory
|
||||||
|
|
||||||
|
__all__ = ["location_definitions", "location_definition_version"]
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
||||||
|
ROOT_DIR = Path(__file__).parent.parent.parent
|
||||||
|
DATA_DIR = ROOT_DIR / "menus"
|
||||||
|
|
||||||
|
location_definitions: List[Location] = parse_all_directory(str(DATA_DIR))
|
||||||
|
location_definitions.sort(key=lambda l: l.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, cwd=str(ROOT_DIR), check=True)
|
||||||
|
location_definition_version = proc.stdout.decode().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
location_definition_version = ""
|
89
app/hlds/hlds.tatsu
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# This is a Tatsu file.
|
||||||
|
# Syntax is explained at
|
||||||
|
# https://tatsu.readthedocs.io/en/stable/syntax.html
|
||||||
|
|
||||||
|
@@grammar :: Hlds
|
||||||
|
|
||||||
|
# Don't ignore any whitespace
|
||||||
|
@@whitespace :: //
|
||||||
|
|
||||||
|
@@eol_comments :: /[ \t]*#.*?$/
|
||||||
|
@@keyword :: single_choice multi_choice dish preset only requires except
|
||||||
|
|
||||||
|
|
||||||
|
start = [n] { @+:location } $ ;
|
||||||
|
|
||||||
|
location_header =
|
||||||
|
/={3,}/ n ~
|
||||||
|
id:identifier ':' s name:string
|
||||||
|
attributes:{ location_attribute }
|
||||||
|
[ n ]
|
||||||
|
/={3,}\n+/
|
||||||
|
;
|
||||||
|
|
||||||
|
location_attribute =
|
||||||
|
n '\t' key:identifier s value:string
|
||||||
|
;
|
||||||
|
|
||||||
|
location = >location_header items:{ block } ;
|
||||||
|
|
||||||
|
|
||||||
|
attributes =
|
||||||
|
name:/[^\n#]*?(?= +-- | | €| *\n| *#)/
|
||||||
|
[ s '--' ~ s description:/[^\n#]*?(?= | *\n| *#)/ ]
|
||||||
|
[ / +/ ~
|
||||||
|
[ {[ s ] ('{' tags+:identifier '}')} / +|$/ ]
|
||||||
|
[ price:price ]
|
||||||
|
]
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
block =
|
||||||
|
| dish_block
|
||||||
|
| choice_block
|
||||||
|
;
|
||||||
|
|
||||||
|
dish_block =
|
||||||
|
'dish' s ~
|
||||||
|
id:identifier ':' s >attributes
|
||||||
|
n
|
||||||
|
choices:{ indent_choice_block }
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
choice_entry = id:identifier ':' s >attributes ;
|
||||||
|
|
||||||
|
noindent_choice_entry = n '\t' >choice_entry ;
|
||||||
|
indent_choice_entry = n '\t\t' >choice_entry ;
|
||||||
|
|
||||||
|
# Toplevel choice definitions
|
||||||
|
choice_block =
|
||||||
|
id:identifier ':' s >attributes entries:{ noindent_choice_entry }
|
||||||
|
n
|
||||||
|
;
|
||||||
|
|
||||||
|
# Appears in a dish block. One of:
|
||||||
|
# 1) reference to toplevel choice definition
|
||||||
|
# 2) inline choice declaration (as opposed to globally declared and referenced)
|
||||||
|
indent_choice_block =
|
||||||
|
'\t' type:choice_type s
|
||||||
|
(
|
||||||
|
| kind:`declaration` id:identifier ':' ~ s >attributes entries:{ indent_choice_entry }
|
||||||
|
| kind:`reference` id:identifier [ s price:price ]
|
||||||
|
)
|
||||||
|
n
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
s = / +/ ;
|
||||||
|
n = '\n' {{'\t'} '\n'} ;
|
||||||
|
|
||||||
|
@name
|
||||||
|
identifier = /[a-z0-9_-]+/ ;
|
||||||
|
string = /[^\n#]+/ ;
|
||||||
|
choice_type = 'single_choice' | 'multi_choice' ;
|
||||||
|
|
||||||
|
int = /[0-9]+/ ;
|
||||||
|
|
||||||
|
currency = '€' ;
|
||||||
|
price = currency:currency s value_unit:int [ '.' value_cents:/[0-9]{,2}/ ] ;
|
135
app/hlds/models.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
from typing import Any, Iterable, List, Mapping, Optional, Tuple
|
||||||
|
|
||||||
|
from utils import euro_string, first
|
||||||
|
|
||||||
|
|
||||||
|
def _format_tags(tags: Iterable[str]) -> str:
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
return " :: {}".format(" ".join(["{" + tag + "}"
|
||||||
|
for tag in tags])) if tags else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _format_price(price: int) -> str:
|
||||||
|
return f" {euro_string(price)}" if price else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _format_type_and_choice(type_and_choice):
|
||||||
|
type_, choice = type_and_choice
|
||||||
|
return f"{type_} {choice}"
|
||||||
|
|
||||||
|
|
||||||
|
class Option:
|
||||||
|
|
||||||
|
def __init__(self, id_, *, name, description, price, tags):
|
||||||
|
self.id: str = id_
|
||||||
|
self.name: str = name
|
||||||
|
self.description: str = description
|
||||||
|
self.price: int = price
|
||||||
|
self.tags: List[str] = tags
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
return "{0.id}: {0.name}{1}{2}{3}".format(
|
||||||
|
self,
|
||||||
|
f" -- {self.description}" if self.description else "",
|
||||||
|
_format_tags(self.tags),
|
||||||
|
_format_price(self.price),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Choice:
|
||||||
|
|
||||||
|
def __init__(self, id_, *, name, description, options):
|
||||||
|
self.id: str = id_
|
||||||
|
self.name: str = name
|
||||||
|
self.description: str = description
|
||||||
|
|
||||||
|
self.options: List[Option] = options
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
||||||
|
self,
|
||||||
|
f" -- {self.description}" if self.description else "",
|
||||||
|
"\n\t\t".join(map(str, self.options)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def option_by_id(self, option_id: str) -> Optional[Option]:
|
||||||
|
return first(filter(lambda o: o.id == option_id, self.options))
|
||||||
|
|
||||||
|
|
||||||
|
class Dish:
|
||||||
|
|
||||||
|
def __init__(self, id_, *, name, description, price, tags, choices):
|
||||||
|
self.id: str = id_
|
||||||
|
self.name: str = name
|
||||||
|
self.description: str = description
|
||||||
|
self.price: int = price
|
||||||
|
self.tags: List[str] = tags
|
||||||
|
|
||||||
|
# The str in (str, Choice) is the type of choice: single_choice or multi_choice
|
||||||
|
self.choices: List[(str, Choice)] = choices
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "dish {0.id}: {0.name}{1}{2}{3}\n\t{4}".format(
|
||||||
|
self,
|
||||||
|
f" -- {self.description}" if self.description else "",
|
||||||
|
_format_tags(self.tags),
|
||||||
|
_format_price(self.price),
|
||||||
|
"\n\t".join(map(_format_type_and_choice, self.choices)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def price_range(self) -> Tuple[int, int]:
|
||||||
|
return (
|
||||||
|
self.price + self._sum_f_option_prices(min),
|
||||||
|
self.price + self._sum_f_option_prices(max),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sum_f_option_prices(self, f):
|
||||||
|
return sum(
|
||||||
|
f(option.price for option in choice.options)
|
||||||
|
for (choice_type, choice) in self.choices
|
||||||
|
if choice_type == "single_choice")
|
||||||
|
|
||||||
|
|
||||||
|
class Location:
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
id_,
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
dishes,
|
||||||
|
osm=None,
|
||||||
|
address=None,
|
||||||
|
telephone=None,
|
||||||
|
website=None):
|
||||||
|
self.id: str = id_
|
||||||
|
self.name: str = name
|
||||||
|
self.osm: Optional[str] = osm
|
||||||
|
self.address: Optional[str] = address
|
||||||
|
self.telephone: Optional[str] = telephone
|
||||||
|
self.website: Optional[str] = website
|
||||||
|
|
||||||
|
self.dishes: List[Dish] = dishes
|
||||||
|
|
||||||
|
def dish_by_id(self, dish_id: str) -> Optional[Dish]:
|
||||||
|
return first(filter(lambda d: d.id == dish_id, self.dishes))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("============================\n"
|
||||||
|
"{0.id}: {0.name}"
|
||||||
|
"{1}\n"
|
||||||
|
"============================\n"
|
||||||
|
"\n"
|
||||||
|
"{2}").format(
|
||||||
|
self,
|
||||||
|
"".join(f"\n\t{k} {v}" for k, v in (
|
||||||
|
("osm", self.osm),
|
||||||
|
("address", self.address),
|
||||||
|
("telephone", self.telephone),
|
||||||
|
("website", self.website),
|
||||||
|
) if v is not None),
|
||||||
|
"\n".join(map(str, self.dishes)),
|
||||||
|
)
|
163
app/hlds/parser.py
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
from copy import deepcopy
|
||||||
|
from glob import glob
|
||||||
|
from os import path
|
||||||
|
from typing import Iterable, List, Tuple, Union
|
||||||
|
|
||||||
|
from tatsu import parse as tatsu_parse
|
||||||
|
from tatsu.ast import AST
|
||||||
|
from tatsu.exceptions import SemanticError
|
||||||
|
from utils import first
|
||||||
|
|
||||||
|
from .models import Choice, Dish, Location, Option
|
||||||
|
|
||||||
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
||||||
|
with open(path.join(path.dirname(__file__), "hlds.tatsu")) as fh:
|
||||||
|
GRAMMAR = fh.read()
|
||||||
|
|
||||||
|
|
||||||
|
def filter_instance(cls, iterable):
|
||||||
|
return [item for item in iterable if isinstance(item, cls)]
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceReference:
|
||||||
|
def __init__(self, identifier, price):
|
||||||
|
self.identifier = identifier
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
class HldsSemanticActions:
|
||||||
|
def location(self, ast) -> Location:
|
||||||
|
choices = {
|
||||||
|
choice.id: choice for choice in filter_instance(Choice, ast["items_"])
|
||||||
|
}
|
||||||
|
dishes: Iterable[Dish] = filter_instance(Dish, ast["items_"])
|
||||||
|
for dish in dishes:
|
||||||
|
for i, choice in enumerate(dish.choices):
|
||||||
|
if not isinstance(choice[1], Choice):
|
||||||
|
choiceId, choiceRef = choice
|
||||||
|
assert isinstance(choiceRef, ChoiceReference)
|
||||||
|
# We must replace the ChoiceReference with the Choice it refers to. A deep copy
|
||||||
|
# allows us to modify the individual Options of the Choice.
|
||||||
|
choiceObject = deepcopy(choices[choiceRef.identifier])
|
||||||
|
|
||||||
|
for option in choiceObject.options:
|
||||||
|
option.price += choiceRef.price
|
||||||
|
|
||||||
|
dish.choices[i] = (choiceId, choiceObject)
|
||||||
|
|
||||||
|
# Move the base price to the first single_choice if the dish doesn't have a fixed price
|
||||||
|
first_single_choice = first(
|
||||||
|
c[1] for c in dish.choices if c[0] == "single_choice"
|
||||||
|
)
|
||||||
|
price_range = dish.price_range()
|
||||||
|
if dish.price and price_range[0] != price_range[1] and first_single_choice:
|
||||||
|
for option in first_single_choice.options:
|
||||||
|
option.price += dish.price
|
||||||
|
dish.price = 0
|
||||||
|
dishes = list(dishes)
|
||||||
|
dishes.append(
|
||||||
|
Dish(
|
||||||
|
"custom",
|
||||||
|
name="Vrije keuze",
|
||||||
|
description="Zet wat je wil in comment",
|
||||||
|
price=0,
|
||||||
|
tags=[],
|
||||||
|
choices=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
||||||
|
|
||||||
|
return Location(
|
||||||
|
ast["id"],
|
||||||
|
name=ast["name"],
|
||||||
|
dishes=dishes,
|
||||||
|
osm=attributes.get("osm"),
|
||||||
|
address=attributes.get("address"),
|
||||||
|
telephone=attributes.get("phone"),
|
||||||
|
website=attributes.get("website"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def dish_block(self, ast) -> Dish:
|
||||||
|
return Dish(
|
||||||
|
ast["id"],
|
||||||
|
name=ast["name"],
|
||||||
|
description=ast["description"],
|
||||||
|
price=ast["price"] or 0,
|
||||||
|
tags=ast["tags"] or [],
|
||||||
|
choices=ast["choices"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def choice_block(self, ast) -> Choice:
|
||||||
|
if ast["price"] or ast["tags"]:
|
||||||
|
raise SemanticError(
|
||||||
|
"Choice block definitions cannot have price or tags, put them on each of its options instead"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Choice(
|
||||||
|
ast["id"],
|
||||||
|
name=ast["name"],
|
||||||
|
description=ast["description"],
|
||||||
|
options=ast["entries"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def indent_choice_block(self, ast) -> Tuple[str, Union[Choice, AST]]:
|
||||||
|
if ast["kind"] == "declaration":
|
||||||
|
return (ast["type"], self.choice_block(ast))
|
||||||
|
else:
|
||||||
|
if ast["type"] == "single_choice" and ast["price"]:
|
||||||
|
raise SemanticError(
|
||||||
|
"Single_choice choices can't have a price, because it would always be triggered"
|
||||||
|
)
|
||||||
|
return (ast["type"], ChoiceReference(ast["id"], ast["price"] or 0))
|
||||||
|
|
||||||
|
def indent_choice_entry(self, ast) -> Option:
|
||||||
|
return Option(
|
||||||
|
ast["id"],
|
||||||
|
name=ast["name"],
|
||||||
|
description=ast["description"],
|
||||||
|
price=ast["price"] or 0,
|
||||||
|
tags=ast["tags"] or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
noindent_choice_entry = indent_choice_entry
|
||||||
|
|
||||||
|
def price(self, ast) -> int:
|
||||||
|
return 100 * int(ast["value_unit"]) + (
|
||||||
|
0
|
||||||
|
if not ast["value_cents"]
|
||||||
|
else 10 * int(ast["value_cents"])
|
||||||
|
if len(ast["value_cents"]) == 1
|
||||||
|
else int(ast["value_cents"])
|
||||||
|
)
|
||||||
|
|
||||||
|
def _default(self, ast):
|
||||||
|
return ast
|
||||||
|
|
||||||
|
|
||||||
|
SEMANTICS = HldsSemanticActions()
|
||||||
|
|
||||||
|
|
||||||
|
def parse(menu: str) -> List[Location]:
|
||||||
|
parsed = tatsu_parse(GRAMMAR, menu, semantics=SEMANTICS)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def parse_file(filename: str) -> List[Location]:
|
||||||
|
with open(filename) as file_handle:
|
||||||
|
return parse(file_handle.read())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_files(files: Iterable[str]) -> List[Location]:
|
||||||
|
menus = map(parse_file, files)
|
||||||
|
return list(itertools.chain.from_iterable(menus))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_all_directory(directory: str) -> List[Location]:
|
||||||
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
||||||
|
files = glob(path.join(directory, "**.hlds"), recursive=True)
|
||||||
|
return parse_files(files)
|
26
app/hlds/testfrituur.hlds
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
============================
|
||||||
|
testfrituur: Testfrituur
|
||||||
|
osm https://www.openstreetmap.org/node/4935648725
|
||||||
|
============================
|
||||||
|
|
||||||
|
sauce: Saus
|
||||||
|
ketchup: Ketchup -- Een tomatensaus met veel suiker € 0.8
|
||||||
|
mayo: Mayonaise -- Eierdooier en olie € 0.8
|
||||||
|
curry: Currysaus € 0.80
|
||||||
|
stoofvlees: Stoofvleessaus € 1.41
|
||||||
|
|
||||||
|
dish veggieburger: Veggieburger € 4.2
|
||||||
|
|
||||||
|
dish friet: Friet -- Een pakje friet :: {has_meat} {has_gluten}
|
||||||
|
|
||||||
|
single_choice size: Grootte
|
||||||
|
small: Small € 2
|
||||||
|
medium: Medium € 2.5
|
||||||
|
large: Large € 3
|
||||||
|
xlarge: Extra large -- Je hebt een auto nodig om dit te vervoeren € 50
|
||||||
|
|
||||||
|
multi_choice sauce
|
||||||
|
|
||||||
|
dish vleeskroket: Vleeskroket -- Een vlezige kroket :: {has_meat} € 4.2
|
||||||
|
dish vleeskroket: Vleeskroket :: {has_meat} € 4.2
|
||||||
|
dish vleeskroket: Vleeskroket -- Een vlezige kroket
|
|
@ -1,6 +1,6 @@
|
||||||
Generic single-database configuration.
|
Generic single-database configuration.
|
||||||
|
|
||||||
+On the existing databases, initialize flask-migrate and alembic with:
|
On the existing databases, initialize flask-migrate and alembic with:
|
||||||
+```
|
```
|
||||||
+python haldis.py db stamp "354676f60be"
|
python app.py db stamp 150252c1cdb1
|
||||||
+```
|
```
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"Script that runs migrations online or offline"
|
"Script that runs migrations online or offline"
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ Revises: None
|
||||||
Create Date: 2019-04-02 18:00:12.618368
|
Create Date: 2019-04-02 18:00:12.618368
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = "150252c1cdb1"
|
revision = "150252c1cdb1"
|
||||||
|
@ -42,7 +43,7 @@ def upgrade():
|
||||||
sa.Column("starttime", sa.DateTime(), nullable=True),
|
sa.Column("starttime", sa.DateTime(), nullable=True),
|
||||||
sa.Column("stoptime", sa.DateTime(), nullable=True),
|
sa.Column("stoptime", sa.DateTime(), nullable=True),
|
||||||
sa.Column("public", sa.Boolean(), nullable=True),
|
sa.Column("public", sa.Boolean(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(["location_id"], ["location.id"]),
|
sa.ForeignKeyConstraint(["location_id"], ["location.id"], name="order_ibfk_1"),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
|
@ -64,7 +65,7 @@ def upgrade():
|
||||||
sa.Column("extra", sa.String(length=254), nullable=True),
|
sa.Column("extra", sa.String(length=254), nullable=True),
|
||||||
sa.Column("name", sa.String(length=120), nullable=True),
|
sa.Column("name", sa.String(length=120), nullable=True),
|
||||||
sa.ForeignKeyConstraint(["order_id"], ["order.id"]),
|
sa.ForeignKeyConstraint(["order_id"], ["order.id"]),
|
||||||
sa.ForeignKeyConstraint(["product_id"], ["product.id"]),
|
sa.ForeignKeyConstraint(["product_id"], ["product.id"], name="order_item_ibfk_3"),
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
|
sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
|
|
30
app/migrations/versions/29ccbe077c57_add_slug.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""add slug
|
||||||
|
|
||||||
|
Revision ID: 29ccbe077c57
|
||||||
|
Revises: 55013fe95bea
|
||||||
|
Create Date: 2022-05-20 19:46:11.924218
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '29ccbe077c57'
|
||||||
|
down_revision = '55013fe95bea'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('order', sa.Column(
|
||||||
|
'slug',
|
||||||
|
sa.String(length=8),
|
||||||
|
nullable=False,
|
||||||
|
# Default: random alphanumerical string
|
||||||
|
server_default=text('SUBSTRING(MD5(RAND()) FROM 1 FOR 7)')
|
||||||
|
))
|
||||||
|
op.create_unique_constraint('order_slug_unique', 'order', ['slug'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_constraint('order_slug_unique', 'order', type_='unique')
|
||||||
|
op.drop_column('order', 'slug')
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""Create price_modified column
|
||||||
|
|
||||||
|
Revision ID: 55013fe95bea
|
||||||
|
Revises: 9159a6fed021
|
||||||
|
Create Date: 2022-04-22 01:00:03.729596
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '55013fe95bea'
|
||||||
|
down_revision = '9159a6fed021'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('order_item', sa.Column('price_modified', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('order_item', 'price_modified')
|
26
app/migrations/versions/89b2c980b663_.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 89b2c980b663
|
||||||
|
Revises: 9eac0f3d7b1e
|
||||||
|
Create Date: 2023-04-20 02:01:54.558602
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '89b2c980b663'
|
||||||
|
down_revision = '9eac0f3d7b1e'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('microsoft_uuid', sa.VARCHAR(length=120), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'microsoft_uuid')
|
||||||
|
# ### end Alembic commands ###
|
164
app/migrations/versions/9159a6fed021_initial_haldis_support.py
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
"""Initial HLDS support
|
||||||
|
|
||||||
|
Revision ID: 9159a6fed021
|
||||||
|
Revises: 150252c1cdb1
|
||||||
|
Create Date: 2020-01-26 16:22:00.935963
|
||||||
|
|
||||||
|
"""
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "9159a6fed021"
|
||||||
|
down_revision = "150252c1cdb1"
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from hlds.definitions import location_definitions
|
||||||
|
from sqlalchemy.sql import column, table, text
|
||||||
|
|
||||||
|
LOCATION_LEGACY_TO_HLDS = {
|
||||||
|
2: "blauw_kotje",
|
||||||
|
3: "ocean_garden",
|
||||||
|
4: "delhaize",
|
||||||
|
5: "simpizza",
|
||||||
|
6: "shell_tankstation",
|
||||||
|
7: "pitta_pinte",
|
||||||
|
8: "lan_pizza",
|
||||||
|
9: "oriental",
|
||||||
|
10: "gouden_sate",
|
||||||
|
11: "quick",
|
||||||
|
12: "dominos",
|
||||||
|
13: "stella_di_mare",
|
||||||
|
14: "olifantje",
|
||||||
|
15: "pauls_boutique",
|
||||||
|
16: "fritoloog",
|
||||||
|
17: "andromeda",
|
||||||
|
19: "pita_cece",
|
||||||
|
20: "pita_uniq",
|
||||||
|
21: "basic_italian",
|
||||||
|
22: "pizza_hut",
|
||||||
|
23: "test",
|
||||||
|
25: "primadonna_takeaway",
|
||||||
|
26: "primadonna_delivery",
|
||||||
|
28: "s5",
|
||||||
|
29: "fitchen",
|
||||||
|
30: "kebab_anadolu",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# First the simple actions
|
||||||
|
op.create_table(
|
||||||
|
"order_item_choice",
|
||||||
|
sa.Column("id", sa.Integer, nullable=False),
|
||||||
|
sa.Column("choice_id", sa.String(length=64), nullable=True),
|
||||||
|
sa.Column("order_item_id", sa.Integer, nullable=False),
|
||||||
|
sa.Column("kind", sa.String(length=1), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=120), nullable=True),
|
||||||
|
sa.Column("value", sa.String(length=120), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["order_item_id"],
|
||||||
|
["order_item.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order_item",
|
||||||
|
sa.Column("hlds_data_version", sa.String(length=40), nullable=True),
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order", "courrier_id", new_column_name="courier_id", type_=sa.Integer
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order_item",
|
||||||
|
"extra",
|
||||||
|
new_column_name="comment",
|
||||||
|
existing_type=sa.String(254),
|
||||||
|
type_=sa.Text,
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order_item", "name", new_column_name="user_name", type_=sa.String(120)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------
|
||||||
|
# Migrate historical product data to order items
|
||||||
|
|
||||||
|
# First create the new columns we will populate
|
||||||
|
op.add_column(
|
||||||
|
"order_item", sa.Column("dish_id", sa.String(length=64), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order_item", sa.Column("dish_name", sa.String(length=120), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column("order_item", sa.Column("price", sa.Integer(), nullable=True))
|
||||||
|
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
||||||
|
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||||
|
order_item = table(
|
||||||
|
"order_item",
|
||||||
|
column("product_id", sa.Integer),
|
||||||
|
column("dish_id", sa.String),
|
||||||
|
column("dish_name", sa.String),
|
||||||
|
column("price", sa.Integer),
|
||||||
|
)
|
||||||
|
# Construct and execute queries
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE order_item
|
||||||
|
SET dish_name = (SELECT product.name FROM product WHERE product.id = order_item.product_id),
|
||||||
|
price = (SELECT product.price FROM product WHERE product.id = order_item.product_id)"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Historical product data migrated, drop obsolete column and table
|
||||||
|
op.drop_constraint("order_item_ibfk_3", "order_item", type_="foreignkey")
|
||||||
|
op.drop_column("order_item", "product_id")
|
||||||
|
op.drop_table("product")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------
|
||||||
|
# Migrate historical location data to orders
|
||||||
|
op.alter_column(
|
||||||
|
"order",
|
||||||
|
"location_id",
|
||||||
|
new_column_name="legacy_location_id",
|
||||||
|
type_=sa.Integer,
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order", sa.Column("location_id", sa.String(length=64), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order", sa.Column("location_name", sa.String(length=128), nullable=True)
|
||||||
|
)
|
||||||
|
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
||||||
|
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||||
|
order = table(
|
||||||
|
"order",
|
||||||
|
column("legacy_location_id", sa.Integer),
|
||||||
|
column("location_id", sa.String),
|
||||||
|
column("location_name", sa.String),
|
||||||
|
)
|
||||||
|
# Construct and execute queries
|
||||||
|
new_location_id = [
|
||||||
|
order.update()
|
||||||
|
.where(order.c.legacy_location_id == old_id)
|
||||||
|
.values(location_id=new_id)
|
||||||
|
for old_id, new_id in LOCATION_LEGACY_TO_HLDS.items()
|
||||||
|
]
|
||||||
|
location_name_from_location = text(
|
||||||
|
"""
|
||||||
|
UPDATE `order`
|
||||||
|
SET location_name = (SELECT location.name FROM location
|
||||||
|
WHERE location.id = `order`.legacy_location_id)"""
|
||||||
|
)
|
||||||
|
for query in chain(new_location_id, [location_name_from_location]):
|
||||||
|
op.execute(query)
|
||||||
|
# Historical location data migrated, drop obsolete column and table
|
||||||
|
op.drop_constraint("order_ibfk_1", "order", type_="foreignkey")
|
||||||
|
op.drop_column("order", "legacy_location_id")
|
||||||
|
op.drop_table("location")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
raise NotImplementedError("Downgrading to before HLDS is not supported")
|
22
app/migrations/versions/9eac0f3d7b1e_.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 9eac0f3d7b1e
|
||||||
|
Revises: ('f6a6004bf4b9', '29ccbe077c57')
|
||||||
|
Create Date: 2022-05-30 18:35:43.918797
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9eac0f3d7b1e'
|
||||||
|
down_revision = ('f6a6004bf4b9', '29ccbe077c57')
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Add user associations
|
||||||
|
|
||||||
|
Revision ID: f6a6004bf4b9
|
||||||
|
Revises: 55013fe95bea
|
||||||
|
Create Date: 2022-05-24 21:23:27.770365
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f6a6004bf4b9'
|
||||||
|
down_revision = '55013fe95bea'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('order', sa.Column('association', sa.String(length=120), server_default='', nullable=False))
|
||||||
|
op.add_column('user', sa.Column('associations', sa.String(length=255), server_default='', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'associations')
|
||||||
|
op.drop_column('order', 'association')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -11,8 +11,7 @@
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .location import Location
|
|
||||||
from .order import Order
|
from .order import Order
|
||||||
from .orderitem import OrderItem
|
from .orderitem import OrderItem
|
||||||
from .product import Product
|
from .orderitemchoice import OrderItemChoice
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
"AnonymouseUser for people who are not logged in the normal way"
|
"AnonymouseUser for people who are not logged in the normal way"
|
||||||
|
from typing import List
|
||||||
# pylint: disable=R0201,C0111
|
# pylint: disable=R0201,C0111
|
||||||
|
|
||||||
|
|
||||||
class AnonymouseUser:
|
class AnonymouseUser:
|
||||||
id = None
|
id = None
|
||||||
|
|
||||||
|
def association_list(self) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
"Script for everything Location related in the database"
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from models import db
|
|
||||||
|
|
||||||
|
|
||||||
class Location(db.Model):
|
|
||||||
"Class used for configuring the Location model in the database"
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(120), nullable=False)
|
|
||||||
address = db.Column(db.String(254))
|
|
||||||
website = db.Column(db.String(120))
|
|
||||||
telephone = db.Column(db.String(20), nullable=True)
|
|
||||||
products = db.relationship("Product", backref="location", lazy="dynamic")
|
|
||||||
orders = db.relationship("Order", backref="location", lazy="dynamic")
|
|
||||||
|
|
||||||
def configure(self, name: str, address: str,
|
|
||||||
telephone: typing.Optional[str], website: str) -> None:
|
|
||||||
"Configure the Location"
|
|
||||||
self.name = name
|
|
||||||
self.address = address
|
|
||||||
self.website = website
|
|
||||||
self.telephone = telephone
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "%s" % (self.name)
|
|
|
@ -1,74 +1,137 @@
|
||||||
"Script for everything Order related in the database"
|
"""Script for everything Order related in the database"""
|
||||||
import typing
|
import typing
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
from hlds.definitions import location_definitions
|
||||||
|
from utils import first
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .location import Location
|
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
BASE31_ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'
|
||||||
|
|
||||||
|
def generate_slug():
|
||||||
|
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
|
||||||
|
while Order.query.filter(Order.slug == secret).first() is not None:
|
||||||
|
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
|
||||||
|
return secret
|
||||||
|
|
||||||
class Order(db.Model):
|
class Order(db.Model):
|
||||||
"Class used for configuring the Order model in the database"
|
"""Class used for configuring the Order model in the database"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
courrier_id = db.Column(db.Integer, nullable=True)
|
courier_id = db.Column(db.Integer, nullable=True)
|
||||||
location_id = db.Column(db.Integer, db.ForeignKey("location.id"))
|
location_id = db.Column(db.String(64))
|
||||||
|
location_name = db.Column(db.String(128))
|
||||||
starttime = db.Column(db.DateTime)
|
starttime = db.Column(db.DateTime)
|
||||||
stoptime = db.Column(db.DateTime)
|
stoptime = db.Column(db.DateTime)
|
||||||
public = db.Column(db.Boolean, default=True)
|
public = db.Column(db.Boolean, default=True)
|
||||||
|
slug = db.Column(db.String(8), default=generate_slug, unique=True)
|
||||||
|
association = db.Column(db.String(120), nullable=False, server_default="")
|
||||||
|
|
||||||
items = db.relationship("OrderItem", backref="order", lazy="dynamic")
|
items = db.relationship("OrderItem", backref="order", lazy="dynamic")
|
||||||
|
|
||||||
def configure(self, courrier: User, location: Location,
|
def __getattr__(self, name):
|
||||||
starttime: db.DateTime, stoptime: db.DateTime,) -> None:
|
if name == "location":
|
||||||
"Configure the Order"
|
return first(
|
||||||
# pylint: disable=W0201
|
filter(lambda l: l.id == self.location_id, location_definitions)
|
||||||
self.courrier = courrier
|
)
|
||||||
self.location = location
|
raise AttributeError()
|
||||||
self.starttime = starttime
|
|
||||||
self.stoptime = stoptime
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
# pylint: disable=R1705
|
# pylint: disable=R1705
|
||||||
if self.location:
|
if self.location:
|
||||||
return "Order %d @ %s" % (self.id, self.location.name or "None")
|
return f"Order {self.id} @ {self.location.name or 'None'}"
|
||||||
else:
|
else:
|
||||||
return "Order %d" % (self.id)
|
return f"Order {self.id}"
|
||||||
|
|
||||||
def group_by_user(self) -> typing.Dict[str, typing.Any]:
|
def update_from_hlds(self) -> None:
|
||||||
"Group items of an Order by user"
|
"""
|
||||||
group: typing.Dict[str, typing.Any] = dict()
|
Update the location name from the HLDS definition.
|
||||||
for item in self.items:
|
User should commit after running this to make the change persistent.
|
||||||
user = group.get(item.get_name(), dict())
|
"""
|
||||||
user["total"] = user.get("total", 0) + item.product.price
|
assert (
|
||||||
user["to_pay"] = (
|
self.location_id
|
||||||
user.get("to_pay", 0) +
|
), "location_id must be configured before updating from HLDS"
|
||||||
item.product.price if not item.paid else 0
|
self.location_name = self.location.name
|
||||||
|
|
||||||
|
def for_user(self, anon=None, user=None) -> typing.List:
|
||||||
|
"""Get the items for a certain user"""
|
||||||
|
return list(
|
||||||
|
filter(
|
||||||
|
(lambda i: i.user == user)
|
||||||
|
if user is not None
|
||||||
|
else (lambda i: i.user_name == anon),
|
||||||
|
self.items,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
user["paid"] = user.get("paid", True) and item.paid
|
|
||||||
user["products"] = user.get("products", []) + [item.product]
|
|
||||||
group[item.get_name()] = user
|
|
||||||
|
|
||||||
return group
|
def group_by_user(self) -> typing.List[typing.Tuple[str, typing.List]]:
|
||||||
|
"""Group items of an Order by user"""
|
||||||
|
group: typing.Dict[str, typing.List] = {}
|
||||||
|
|
||||||
def group_by_product(self) -> typing.Dict[str, typing.Any]:
|
# pylint: disable=E1133
|
||||||
"Group items of an Order by product"
|
|
||||||
group: typing.Dict[str, typing.Any] = dict()
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
product = group.get(item.product.name, dict())
|
if item.for_name not in group:
|
||||||
product["count"] = product.get("count", 0) + 1
|
group[item.for_name] = []
|
||||||
if item.extra:
|
|
||||||
product["extras"] = product.get("extras", []) + [item.extra]
|
|
||||||
group[item.product.name] = product
|
|
||||||
|
|
||||||
return group
|
group[item.for_name].append(item)
|
||||||
|
|
||||||
|
for _user_name, order_items in group.items():
|
||||||
|
order_items.sort(key=lambda order_item: order_item.comment or "")
|
||||||
|
|
||||||
|
return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or "")))
|
||||||
|
|
||||||
|
def group_by_dish(
|
||||||
|
self,
|
||||||
|
) -> typing.List[
|
||||||
|
typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]
|
||||||
|
]:
|
||||||
|
"""Group items of an Order by dish"""
|
||||||
|
group: typing.Dict[str, typing.Dict[str, typing.List]] = defaultdict(
|
||||||
|
lambda: defaultdict(list)
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=E1133
|
||||||
|
for item in self.items:
|
||||||
|
group[item.dish_name][item.comment].append(item)
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
(
|
||||||
|
dish_name,
|
||||||
|
# Amount of items of this dish
|
||||||
|
sum(map(len, comment_group.values())),
|
||||||
|
sorted(
|
||||||
|
(comment, sorted(items, key=lambda x: (x.for_name or "")))
|
||||||
|
for comment, items in comment_group.items()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for dish_name, comment_group in group.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Return whether the order is closed"""
|
||||||
|
return self.stoptime and datetime.now() > self.stoptime
|
||||||
|
|
||||||
def can_close(self, user_id: int) -> bool:
|
def can_close(self, user_id: int) -> bool:
|
||||||
"Check if a user can close the Order"
|
"""Check if a user can close the Order"""
|
||||||
if self.stoptime and self.stoptime < datetime.now():
|
if self.stoptime and self.stoptime < datetime.now():
|
||||||
return False
|
return False
|
||||||
user = None
|
user = None
|
||||||
if user_id:
|
if user_id:
|
||||||
user = User.query.filter_by(id=user_id).first()
|
user = User.query.filter_by(id=user_id).first()
|
||||||
print(user)
|
if self.courier_id == user_id or (user and user.is_admin()):
|
||||||
if self.courrier_id == user_id or (user and user.is_admin()):
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def can_modify_prices(self, user_id: int) -> bool:
|
||||||
|
if not self.is_closed():
|
||||||
|
return False
|
||||||
|
user = User.query.filter_by(id=user_id).first()
|
||||||
|
return user and (user.is_admin() or user == self.courier)
|
||||||
|
|
||||||
|
def can_modify_payment(self, user_id: int) -> bool:
|
||||||
|
user = User.query.filter_by(id=user_id).first()
|
||||||
|
return user and (user.is_admin() or user == self.courier)
|
||||||
|
|
|
@ -1,61 +1,85 @@
|
||||||
"Script for everything OrderItem related in the database"
|
"Script for everything OrderItem related in the database"
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from hlds.definitions import location_definitions
|
||||||
|
from utils import first
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .order import Order
|
from .order import Order
|
||||||
from .product import Product
|
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(db.Model):
|
class OrderItem(db.Model):
|
||||||
"Class used for configuring the OrderItem model in the database"
|
"""Class used for configuring the OrderItem model in the database"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
|
||||||
order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
|
order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
|
||||||
product_id = db.Column(
|
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
db.Integer, db.ForeignKey("product.id"), nullable=True
|
user_name = db.Column(db.String(120))
|
||||||
) # TODO make false after init migration
|
dish_id = db.Column(db.String(64), nullable=True)
|
||||||
paid = db.Column(
|
dish_name = db.Column(db.String(120), nullable=True)
|
||||||
db.Boolean, default=False, nullable=True
|
price = db.Column(db.Integer, nullable=True)
|
||||||
) # TODO make false after init migration
|
price_modified = db.Column(db.DateTime, nullable=True)
|
||||||
extra = db.Column(db.String(254), nullable=True)
|
paid = db.Column(db.Boolean, default=False, nullable=True)
|
||||||
name = db.Column(db.String(120))
|
comment = db.Column(db.Text(), nullable=True)
|
||||||
|
hlds_data_version = db.Column(db.String(40), nullable=True)
|
||||||
|
|
||||||
def configure(self, user: User, order: Order, product: Product) -> None:
|
choices = db.relationship("OrderItemChoice",
|
||||||
"Configure the OrderItem"
|
backref="order_item",
|
||||||
# pylint: disable=W0201
|
lazy="dynamic")
|
||||||
self.user = user
|
|
||||||
self.order = order
|
|
||||||
self.product = product
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
def __getattr__(self, name):
|
||||||
|
if name == "dish":
|
||||||
|
location_id = (Order.query.filter(
|
||||||
|
Order.id == self.order_id).first().location_id)
|
||||||
|
location = first(
|
||||||
|
filter(lambda l: l.id == location_id, location_definitions))
|
||||||
|
if location:
|
||||||
|
return first(
|
||||||
|
filter(lambda d: d.id == self.dish_id, location.dishes))
|
||||||
|
raise ValueError(f"No Location found with id: {location_id}")
|
||||||
|
raise AttributeError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def for_name(self) -> str:
|
||||||
"Get the name of the user which 'owns' the item"
|
"Get the name of the user which 'owns' the item"
|
||||||
if self.user_id is not None and self.user_id > 0:
|
if self.user_id is not None and self.user_id > 0:
|
||||||
return self.user.username
|
return self.user.username
|
||||||
return self.name
|
return self.user_name
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
product_name = None
|
return "Order {self.order_id or 0}: {self.for_name} wants {self.dish_name or 'None'}"
|
||||||
if self.product:
|
|
||||||
product_name = self.product.name
|
def update_from_hlds(self) -> None:
|
||||||
return "Order %d: %s wants %s" % (
|
"""
|
||||||
self.order_id or 0,
|
Update the dish name and price from the HLDS definition.
|
||||||
self.get_name(),
|
User should commit after running this to make the change persistent.
|
||||||
product_name or "None",
|
"""
|
||||||
)
|
assert self.order_id, "order_id must be configured before updating from HLDS"
|
||||||
|
assert self.dish_id, "dish_id must be configured before updating from HLDS"
|
||||||
|
self.dish_name = self.dish.name
|
||||||
|
self.price = self.dish.price
|
||||||
|
|
||||||
# pylint: disable=W0613
|
# pylint: disable=W0613
|
||||||
def can_delete(self, order_id: int, user_id: int, name: str) -> bool:
|
def can_delete(self, order_id: int, user_id: int, name: str) -> bool:
|
||||||
"Check if a user can delete an item"
|
"""Check if a user can delete an item"""
|
||||||
if int(self.order_id) != int(order_id):
|
if int(self.order_id) != int(order_id):
|
||||||
return False
|
return False
|
||||||
if self.order.stoptime and self.order.stoptime < datetime.now():
|
if self.order.is_closed():
|
||||||
return False
|
return False
|
||||||
if self.user is not None and self.user_id == user_id:
|
if self.user is not None and self.user_id == user_id:
|
||||||
return True
|
return True
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
return False
|
return False
|
||||||
user = User.query.filter(User.id == user_id).first()
|
user = User.query.filter(User.id == user_id).first()
|
||||||
if user and (user.is_admin() or user == self.order.courrier):
|
if user and (user.is_admin() or user == self.order.courier):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
def can_modify_payment(self, order_id: int, user_id: int) -> bool:
|
||||||
|
if int(self.order_id) != int(order_id):
|
||||||
|
return False
|
||||||
|
user = User.query.filter(User.id == user_id).first()
|
||||||
|
if user and (user.is_admin() or user == self.order.courier):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
25
app/models/orderitemchoice.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"Script for everything OrderItemChoice related in the database"
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .database import db
|
||||||
|
from .orderitem import OrderItem
|
||||||
|
|
||||||
|
|
||||||
|
class OrderItemChoice(db.Model):
|
||||||
|
"Class used for configuring the OrderItemChoice model in the database"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
choice_id = db.Column(db.String(64), nullable=True)
|
||||||
|
order_item_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("order_item.id"), nullable=False
|
||||||
|
)
|
||||||
|
kind = db.Column(db.String(1), nullable=False)
|
||||||
|
name = db.Column(db.String(120), nullable=True)
|
||||||
|
value = db.Column(db.String(120), nullable=True)
|
||||||
|
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
def configure(self, order: OrderItem) -> None:
|
||||||
|
"Set the orderitem"
|
||||||
|
self.order = order
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.name}: {self.value}"
|
|
@ -1,25 +0,0 @@
|
||||||
"Script for everything Product related in the database"
|
|
||||||
from models import db
|
|
||||||
|
|
||||||
from .location import Location
|
|
||||||
|
|
||||||
|
|
||||||
class Product(db.Model):
|
|
||||||
"Class used for configuring the Product model in the database"
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
location_id = db.Column(db.Integer, db.ForeignKey("location.id"))
|
|
||||||
name = db.Column(db.String(120), nullable=False)
|
|
||||||
price = db.Column(db.Integer, nullable=False)
|
|
||||||
orderItems = db.relationship("OrderItem",
|
|
||||||
backref="product", lazy="dynamic")
|
|
||||||
|
|
||||||
def configure(self, location: Location, name: str, price: int) -> None:
|
|
||||||
"Configure the Product"
|
|
||||||
# pylint: disable=W0201
|
|
||||||
self.location = location
|
|
||||||
self.name = name
|
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "%s (€%d)from %s" % (self.name, self.price / 100,
|
|
||||||
self.location or "None",)
|
|
|
@ -1,26 +1,41 @@
|
||||||
"Script for everything User related in the database"
|
"Script for everything User related in the database"
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from models import db
|
from models import db
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
class User(db.Model):
|
||||||
"Class used for configuring the User model in the database"
|
"""Class used for configuring the User model in the database"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
admin = db.Column(db.Boolean)
|
admin = db.Column(db.Boolean)
|
||||||
bias = db.Column(db.Integer)
|
bias = db.Column(db.Integer)
|
||||||
|
# Microsoft OAUTH info
|
||||||
|
microsoft_uuid = db.Column(db.String(120), unique=True)
|
||||||
|
# Association logic
|
||||||
|
associations = db.Column(db.String(255), nullable=False, server_default="")
|
||||||
|
|
||||||
|
# Relations
|
||||||
runs = db.relation(
|
runs = db.relation(
|
||||||
"Order",
|
"Order",
|
||||||
backref="courrier",
|
backref="courier",
|
||||||
primaryjoin="Order.courrier_id==User.id",
|
primaryjoin="Order.courier_id==User.id",
|
||||||
foreign_keys="Order.courrier_id",
|
foreign_keys="Order.courier_id",
|
||||||
)
|
)
|
||||||
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
|
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
|
||||||
|
|
||||||
def configure(self, username: str, admin: bool, bias: int) -> None:
|
def association_list(self) -> List[str]:
|
||||||
"Configure the User"
|
return self.associations.split(",")
|
||||||
|
|
||||||
|
def configure(self, username: str, admin: bool, bias: int, *, microsoft_uuid: str = None, associations: Optional[List[str]] = None) -> None:
|
||||||
|
"""Configure the User"""
|
||||||
|
if associations is None:
|
||||||
|
associations = []
|
||||||
self.username = username
|
self.username = username
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.bias = bias
|
self.bias = bias
|
||||||
|
self.microsoft_uuid = microsoft_uuid
|
||||||
|
self.associations = ",".join(associations)
|
||||||
|
|
||||||
# pylint: disable=C0111, R0201
|
# pylint: disable=C0111, R0201
|
||||||
def is_authenticated(self) -> bool:
|
def is_authenticated(self) -> bool:
|
||||||
|
@ -39,4 +54,4 @@ class User(db.Model):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "%s" % self.username
|
return f"{self.username}"
|
||||||
|
|
|
@ -10,41 +10,41 @@ from flask import url_for
|
||||||
from models.order import Order
|
from models.order import Order
|
||||||
|
|
||||||
|
|
||||||
def webhook_text(order_item: Order) -> typing.Optional[str]:
|
def webhook_text(order: Order) -> typing.Optional[str]:
|
||||||
"Function that makes the text for the notification"
|
"""Function that makes the text for the notification"""
|
||||||
if "Testlocation" in order_item.location.name:
|
if order.location_id == "test":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if order_item.courrier is not None:
|
if order.courier is not None:
|
||||||
# pylint: disable=C0301
|
# pylint: disable=C0301, C0209
|
||||||
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
|
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
|
||||||
url_for("order_bp.order_from_id", order_id=order_item.id, _external=True),
|
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
|
||||||
order_item.location.name,
|
order.location_name,
|
||||||
remaining_minutes(order_item.stoptime),
|
remaining_minutes(order.stoptime),
|
||||||
order_item.courrier.username.title(),
|
order.courier.username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=C0209
|
||||||
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
|
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
|
||||||
order_item.location.name,
|
order.location_name,
|
||||||
remaining_minutes(order_item.stoptime),
|
remaining_minutes(order.stoptime),
|
||||||
url_for("order_bp.order_from_id", order_id=order_item.id, _external=True),
|
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_order_to_webhook(order_item: Order) -> None:
|
def post_order_to_webhook(order: Order) -> None:
|
||||||
"Function that sends the notification for the order"
|
"""Function that sends the notification for the order"""
|
||||||
message = webhook_text(order_item)
|
message = webhook_text(order)
|
||||||
if message:
|
if message:
|
||||||
webhookthread = WebhookSenderThread(
|
webhookthread = WebhookSenderThread(message, app.config["SLACK_WEBHOOK"])
|
||||||
message, app.config["SLACK_WEBHOOK"])
|
|
||||||
webhookthread.start()
|
webhookthread.start()
|
||||||
|
|
||||||
|
|
||||||
class WebhookSenderThread(Thread):
|
class WebhookSenderThread(Thread):
|
||||||
"Extension of the Thread class, which sends a webhook for the notification"
|
"""Extension of the Thread class, which sends a webhook for the notification"""
|
||||||
|
|
||||||
def __init__(self, message: str, url: str) -> None:
|
def __init__(self, message: str, url: str) -> None:
|
||||||
super(WebhookSenderThread, self).__init__()
|
super().__init__()
|
||||||
self.message = message
|
self.message = message
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class WebhookSenderThread(Thread):
|
||||||
self.slack_webhook()
|
self.slack_webhook()
|
||||||
|
|
||||||
def slack_webhook(self) -> None:
|
def slack_webhook(self) -> None:
|
||||||
"The webhook for the specified chat platform"
|
"""The webhook for the specified chat platform"""
|
||||||
if self.url:
|
if self.url:
|
||||||
requests.post(self.url, json={"text": self.message})
|
requests.post(self.url, json={"text": self.message})
|
||||||
else:
|
else:
|
||||||
|
@ -60,9 +60,9 @@ class WebhookSenderThread(Thread):
|
||||||
|
|
||||||
|
|
||||||
def remaining_minutes(value) -> str:
|
def remaining_minutes(value) -> str:
|
||||||
"Return the remaining minutes until the deadline of and order"
|
"""Return the remaining minutes until the deadline of and order"""
|
||||||
delta = value - datetime.now()
|
delta = value - datetime.now()
|
||||||
if delta.total_seconds() < 0:
|
if delta.total_seconds() < 0:
|
||||||
return "0"
|
return "0"
|
||||||
minutes = delta.total_seconds() // 60
|
minutes = int(delta.total_seconds() // 60)
|
||||||
return "%02d" % minutes
|
return f"{minutes:02}"
|
||||||
|
|
31
app/parse_hlds.py
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"Module used for parsing the HLDS files"
|
||||||
|
|
||||||
|
from hlds.parser import parse_files
|
||||||
|
|
||||||
|
USAGE = """{0} [filename]...
|
||||||
|
Parse HLDS files, print as JSON
|
||||||
|
|
||||||
|
Without arguments, parse the default definitions.
|
||||||
|
With filenames as arguments, parse those files as HLDS.
|
||||||
|
|
||||||
|
{} --help Print this help text"""
|
||||||
|
|
||||||
|
|
||||||
|
def main(filenames):
|
||||||
|
if filenames:
|
||||||
|
location_definitions = parse_files(filenames)
|
||||||
|
else:
|
||||||
|
from hlds.definitions import location_definitions
|
||||||
|
|
||||||
|
print("\n\n".join(map(str, location_definitions)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if "-h" in args or "--help" in args:
|
||||||
|
print(USAGE.format(sys.argv[0]), file=sys.stderr)
|
||||||
|
else:
|
||||||
|
main(args)
|
|
@ -1,16 +1,26 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"Script to run Haldis on a server"
|
"""
|
||||||
|
Used by Zeus in production.
|
||||||
|
This script makes Haldis acceptable to Phusion Passenger assuming a setup like on Zeus servers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# User has the virtual environment in ~/env/
|
||||||
INTERP = os.path.expanduser("~/env/bin/python3")
|
INTERP = os.path.expanduser("~/env/bin/python3")
|
||||||
if sys.executable != INTERP:
|
if sys.executable != INTERP:
|
||||||
os.execl(INTERP, INTERP, *sys.argv)
|
os.execl(INTERP, INTERP, *sys.argv)
|
||||||
|
|
||||||
sys.path.append(os.getcwd())
|
sys.path.append(os.getcwd())
|
||||||
|
|
||||||
|
# Phusion Passenger expects this file to be called `passenger_wsgi.py`
|
||||||
|
# and the WSGI object to be called `application`
|
||||||
from app import create_app
|
from app import create_app
|
||||||
application = create_app().app
|
|
||||||
|
application, appmgr = create_app()
|
||||||
|
|
||||||
# For running on the server with passenger etc
|
# For running on the server with passenger etc
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
9
app/static/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/static/icons/mstile-150x150.png"/>
|
||||||
|
<TileColor>#1c1c1c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
366
app/static/css/bootstrap-datetimepicker.css
vendored
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
/*!
|
||||||
|
* Datetimepicker for Bootstrap 3
|
||||||
|
* ! version : 4.7.14
|
||||||
|
* https://github.com/Eonasdan/bootstrap-datetimepicker/
|
||||||
|
*/
|
||||||
|
.bootstrap-datetimepicker-widget {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 4px;
|
||||||
|
width: 19em;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu:before,
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-bottom: 7px solid #cccccc;
|
||||||
|
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||||
|
top: -7px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid white;
|
||||||
|
top: -6px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-top: 7px solid #cccccc;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.2);
|
||||||
|
bottom: -7px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 6px solid white;
|
||||||
|
bottom: -6px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
|
||||||
|
left: auto;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
|
||||||
|
left: auto;
|
||||||
|
right: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .list-unstyled {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget a[data-action] {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget a[data-action]:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-hour,
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-minute,
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-second {
|
||||||
|
width: 54px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget button[data-action] {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Increment Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Increment Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Decrement Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Decrement Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Show Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Show Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Toggle AM/PM";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Clear the picker";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Set the date to today";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Toggle Date and Time Screens";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch td {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch td span {
|
||||||
|
line-height: 2.5;
|
||||||
|
height: 2.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td,
|
||||||
|
.bootstrap-datetimepicker-widget table th {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.picker-switch {
|
||||||
|
width: 145px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table th.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.prev::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Previous Month";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.next::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Next Month";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table thead tr:first-child th {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td {
|
||||||
|
height: 54px;
|
||||||
|
line-height: 54px;
|
||||||
|
width: 54px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.cw {
|
||||||
|
font-size: .8em;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.day {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.day:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.hour:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.minute:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.second:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.old,
|
||||||
|
.bootstrap-datetimepicker-widget table td.new {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.today {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.today:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
border: 0 0 7px 7px solid transparent;
|
||||||
|
border-bottom-color: #337ab7;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.2);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.active,
|
||||||
|
.bootstrap-datetimepicker-widget table td.active:hover {
|
||||||
|
background-color: #337ab7;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.active.today:before {
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table td.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
line-height: 54px;
|
||||||
|
margin: 2px 1.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.active {
|
||||||
|
background-color: #337ab7;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.old {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table td span.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
|
||||||
|
height: 27px;
|
||||||
|
line-height: 27px;
|
||||||
|
}
|
||||||
|
.input-group.date .input-group-addon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
366
app/static/css/bootstrap-datetimepicker.min.css
vendored
|
@ -1,366 +0,0 @@
|
||||||
/*!
|
|
||||||
* Datetimepicker for Bootstrap 3
|
|
||||||
* ! version : 4.7.14
|
|
||||||
* https://github.com/Eonasdan/bootstrap-datetimepicker/
|
|
||||||
*/
|
|
||||||
.bootstrap-datetimepicker-widget {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu {
|
|
||||||
margin: 2px 0;
|
|
||||||
padding: 4px;
|
|
||||||
width: 19em;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
|
||||||
width: 38em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
|
||||||
width: 38em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
|
||||||
width: 38em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu:before,
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu:after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
border-bottom: 7px solid #cccccc;
|
|
||||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
|
||||||
top: -7px;
|
|
||||||
left: 7px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid white;
|
|
||||||
top: -6px;
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
border-top: 7px solid #cccccc;
|
|
||||||
border-top-color: rgba(0, 0, 0, 0.2);
|
|
||||||
bottom: -7px;
|
|
||||||
left: 6px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-top: 6px solid white;
|
|
||||||
bottom: -6px;
|
|
||||||
left: 7px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
|
|
||||||
left: auto;
|
|
||||||
right: 6px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
|
|
||||||
left: auto;
|
|
||||||
right: 7px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .list-unstyled {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget a[data-action] {
|
|
||||||
padding: 6px 0;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget a[data-action]:active {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .timepicker-hour,
|
|
||||||
.bootstrap-datetimepicker-widget .timepicker-minute,
|
|
||||||
.bootstrap-datetimepicker-widget .timepicker-second {
|
|
||||||
width: 54px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget button[data-action] {
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Increment Hours";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Increment Minutes";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Decrement Hours";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Decrement Minutes";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Show Hours";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Show Minutes";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Toggle AM/PM";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Clear the picker";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Set the date to today";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .picker-switch {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .picker-switch::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Toggle Date and Time Screens";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .picker-switch td {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget .picker-switch td span {
|
|
||||||
line-height: 2.5;
|
|
||||||
height: 2.5em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td,
|
|
||||||
.bootstrap-datetimepicker-widget table th {
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table th {
|
|
||||||
height: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table th.picker-switch {
|
|
||||||
width: 145px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table th.disabled,
|
|
||||||
.bootstrap-datetimepicker-widget table th.disabled:hover {
|
|
||||||
background: none;
|
|
||||||
color: #777777;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table th.prev::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Previous Month";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table th.next::after {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
content: "Next Month";
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table thead tr:first-child th {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td {
|
|
||||||
height: 54px;
|
|
||||||
line-height: 54px;
|
|
||||||
width: 54px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.cw {
|
|
||||||
font-size: .8em;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #777777;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.day {
|
|
||||||
height: 20px;
|
|
||||||
line-height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.day:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.hour:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.minute:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.second:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.old,
|
|
||||||
.bootstrap-datetimepicker-widget table td.new {
|
|
||||||
color: #777777;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.today {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.today:before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
border: 0 0 7px 7px solid transparent;
|
|
||||||
border-bottom-color: #337ab7;
|
|
||||||
border-top-color: rgba(0, 0, 0, 0.2);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 4px;
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.active,
|
|
||||||
.bootstrap-datetimepicker-widget table td.active:hover {
|
|
||||||
background-color: #337ab7;
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.active.today:before {
|
|
||||||
border-bottom-color: #fff;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td.disabled,
|
|
||||||
.bootstrap-datetimepicker-widget table td.disabled:hover {
|
|
||||||
background: none;
|
|
||||||
color: #777777;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td span {
|
|
||||||
display: inline-block;
|
|
||||||
width: 54px;
|
|
||||||
height: 54px;
|
|
||||||
line-height: 54px;
|
|
||||||
margin: 2px 1.5px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td span:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td span.active {
|
|
||||||
background-color: #337ab7;
|
|
||||||
color: #ffffff;
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td span.old {
|
|
||||||
color: #777777;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table td span.disabled,
|
|
||||||
.bootstrap-datetimepicker-widget table td span.disabled:hover {
|
|
||||||
background: none;
|
|
||||||
color: #777777;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
|
|
||||||
height: 27px;
|
|
||||||
line-height: 27px;
|
|
||||||
}
|
|
||||||
.input-group.date .input-group-addon {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
2
app/static/css/bootstrap.min.css
vendored
|
@ -1,4 +1,4 @@
|
||||||
* bootswatch v3.3.4+1
|
/* bootswatch v3.3.4+1
|
||||||
* Homepage: http://bootswatch.com
|
* Homepage: http://bootswatch.com
|
||||||
* Copyright 2012-2015 Thomas Park
|
* Copyright 2012-2015 Thomas Park
|
||||||
* Licensed under MIT
|
* Licensed under MIT
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#D0D0D8;
|
|
||||||
--dGray1:#8E8E93;
|
|
||||||
--dGray2:#636366;
|
|
||||||
--dGray3:#48484A;
|
|
||||||
--dGray4:#3A3A3C;
|
|
||||||
--dGray5:#2C2C2E;
|
|
||||||
--dGray6:#1C1C1E;
|
|
||||||
--dBlue:#0A84FF;
|
|
||||||
}
|
|
||||||
.table-hover tbody tr:hover{
|
|
||||||
background-color: var(--dGray3);
|
|
||||||
}
|
|
||||||
body{
|
|
||||||
background-color: var(--dGray5);
|
|
||||||
color: var(--dGray1);
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--dBlue);
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
color: var(--dGray6);
|
|
||||||
background-color: var(--dBlue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background-color: var(--dGray6);
|
|
||||||
}
|
|
||||||
.navbar-default .navbar-nav .active a{
|
|
||||||
background-color: var(--dGray4);
|
|
||||||
color: var(--dGray1);
|
|
||||||
}
|
|
||||||
.navbar-default .navbar-nav .active a:hover{
|
|
||||||
background-color: var(--dGray3);
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.navbar-default .navbar-nav li a,.navbar-default .navbar-brand{
|
|
||||||
color: var(--dGray1);
|
|
||||||
}
|
|
||||||
.navbar-default .navbar-nav li a:hover,.navbar-default .navbar-brand:hover{
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
hr{
|
|
||||||
border-top: 1px solid var(--dGray2);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6{
|
|
||||||
color: var(--dGray1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jumbotron, .darker {
|
|
||||||
background-color: var(--dGray4);
|
|
||||||
}
|
|
||||||
.table tbody tr td {
|
|
||||||
border-top: 1px solid var(--dGray3);
|
|
||||||
}
|
|
||||||
.table thead tr th {
|
|
||||||
border-bottom: 2px solid var(--dGray2);
|
|
||||||
}
|
|
||||||
.navbar-toggle .icon-bar {
|
|
||||||
background-color: var(--dGray0);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
.select2-container--default .select2-selection--single{
|
|
||||||
background-color: var(--dGray3);
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__rendered{
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.select2-results__option{
|
|
||||||
background-color: var(--dGray5);
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
|
||||||
background-color: var(--dGray4);
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.day:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.hour:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.minute:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td span:hover,
|
|
||||||
.bootstrap-datetimepicker-widget table td.second:hover {
|
|
||||||
background: var(--dGray4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option[aria-selected=true]{
|
|
||||||
background-color: var(--dBlue);
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.select2-search{
|
|
||||||
background-color: var(--dGray2);
|
|
||||||
}
|
|
||||||
.select2-search input{
|
|
||||||
background-color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.dropdown-menu{
|
|
||||||
background-color: var(--dGray5);
|
|
||||||
}
|
|
||||||
.form-control{
|
|
||||||
color: var(--dGray0);
|
|
||||||
}
|
|
||||||
.form-control::placeholder{
|
|
||||||
color: var(--dGray2);
|
|
||||||
}
|
|
||||||
.enter_darkmode>a {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,16 +1,19 @@
|
||||||
/* Custom CSS */
|
|
||||||
:root {
|
:root {
|
||||||
/* Darkmode colors */
|
/* Darkmode colors */
|
||||||
--dGray0:#D0D0D8;
|
--gray0: #d0d0d8;
|
||||||
--dGray1:#8E8E93;
|
--gray1: #8e8e93;
|
||||||
--dGray2:#636366;
|
--gray2: #636366;
|
||||||
--dGray3:#48484A;
|
--gray3: #48484a;
|
||||||
--dGray4:#3A3A3C;
|
--gray4: #3a3a3c;
|
||||||
--dGray5:#2C2C2E;
|
--gray5: #2c2c2e;
|
||||||
--dGray6:#1C1C1E;
|
--gray6: #1c1c1e;
|
||||||
--dBlue:#0A84FF;
|
--accent: #0a84ff;
|
||||||
--FontFamily:"Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
|
||||||
--FontSize:13px;
|
--bg: #2c2c2e;
|
||||||
|
--navbarBg: #1c1c1e;
|
||||||
|
|
||||||
|
--fontFamily: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
--fontSize: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -19,11 +22,11 @@ html {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
background-color: var(--dGray5);
|
background-color: var(--bg);
|
||||||
color: var(--dGray1);
|
color: var(--gray1);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: var(--FontFamily);
|
font-family: var(--fontFamily);
|
||||||
font-size: var(--FontSize);
|
font-size: var(--fontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
|
@ -47,12 +50,6 @@ body {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.darker {
|
|
||||||
background-color: #fafafa;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.align-bottom {
|
.align-bottom {
|
||||||
margin-top: 2.5em;
|
margin-top: 2.5em;
|
||||||
|
@ -63,14 +60,6 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product .extras {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order_row {
|
|
||||||
background: var(--dGray4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.time_data{
|
.time_data{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -90,18 +79,6 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.showcase .product {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add clickable box */
|
|
||||||
div.box:hover {
|
|
||||||
cursor: hand;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: .9;
|
|
||||||
box-shadow: 2px 4px 4px -1px #888888;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.divLink {
|
a.divLink {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -135,68 +112,87 @@ a.divLink {
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover{
|
.table-hover tbody tr:hover{
|
||||||
background-color: var(--dGray3);
|
background-color: var(--gray4);
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: var(--dBlue);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: var(--dGray6);
|
color: var(--gray6);
|
||||||
background-color: var(--dBlue);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: var(--dGray6);
|
background-color: var(--navbarBg);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav .active a {
|
.navbar-default .navbar-nav .active a {
|
||||||
background-color: var(--dGray4);
|
background-color: var(--gray5);
|
||||||
color: var(--dGray1);
|
color: var(--gray1);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav .active a:hover{
|
.navbar-default .navbar-nav .active a:hover,
|
||||||
background-color: var(--dGray3);
|
.navbar-default .navbar-nav .active a:focus,
|
||||||
color: var(--dGray0);
|
.navbar-default .navbar-nav > li > a:hover,
|
||||||
|
.navbar-default .navbar-nav > li > a:focus {
|
||||||
|
background-color: var(--gray4);
|
||||||
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav li a,.navbar-default .navbar-brand{
|
.navbar-default .navbar-nav li a,
|
||||||
color: var(--dGray1);
|
.navbar-default .navbar-brand {
|
||||||
|
color: var(--gray1);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav li a:hover,.navbar-default .navbar-brand:hover{
|
.navbar-default .navbar-nav li a:hover,
|
||||||
color: var(--dGray0);
|
.navbar-default .navbar-nav li a:focus,
|
||||||
|
.navbar-default .navbar-brand:hover,
|
||||||
|
.navbar-default .navbar-brand:focus {
|
||||||
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
hr {
|
hr {
|
||||||
border-top: 1px solid var(--dGray2);
|
border-top: 1px solid var(--gray2);
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 23px;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 23px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6{
|
h1, h2, h3, h4, h5, h6{
|
||||||
color: var(--dGray1);
|
color: var(--gray1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron, .darker {
|
.jumbotron, .darker, .order_row {
|
||||||
background-color: var(--dGray4);
|
background: var(--gray5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.darker {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.table tbody tr td {
|
.table tbody tr td {
|
||||||
border-top: 1px solid var(--dGray3);
|
border-top: 1px solid var(--gray3);
|
||||||
}
|
}
|
||||||
.table thead tr th {
|
.table thead tr th {
|
||||||
border-bottom: 2px solid var(--dGray2);
|
border-bottom: 2px solid var(--gray2);
|
||||||
}
|
}
|
||||||
.navbar-toggle .icon-bar {
|
.navbar-toggle .icon-bar {
|
||||||
background-color: var(--dGray0);
|
background-color: var(--gray0);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.select2-container--default .select2-selection--single{
|
.select2-container--default .select2-selection--single{
|
||||||
background-color: var(--dGray3);
|
background-color: var(--gray3);
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.select2-container--default .select2-selection--single .select2-selection__rendered{
|
.select2-container--default .select2-selection--single .select2-selection__rendered{
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.select2-results__option{
|
.select2-results__option{
|
||||||
background-color: var(--dGray5);
|
background-color: var(--gray5);
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||||
background-color: var(--dGray4);
|
background-color: var(--gray4);
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover,
|
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover,
|
||||||
.bootstrap-datetimepicker-widget table td.day:hover,
|
.bootstrap-datetimepicker-widget table td.day:hover,
|
||||||
|
@ -204,28 +200,52 @@ h1, h2, h3, h4, h5, h6{
|
||||||
.bootstrap-datetimepicker-widget table td.minute:hover,
|
.bootstrap-datetimepicker-widget table td.minute:hover,
|
||||||
.bootstrap-datetimepicker-widget table td span:hover,
|
.bootstrap-datetimepicker-widget table td span:hover,
|
||||||
.bootstrap-datetimepicker-widget table td.second:hover {
|
.bootstrap-datetimepicker-widget table td.second:hover {
|
||||||
background: var(--dGray4);
|
background: var(--gray4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default .select2-results__option[aria-selected=true]{
|
.select2-container--default .select2-results__option[aria-selected=true]{
|
||||||
background-color: var(--dBlue);
|
background-color: var(--accent);
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.select2-search{
|
.select2-search{
|
||||||
background-color: var(--dGray2);
|
background-color: var(--gray2);
|
||||||
|
}
|
||||||
|
.select2-selection--multiple .select2-search{
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.select2-selection--multiple .select2-search{
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.select2-selection--multiple .select2-search.select2-search--inline .select2-search__field{
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
.select2-search input{
|
.select2-search input{
|
||||||
background-color: var(--dGray0);
|
background-color: var(--gray0);
|
||||||
}
|
}
|
||||||
.dropdown-menu{
|
.dropdown-menu{
|
||||||
background-color: var(--dGray5);
|
background-color: var(--gray5);
|
||||||
}
|
}
|
||||||
.form-control{
|
.form-control{
|
||||||
color: var(--dGray0);
|
color: var(--gray0);
|
||||||
}
|
}
|
||||||
.form-control::placeholder{
|
.form-control::placeholder{
|
||||||
color: var(--dGray2);
|
color: var(--gray2);
|
||||||
}
|
}
|
||||||
.enter_darkmode>a {
|
|
||||||
text-align: center;
|
.dish-choices summary {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
details summary:before {
|
||||||
|
font-style: normal;
|
||||||
|
content: "▸";
|
||||||
|
padding-right: 0.4em;
|
||||||
|
}
|
||||||
|
details[open] summary:before {
|
||||||
|
content: "▾";
|
||||||
}
|
}
|
||||||
|
|
109
app/static/css/shop_view.css
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
:root {
|
||||||
|
--bg: #eee;
|
||||||
|
--ticketBg: #fff;
|
||||||
|
--fg: #000;
|
||||||
|
--dashes: 1px dashed #444;
|
||||||
|
--fontFamily: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
--fontSize: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--fontFamily);
|
||||||
|
font-size: var(--fontSize);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket {
|
||||||
|
background: var(--ticketBg);
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 25px auto;
|
||||||
|
margin: 7vh auto;
|
||||||
|
box-shadow: 0 0.15em 0.3em rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
body {
|
||||||
|
background: var(--ticketBg);
|
||||||
|
}
|
||||||
|
.ticket {
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 200%;
|
||||||
|
margin: 0 ;
|
||||||
|
padding: 0.3em 0 0.3em;
|
||||||
|
border-bottom: var(--dashes);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 110%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-order-warning {
|
||||||
|
font-size: 150%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
background-color: rgba(255, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dish {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
@media (min-width: 500px) {
|
||||||
|
.dish {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity {
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
.comments li {
|
||||||
|
margin: 0.5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment_part {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-right: 0.4em;
|
||||||
|
border-radius: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.comment_part_separator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
border-top: var(--dashes);
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
margin-top: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time_data{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
23
app/static/css/themes/Makefile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Find a SASS preprocessor
|
||||||
|
SASS := $(shell command -v sass 2>/dev/null)
|
||||||
|
ifndef SASS
|
||||||
|
SASS := $(shell command -v sassc 2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifndef SASS
|
||||||
|
$(error No SASS preprocessor found)
|
||||||
|
endif
|
||||||
|
|
||||||
|
SASSFLAGS := --sourcemap=auto --
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
# Detect SCSS files, use their CSS counterparts as `all` target
|
||||||
|
SCSS_FILES := $(wildcard *.scss)
|
||||||
|
all: $(SCSS_FILES:%.scss=%.css)
|
||||||
|
|
||||||
|
# Teach Make how to convert SCSS to CSS
|
||||||
|
%.css: %.scss
|
||||||
|
$(SASS) $(SASSFLAGS) $< $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(SCSS_FILES:%.scss=%.css) $(SCSS_FILES:%.scss=%.css.map)
|
|
@ -9,22 +9,21 @@ Enige discretie is aangeraden.
|
||||||
---=§[ Arnhoudt ]§=---
|
---=§[ Arnhoudt ]§=---
|
||||||
|
|
||||||
*/
|
*/
|
||||||
/*high performance kerstmis*/
|
|
||||||
:root {
|
:root {
|
||||||
/*Darkmode colors*/
|
--gray0: #F28705;
|
||||||
--dGray0:#F28705;
|
--gray1: white;
|
||||||
--dGray1:white;
|
--gray2: #590212;
|
||||||
--dGray2:#590212;
|
--gray3: #590212;
|
||||||
--dGray3:#590212;
|
--gray4: #274001;
|
||||||
--dGray4:#274001;
|
--gray5: #274001;
|
||||||
--dGray5:#274001;
|
--gray6: #F2778D;
|
||||||
--dGray6:#F2778D;
|
--accent: #F2778D;
|
||||||
--dBlue:#F2778D; }
|
--bg: #2F0000;
|
||||||
|
--navbarBg: #F2778D;
|
||||||
|
--fontFamily: Radikal, Optima, Segoe, Segoe UI, Candara, Calibri, Arial, sans-serif; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%; }
|
||||||
font-family: Radikal,Optima,Segoe,Segoe UI,Candara,Calibri,Arial,sans-serif;
|
|
||||||
background-color: #2F0000; }
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Radikal;
|
font-family: Radikal;
|
||||||
|
@ -432,7 +431,7 @@ footer > hr {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
filter: blur(1.5px);
|
filter: blur(1.5px);
|
||||||
box-shadow: 71vw 89.7vh 0 -0.13rem#fff,37.6vw 73.2vh 0 -0.46rem#fff,31.1vw 76.6vh 0 -0.42rem#fff,41.5vw 94vh 0 -0.3rem#fff,76.7vw 28.6vh 0 -0.18rem#fff,21.7vw 70.7vh 0 -0.34rem#fff,14.6vw 72.9vh 0 -0.04rem#fff,72vw 50vh 0 -0.12rem#fff,89.6vw 90.4vh 0 -0.45rem#fff,49.7vw 21.5vh 0 -0.02rem#fff,15.8vw 87.2vh 0 -0.04rem#fff,46.3vw 85.9vh 0 -0.24rem#fff,91.6vw 7.4vh 0 -0.11rem#fff,83.3vw 60.5vh 0 -0.06rem#fff,15.2vw 3.2vh 0 -0.31rem#fff,52.9vw 54.1vh 0 -0.48rem#fff,58.9vw 42.1vh 0 -0.07rem#fff,40.7vw 10.5vh 0 -0.25rem#fff,53.7vw 74.5vh 0 -0.33rem#fff,89vw 50.9vh 0 -0.2rem#fff,35vw 22.6vh 0 -0.2rem#fff,18.9vw 66.4vh 0 -0.33rem#fff,66.8vw 22.9vh 0 -0.05rem#fff,34.3vw 46.9vh 0 -0.26rem#fff,72.9vw 33.1vh 0 -0.25rem#fff,23.2vw 2.5vh 0 -0.16rem#fff,60.3vw 21.6vh 0 -0.35rem#fff,14.3vw 0.6vh 0 -0.09rem#fff,95.6vw 72.2vh 0 -0.03rem#fff,4vw 28.6vh 0 -0.17rem#fff,40.8vw 67.7vh 0 -0.16rem#fff,85vw 88.1vh 0 -0.41rem#fff,37.4vw 50.1vh 0 -0.07rem#fff,50.8vw 39.8vh 0 -0.01rem#fff,14.4vw 95.1vh 0 -0.23rem#fff,77.7vw 10.2vh 0 -0.14rem#fff,35.1vw 59vh 0 -0.03rem#fff,45.8vw 42.4vh 0 -0.37rem#fff,48.3vw 51.3vh 0 -0.45rem#fff,98.3vw 5.8vh 0 -0.26rem#fff,2.4vw 88vh 0 -0.17rem#fff,16vw 49.1vh 0 -0.39rem#fff,76.6vw 42.6vh 0 -0.25rem#fff,17.2vw 44.5vh 0 -0.1rem#fff,51.2vw 73.7vh 0 -0.33rem#fff,31.7vw 59.2vh 0 -0.47rem#fff,32.4vw 68.9vh 0 -0.07rem#fff,3.7vw 94.8vh 0 -0.33rem#fff,55.3vw 3.8vh 0 -0.24rem#fff,25.3vw 81.2vh 0 -0.21rem#fff,68.2vw 97.6vh 0 -0.27rem#fff,43.4vw 56.5vh 0 -0.06rem#fff,40.6vw 98.7vh 0 -0.49rem#fff,41.2vw 37.2vh 0 -0.22rem#fff,66.7vw 21.2vh 0 -0.32rem#fff,3.6vw 75.9vh 0 -0.04rem#fff,66.2vw 71.2vh 0 -0.33rem#fff,30.6vw 59.9vh 0 -0.02rem#fff,22.6vw 72.1vh 0 -0.02rem#fff,93.9vw 9.7vh 0 -0.19rem#fff,99.1vw 73.3vh 0 -0.07rem#fff,48.4vw 94.2vh 0 -0.33rem#fff,44.1vw 55.1vh 0 -0.35rem#fff,98.3vw 34vh 0 -0.07rem#fff,85.1vw 30.4vh 0 -0.42rem#fff,65.2vw 21.4vh 0 -0.05rem#fff,72.1vw 92.9vh 0 -0.48rem#fff,25.8vw 53.6vh 0 -0.02rem#fff,13.8vw 12.7vh 0 -0.26rem#fff,79.4vw 94.9vh 0 -0.06rem#fff,41.8vw 56.2vh 0 -0.31rem#fff,67.4vw 20.2vh 0 -0.34rem#fff,35.6vw 88.7vh 0 -0.1rem#fff,12.5vw 20.1vh 0 -0.06rem#fff,30.3vw 32.7vh 0 -0.34rem#fff,51.4vw 84.2vh 0 -0.15rem#fff,16.2vw 80.1vh 0 -0.31rem#fff,6.1vw 14.1vh 0 -0.41rem#fff,86.6vw 55.9vh 0 -0.41rem#fff,43.5vw 75.9vh 0 -0.45rem#fff,77.5vw 20.4vh 0 -0.12rem#fff,67.7vw 97.6vh 0 -0.08rem#fff,0.8vw 18.1vh 0 -0.33rem#fff,60.6vw 21.3vh 0 -0.19rem#fff,70.2vw 79.3vh 0 -0.26rem#fff,50.8vw 68.8vh 0 -0.35rem#fff,53.9vw 12.5vh 0 -0.39rem#fff,76.4vw 45.9vh 0 -0.12rem#fff,11.5vw 58.7vh 0 -0.31rem#fff,76.3vw 74.7vh 0 -0.4rem#fff,4.7vw 46.7vh 0 -0.39rem#fff,54.5vw 63.6vh 0 -0.28rem#fff,51.6vw 65.9vh 0 -0.3rem#fff,65.9vw 47.6vh 0 -0.08rem#fff,91.6vw 58.8vh 0 -0.12rem#fff,26.9vw 71.6vh 0 -0.36rem#fff,59.7vw 71.2vh 0 -0.37rem#fff,47.1vw 16.2vh 0 -0.14rem#fff,72.4vw 45.7vh 0 -0.06rem#fff,30.8vw 39.3vh 0 -0.38rem#fff;
|
box-shadow: 38.1vw 38.9vh 0 -0.23rem#fff,63.6vw 92.5vh 0 -0.37rem#fff,26.2vw 19vh 0 -0.13rem#fff,43.8vw 53.8vh 0 -0.32rem#fff,18.9vw 78.9vh 0 -0.48rem#fff,78vw 88.3vh 0 -0.45rem#fff,76.2vw 40.9vh 0 -0.35rem#fff,31vw 100vh 0 -0.35rem#fff,30.9vw 40.6vh 0 -0.38rem#fff,78.3vw 68.7vh 0 -0.38rem#fff,90.8vw 12.9vh 0 -0.25rem#fff,80.4vw 98.6vh 0 -0.33rem#fff,8.8vw 75.2vh 0 -0.36rem#fff,45.3vw 50.7vh 0 -0.36rem#fff,79.6vw 2.2vh 0 -0.39rem#fff,4vw 57.4vh 0 -0.24rem#fff,68.4vw 42.8vh 0 -0.38rem#fff,62.4vw 0.5vh 0 -0.04rem#fff,99.8vw 32.8vh 0 -0.41rem#fff,71.4vw 5.4vh 0 -0.15rem#fff,62.8vw 79.2vh 0 -0.18rem#fff,82.1vw 85.2vh 0 -0.18rem#fff,45.1vw 19.1vh 0 -0.4rem#fff,35.2vw 39.2vh 0 -0.03rem#fff,95.6vw 85.1vh 0 -0.06rem#fff,77.3vw 53.1vh 0 -0.36rem#fff,3.2vw 18.3vh 0 -0.43rem#fff,55vw 88.9vh 0 -0.27rem#fff,73.7vw 1.7vh 0 -0.28rem#fff,16.7vw 45vh 0 -0.37rem#fff,78.8vw 65.8vh 0 -0.19rem#fff,10.9vw 64.9vh 0 -0.4rem#fff,42.6vw 84.1vh 0 -0.44rem#fff,4.6vw 77.8vh 0 -0.39rem#fff,40.6vw 37.6vh 0 -0.19rem#fff,82.6vw 51.2vh 0 -0.4rem#fff,15.9vw 87.9vh 0 -0.06rem#fff,7.3vw 98.7vh 0 -0.23rem#fff,91.6vw 20vh 0 -0.48rem#fff,73.6vw 26.2vh 0 -0.32rem#fff,36.3vw 24.8vh 0 -0.39rem#fff,95.3vw 55.1vh 0 -0.12rem#fff,33.1vw 34.4vh 0 -0.09rem#fff,2.7vw 86.9vh 0 -0.25rem#fff,99.8vw 99.6vh 0 -0.06rem#fff,59.3vw 88.2vh 0 -0.48rem#fff,46.5vw 84.2vh 0 -0.4rem#fff,0.9vw 40.4vh 0 -0.04rem#fff,35.8vw 16.7vh 0 -0.41rem#fff,42.7vw 19.1vh 0 -0.25rem#fff,62.9vw 89.8vh 0 -0.43rem#fff,61.6vw 100vh 0 -0.27rem#fff,61.5vw 76.9vh 0 -0.25rem#fff,33.6vw 33.1vh 0 -0.03rem#fff,59.5vw 15.7vh 0 -0.03rem#fff,13.1vw 17.1vh 0 -0.41rem#fff,39.4vw 27.9vh 0 -0.13rem#fff,70.6vw 15.5vh 0 -0.41rem#fff,29vw 27.7vh 0 -0.46rem#fff,88.2vw 64.1vh 0 -0.33rem#fff,93.3vw 42.2vh 0 -0.38rem#fff,52.4vw 55vh 0 -0.01rem#fff,48.2vw 21.7vh 0 -0.4rem#fff,16.2vw 48.9vh 0 -0.36rem#fff,36.3vw 24.2vh 0 -0.24rem#fff,28.9vw 39.6vh 0 -0.32rem#fff,50.2vw 5.1vh 0 -0.11rem#fff,67.5vw 54.6vh 0 -0.12rem#fff,44.1vw 24.5vh 0 -0.23rem#fff,91.4vw 27.6vh 0 -0.1rem#fff,93.2vw 42.3vh 0 -0.46rem#fff,59.6vw 42.4vh 0 -0.29rem#fff,88vw 39.1vh 0 -0.03rem#fff,74vw 5.6vh 0 -0.31rem#fff,49.7vw 0.1vh 0 -0.36rem#fff,93.1vw 47.2vh 0 -0.21rem#fff,54.8vw 59.2vh 0 -0.38rem#fff,27.1vw 71.9vh 0 -0.3rem#fff,20.8vw 10.8vh 0 -0.46rem#fff,20.2vw 10vh 0 -0.45rem#fff,48.9vw 79.3vh 0 -0.36rem#fff,73.3vw 21.8vh 0 -0.2rem#fff,68.4vw 21.6vh 0 -0.06rem#fff,90.1vw 87.1vh 0 -0.24rem#fff,41.6vw 99.4vh 0 -0.03rem#fff,51.4vw 58.8vh 0 -0.43rem#fff,45.7vw 3.5vh 0 -0.14rem#fff,64.7vw 99.3vh 0 -0.31rem#fff,4vw 48.3vh 0 -0.42rem#fff,86.8vw 87.5vh 0 -0.19rem#fff,81.2vw 56.6vh 0 -0.48rem#fff,95.2vw 83.9vh 0 -0.05rem#fff,30.6vw 15.7vh 0 -0.1rem#fff,32.8vw 24.3vh 0 -0.39rem#fff,83.3vw 7.8vh 0 -0.34rem#fff,41.3vw 13.3vh 0 -0.23rem#fff,63.4vw 16.5vh 0 -0.08rem#fff,57.6vw 98.5vh 0 -0.22rem#fff,78.4vw 63.8vh 0 -0.08rem#fff,49vw 29.5vh 0 -0.01rem#fff;
|
||||||
animation-duration: 18s; }
|
animation-duration: 18s; }
|
||||||
|
|
||||||
.layer1.a {
|
.layer1.a {
|
||||||
|
@ -442,7 +441,7 @@ footer > hr {
|
||||||
width: 0.8rem;
|
width: 0.8rem;
|
||||||
height: 0.8rem;
|
height: 0.8rem;
|
||||||
filter: blur(3px);
|
filter: blur(3px);
|
||||||
box-shadow: 71.8vw 15.9vh 0 -0.3rem#fff,4.2vw 8.1vh 0 -0.41rem#fff,67.5vw 49.2vh 0 -0.28rem#fff,72vw 97.8vh 0 -0.22rem#fff,78.1vw 28vh 0 -0.41rem#fff,34.2vw 51.7vh 0 -0.47rem#fff,37.9vw 76.5vh 0 -0.39rem#fff,92.6vw 36.3vh 0 -0.22rem#fff,59.9vw 8.6vh 0 -0.07rem#fff,32.5vw 74vh 0 -0.07rem#fff,75.7vw 81.6vh 0 -0.12rem#fff,1.7vw 18.5vh 0 -0.2rem#fff,12.3vw 64.7vh 0 -0.37rem#fff,83.9vw 47vh 0 -0.12rem#fff,33.8vw 21.3vh 0 -0.14rem#fff,6.5vw 92.5vh 0 -0.29rem#fff,72.2vw 60.8vh 0 -0.17rem#fff,38.4vw 16vh 0 -0.17rem#fff,23.9vw 58.4vh 0 -0.32rem#fff,40.2vw 52.4vh 0 -0.23rem#fff,48.9vw 74.6vh 0 -0.03rem#fff,61.5vw 36.1vh 0 -0.49rem#fff,60.9vw 94.5vh 0 -0.42rem#fff,2.8vw 59.7vh 0 -0.27rem#fff,62.6vw 32.4vh 0 -0.45rem#fff,91.3vw 18vh 0 -0.09rem#fff,35.9vw 35.5vh 0 -0.11rem#fff,60.2vw 95.5vh 0 -0.21rem#fff,19.8vw 46.5vh 0 -0.09rem#fff,57.7vw 20.7vh 0 -0.03rem#fff,69.8vw 33.8vh 0 -0.34rem#fff,69.1vw 71.5vh 0 -0.45rem#fff,70.5vw 87.9vh 0 -0.5rem#fff,77vw 34.9vh 0 -0.43rem#fff,35.6vw 28.7vh 0 -0.14rem#fff,28.6vw 22.7vh 0 -0.47rem#fff,46.3vw 7.1vh 0 -0.06rem#fff,66.6vw 92.6vh 0 -0.32rem#fff,96.5vw 19.1vh 0 -0.12rem#fff,24.6vw 45.5vh 0 -0.27rem#fff,71.6vw 85.8vh 0 -0.16rem#fff,1.1vw 44.9vh 0 -0.36rem#fff,94.4vw 84.2vh 0 -0.19rem#fff,92.2vw 20.2vh 0 -0.27rem#fff,42.5vw 66.1vh 0 -0.46rem#fff,70.8vw 98.4vh 0 -0.01rem#fff,71.7vw 99vh 0 -0.19rem#fff,0.2vw 34.1vh 0 -0.22rem#fff,87.3vw 30.7vh 0 -0.09rem#fff,80.3vw 93.8vh 0 -0.41rem#fff,72.2vw 8.3vh 0 -0.09rem#fff,27.1vw 46.1vh 0 -0.15rem#fff,10.2vw 93.6vh 0 -0.42rem#fff,76vw 51.5vh 0 -0.26rem#fff,28.7vw 76.3vh 0 -0.11rem#fff,85.1vw 21.2vh 0 -0.46rem#fff,25.9vw 82.2vh 0 -0.34rem#fff,32.3vw 69.8vh 0 -0.29rem#fff,97.3vw 56.8vh 0 -0.26rem#fff,48.2vw 29.6vh 0 -0.28rem#fff,76.2vw 61.9vh 0 -0.1rem#fff,62vw 96vh 0 -0.01rem#fff,76vw 79.9vh 0 -0.36rem#fff,59.9vw 86.8vh 0 -0.22rem#fff,72.8vw 92.3vh 0 -0.02rem#fff,62.7vw 55.9vh 0 -0.46rem#fff,81.1vw 53.3vh 0 -0.09rem#fff,42.4vw 20.2vh 0 -0.15rem#fff,72.7vw 95.8vh 0 -0.26rem#fff,67.6vw 11vh 0 -0.08rem#fff,62.5vw 99vh 0 -0.17rem#fff,52.7vw 46.1vh 0 -0.35rem#fff,49.7vw 73vh 0 -0.33rem#fff,1.9vw 25.1vh 0 -0.06rem#fff,25.6vw 11.6vh 0 -0.43rem#fff,9.7vw 28.4vh 0 -0.11rem#fff,52.1vw 87.4vh 0 -0.45rem#fff,94.6vw 3.6vh 0 -0.37rem#fff,67.5vw 67vh 0 -0.29rem#fff,45.4vw 34.9vh 0 -0.02rem#fff,41.6vw 99.6vh 0 -0.34rem#fff,26.3vw 53vh 0 -0.01rem#fff,41.2vw 73.7vh 0 -0.36rem#fff,6.9vw 29.5vh 0 -0.17rem#fff,25.1vw 39.2vh 0 -0.2rem#fff,93.2vw 58.2vh 0 -0.3rem#fff,93.2vw 58.2vh 0 -0.31rem#fff,91.7vw 29vh 0 -0.01rem#fff,90.1vw 25.4vh 0 -0.23rem#fff,12.2vw 98.7vh 0 -0.33rem#fff,88vw 73.3vh 0 -0.29rem#fff,91.5vw 61vh 0 -0.43rem#fff,96.1vw 70.8vh 0 -0.21rem#fff,98.2vw 55.4vh 0 -0.27rem#fff,15.2vw 59vh 0 -0.34rem#fff,66.3vw 83.5vh 0 -0.05rem#fff,49vw 8.5vh 0 -0.47rem#fff,93.3vw 91.7vh 0 -0.17rem#fff,15.4vw 35.4vh 0 -0.47rem#fff,14.3vw 48.5vh 0 -0.44rem#fff;
|
box-shadow: 79.8vw 46.8vh 0 -0.07rem#fff,11.3vw 43.8vh 0 -0.4rem#fff,69.3vw 2.8vh 0 -0.27rem#fff,48.8vw 5.8vh 0 -0.13rem#fff,61.1vw 28vh 0 -0.02rem#fff,74.7vw 11.5vh 0 -0.24rem#fff,57.8vw 84.7vh 0 -0.34rem#fff,79.9vw 69.7vh 0 -0.33rem#fff,12vw 68.1vh 0 -0.49rem#fff,47.9vw 86.4vh 0 -0.23rem#fff,82.5vw 46.9vh 0 -0.24rem#fff,21.6vw 20.8vh 0 -0.14rem#fff,9.7vw 80.1vh 0 -0.29rem#fff,6.1vw 47.4vh 0 -0.17rem#fff,59.1vw 97.7vh 0 -0.19rem#fff,11.7vw 40.7vh 0 -0.46rem#fff,92.2vw 58.5vh 0 -0.2rem#fff,71.6vw 91.9vh 0 -0.01rem#fff,9.5vw 12.5vh 0 -0.33rem#fff,73.1vw 91.5vh 0 -0.13rem#fff,53.5vw 55.7vh 0 -0.01rem#fff,7.5vw 95.5vh 0 -0.48rem#fff,11.2vw 13.7vh 0 -0.08rem#fff,3.4vw 81.1vh 0 -0.5rem#fff,40.9vw 43vh 0 -0.4rem#fff,78.3vw 33.4vh 0 -0.35rem#fff,92.7vw 70.1vh 0 -0.36rem#fff,90.1vw 17.5vh 0 -0.18rem#fff,6.6vw 15.7vh 0 -0.41rem#fff,13.8vw 74.1vh 0 -0.38rem#fff,40vw 11.6vh 0 -0.35rem#fff,90.4vw 70.9vh 0 -0.48rem#fff,82.7vw 73.9vh 0 -0.23rem#fff,91.4vw 44.3vh 0 -0.21rem#fff,85.9vw 95.5vh 0 -0.01rem#fff,96.5vw 63.6vh 0 -0.39rem#fff,23.6vw 10.6vh 0 -0.04rem#fff,0.4vw 0.7vh 0 -0.24rem#fff,51.1vw 61.4vh 0 -0.12rem#fff,2.4vw 52.3vh 0 -0.06rem#fff,99.9vw 25.8vh 0 -0.22rem#fff,20.4vw 18.4vh 0 -0.05rem#fff,28vw 64.9vh 0 -0.1rem#fff,95.2vw 34.5vh 0 -0.05rem#fff,43.9vw 96.6vh 0 -0.23rem#fff,10.3vw 27.4vh 0 -0.11rem#fff,2.6vw 91.2vh 0 -0.41rem#fff,85.3vw 12.9vh 0 -0.2rem#fff,50.4vw 32.7vh 0 -0.38rem#fff,12.1vw 76.7vh 0 -0.4rem#fff,24.1vw 83.3vh 0 -0.32rem#fff,80.2vw 30.9vh 0 -0.24rem#fff,30.8vw 34.7vh 0 -0.48rem#fff,94.6vw 65vh 0 -0.08rem#fff,1.3vw 55.1vh 0 -0.36rem#fff,74.8vw 69.7vh 0 -0.04rem#fff,79.4vw 78.9vh 0 -0.19rem#fff,16.7vw 35.6vh 0 -0.19rem#fff,36vw 13.4vh 0 -0.36rem#fff,77.5vw 44.7vh 0 -0.42rem#fff,88.9vw 90.4vh 0 -0.33rem#fff,83vw 74.8vh 0 -0.3rem#fff,67.2vw 98.4vh 0 -0.14rem#fff,10vw 10.9vh 0 -0.27rem#fff,97.3vw 64.2vh 0 -0.03rem#fff,57.8vw 30.6vh 0 -0.37rem#fff,55.2vw 33.3vh 0 -0.39rem#fff,83.8vw 43.2vh 0 -0.28rem#fff,99.4vw 93.6vh 0 -0.45rem#fff,63.2vw 31.2vh 0 -0.16rem#fff,32.5vw 96.4vh 0 -0.49rem#fff,8.9vw 70.8vh 0 -0.27rem#fff,27vw 2.3vh 0 -0.46rem#fff,26.6vw 52.4vh 0 -0.31rem#fff,39.9vw 24.2vh 0 -0.04rem#fff,32.8vw 86.7vh 0 -0.42rem#fff,85.3vw 11.8vh 0 -0.14rem#fff,69vw 85.4vh 0 -0.19rem#fff,84.5vw 54.2vh 0 -0.09rem#fff,22vw 38.9vh 0 -0.34rem#fff,42.8vw 5.4vh 0 -0.35rem#fff,61.2vw 68.5vh 0 -0.26rem#fff,20.8vw 50.7vh 0 -0.01rem#fff,41.6vw 69.5vh 0 -0.15rem#fff,37.8vw 46.8vh 0 -0.05rem#fff,23vw 9.5vh 0 -0.01rem#fff,1.3vw 84.1vh 0 -0.07rem#fff,59.5vw 95.9vh 0 -0.36rem#fff,1.6vw 49.5vh 0 -0.23rem#fff,40.4vw 67.3vh 0 -0.33rem#fff,84vw 12.2vh 0 -0.27rem#fff,53.5vw 55.2vh 0 -0.39rem#fff,98.1vw 39.7vh 0 -0.33rem#fff,68.5vw 53.8vh 0 -0.33rem#fff,6.8vw 4.4vh 0 -0.45rem#fff,35.9vw 51.8vh 0 -0.11rem#fff,42.1vw 77.1vh 0 -0.21rem#fff,9.8vw 37.4vh 0 -0.01rem#fff,99.2vw 13.4vh 0 -0.26rem#fff,88.2vw 21vh 0 -0.33rem#fff;
|
||||||
animation-duration: 24s; }
|
animation-duration: 24s; }
|
||||||
|
|
||||||
.layer2.a {
|
.layer2.a {
|
||||||
|
@ -452,7 +451,7 @@ footer > hr {
|
||||||
width: 0.6rem;
|
width: 0.6rem;
|
||||||
height: 0.6rem;
|
height: 0.6rem;
|
||||||
filter: blur(6px);
|
filter: blur(6px);
|
||||||
box-shadow: 23.4vw 84.5vh 0 -0.38rem#fff,47.1vw 27.7vh 0 -0.23rem#fff,57.9vw 71.8vh 0 -0.09rem#fff,99vw 88.7vh 0 -0.37rem#fff,69vw 41.3vh 0 -0.14rem#fff,44.7vw 79.1vh 0 -0.4rem#fff,53.2vw 22.3vh 0 -0.5rem#fff,37.8vw 79.6vh 0 -0.08rem#fff,46.1vw 40.6vh 0 -0.2rem#fff,9.8vw 50.6vh 0 -0.05rem#fff,45.6vw 13.3vh 0 -0.02rem#fff,23.3vw 18.3vh 0 -0.32rem#fff,38.4vw 20.4vh 0 -0.22rem#fff,37.5vw 34.1vh 0 -0.21rem#fff,31vw 96.9vh 0 -0.1rem#fff,6.8vw 99vh 0 -0.49rem#fff,19.7vw 13.4vh 0 -0.28rem#fff,24vw 16.4vh 0 -0.09rem#fff,98.6vw 17.6vh 0 -0.08rem#fff,5.2vw 26.8vh 0 -0.35rem#fff,60.5vw 57.7vh 0 -0.34rem#fff,63.4vw 34.1vh 0 -0.46rem#fff,62.2vw 9.9vh 0 -0.13rem#fff,31.8vw 40vh 0 -0.19rem#fff,28vw 68.9vh 0 -0.33rem#fff,74.5vw 21.3vh 0 -0.32rem#fff,30.8vw 29.5vh 0 -0.25rem#fff,80.3vw 28vh 0 -0.12rem#fff,88.7vw 47.8vh 0 -0.33rem#fff,7.9vw 70.8vh 0 -0.46rem#fff,26.6vw 49.2vh 0 -0.04rem#fff,98.4vw 42.8vh 0 -0.09rem#fff,62.5vw 64.5vh 0 -0.48rem#fff,60.7vw 92.5vh 0 -0.13rem#fff,2.8vw 99.2vh 0 -0.49rem#fff,81.4vw 21.3vh 0 -0.4rem#fff,83.4vw 47.1vh 0 -0.46rem#fff,79.2vw 2.6vh 0 -0.17rem#fff,17.7vw 3.1vh 0 -0.12rem#fff,66.4vw 98.4vh 0 -0.34rem#fff,59.5vw 51.1vh 0 -0.2rem#fff,5.8vw 28.2vh 0 -0.41rem#fff,9.7vw 54vh 0 -0.48rem#fff,24.1vw 98.3vh 0 -0.29rem#fff,9.7vw 73.1vh 0 -0.44rem#fff,10vw 53.7vh 0 -0.5rem#fff,37.7vw 16.1vh 0 -0.31rem#fff,43.9vw 51.8vh 0 -0.29rem#fff,70.8vw 54vh 0 -0.19rem#fff,61.5vw 91vh 0 -0.41rem#fff,87.1vw 13.1vh 0 -0.22rem#fff,89.6vw 34.1vh 0 -0.25rem#fff,52.8vw 38.1vh 0 -0.33rem#fff,88.4vw 79.4vh 0 -0.22rem#fff,84.4vw 84.6vh 0 -0.1rem#fff,69.4vw 8.7vh 0 -0.46rem#fff,8.8vw 73.7vh 0 -0.19rem#fff,89.3vw 14.7vh 0 -0.1rem#fff,100vw 72.7vh 0 -0.2rem#fff,16.9vw 93.8vh 0 -0.22rem#fff,90.7vw 36.1vh 0 -0.43rem#fff,46.6vw 49.6vh 0 -0.21rem#fff,57.3vw 72.3vh 0 -0.03rem#fff,49.7vw 2vh 0 -0.15rem#fff,67vw 96.2vh 0 -0.47rem#fff,86vw 71.7vh 0 -0.11rem#fff,66vw 65.8vh 0 -0.32rem#fff,85.2vw 7.6vh 0 -0.12rem#fff,95.9vw 49.4vh 0 -0.15rem#fff,33.2vw 66.6vh 0 -0.14rem#fff,75.4vw 26.3vh 0 -0.02rem#fff,52.7vw 56vh 0 -0.11rem#fff,37.7vw 77vh 0 -0.18rem#fff,26.3vw 59.9vh 0 -0.18rem#fff,88.2vw 74.6vh 0 -0.39rem#fff,89.5vw 61.2vh 0 -0.48rem#fff,62vw 79.5vh 0 -0.4rem#fff,98.7vw 3.9vh 0 -0.01rem#fff,63.3vw 32vh 0 -0.43rem#fff,49.6vw 28.5vh 0 -0.09rem#fff,70vw 87vh 0 -0.05rem#fff,38.3vw 67.3vh 0 -0.49rem#fff,41.8vw 82.5vh 0 -0.36rem#fff,85.8vw 74vh 0 -0.07rem#fff,82.3vw 48.8vh 0 -0.11rem#fff,35.2vw 79.8vh 0 -0.4rem#fff,73.3vw 72.4vh 0 -0.36rem#fff,62.4vw 30.5vh 0 -0.14rem#fff,48.5vw 51.9vh 0 -0.03rem#fff,74.6vw 51.1vh 0 -0.35rem#fff,62.6vw 12.8vh 0 -0.33rem#fff,10vw 72.1vh 0 -0.2rem#fff,59vw 50.5vh 0 -0.04rem#fff,79.1vw 60.1vh 0 -0.34rem#fff,77.5vw 45.1vh 0 -0.23rem#fff,53vw 77.3vh 0 -0.4rem#fff,46.8vw 52.1vh 0 -0.44rem#fff,60.7vw 81vh 0 -0.17rem#fff,86.2vw 53.9vh 0 -0.01rem#fff,85.8vw 79.2vh 0 -0.39rem#fff;
|
box-shadow: 64.8vw 75.7vh 0 -0.14rem#fff,58.4vw 85.5vh 0 -0.16rem#fff,98.2vw 40.9vh 0 -0.03rem#fff,50.5vw 40.3vh 0 -0.42rem#fff,28.8vw 85.1vh 0 -0.3rem#fff,86.8vw 47.7vh 0 -0.47rem#fff,87vw 17.9vh 0 -0.06rem#fff,31.7vw 16.2vh 0 -0.16rem#fff,90.8vw 9.8vh 0 -0.49rem#fff,98.4vw 66.5vh 0 -0.43rem#fff,33.1vw 92vh 0 -0.46rem#fff,44.7vw 23vh 0 -0.21rem#fff,82.8vw 10.7vh 0 -0.1rem#fff,14.2vw 75.9vh 0 -0.33rem#fff,7.7vw 13vh 0 -0.29rem#fff,39.8vw 98.5vh 0 -0.27rem#fff,24.1vw 80.8vh 0 -0.13rem#fff,30.6vw 21.2vh 0 -0.36rem#fff,69.1vw 92.1vh 0 -0.17rem#fff,20.6vw 58.8vh 0 -0.28rem#fff,1vw 86.4vh 0 -0.27rem#fff,32.7vw 56.3vh 0 -0.43rem#fff,83.3vw 12.9vh 0 -0.07rem#fff,19.4vw 82.1vh 0 -0.46rem#fff,59.9vw 27.9vh 0 -0.22rem#fff,45.9vw 40vh 0 -0.11rem#fff,51.5vw 21.6vh 0 -0.03rem#fff,70.8vw 15.1vh 0 -0.1rem#fff,50.7vw 44.1vh 0 -0.18rem#fff,31.3vw 0.6vh 0 -0.19rem#fff,65.4vw 13.1vh 0 -0.19rem#fff,10.6vw 64.5vh 0 -0.15rem#fff,95.5vw 75.9vh 0 -0.12rem#fff,66.4vw 94vh 0 -0.19rem#fff,33.2vw 46.2vh 0 -0.12rem#fff,33.9vw 1.5vh 0 -0.13rem#fff,43.1vw 89vh 0 -0.1rem#fff,30.4vw 39.9vh 0 -0.45rem#fff,9.3vw 93.2vh 0 -0.35rem#fff,62.6vw 99.4vh 0 -0.09rem#fff,6.5vw 68.9vh 0 -0.14rem#fff,53.8vw 8.3vh 0 -0.46rem#fff,5.4vw 96.1vh 0 -0.18rem#fff,2.8vw 60.2vh 0 -0.05rem#fff,79.9vw 46.9vh 0 -0.48rem#fff,20.9vw 40.4vh 0 -0.24rem#fff,82.2vw 44.5vh 0 -0.37rem#fff,2.9vw 94.1vh 0 -0.31rem#fff,87.7vw 28.5vh 0 -0.44rem#fff,7.7vw 46.6vh 0 -0.25rem#fff,78.5vw 59.4vh 0 -0.48rem#fff,4.5vw 72.6vh 0 -0.21rem#fff,83.8vw 73.9vh 0 -0.25rem#fff,99.8vw 8.1vh 0 -0.42rem#fff,86.6vw 31.8vh 0 -0.21rem#fff,38.8vw 77.2vh 0 -0.13rem#fff,4.6vw 16.4vh 0 -0.17rem#fff,61.3vw 85.8vh 0 -0.06rem#fff,20.3vw 97vh 0 -0.21rem#fff,26.7vw 94.3vh 0 -0.06rem#fff,10.3vw 32.7vh 0 -0.37rem#fff,66.1vw 7.3vh 0 -0.24rem#fff,23.7vw 17.2vh 0 -0.48rem#fff,79.9vw 55.4vh 0 -0.03rem#fff,76.9vw 92.2vh 0 -0.43rem#fff,14.7vw 7.2vh 0 -0.35rem#fff,27.5vw 67.1vh 0 -0.23rem#fff,50vw 7.8vh 0 -0.4rem#fff,25.3vw 79.1vh 0 -0.34rem#fff,3vw 26.3vh 0 -0.26rem#fff,81.4vw 75vh 0 -0.38rem#fff,76.9vw 11.4vh 0 -0.18rem#fff,63.1vw 29.7vh 0 -0.16rem#fff,91.5vw 28.9vh 0 -0.47rem#fff,4.4vw 33.7vh 0 -0.35rem#fff,63.5vw 73.1vh 0 -0.1rem#fff,6.6vw 83.1vh 0 -0.45rem#fff,16.4vw 89.5vh 0 -0.01rem#fff,20.2vw 70.9vh 0 -0.18rem#fff,87.6vw 62vh 0 -0.2rem#fff,94vw 56.4vh 0 -0.03rem#fff,56.8vw 71vh 0 -0.26rem#fff,34.2vw 78.5vh 0 -0.4rem#fff,81.6vw 3.9vh 0 -0.21rem#fff,84.6vw 0.4vh 0 -0.5rem#fff,25vw 38.4vh 0 -0.23rem#fff,49.5vw 22.8vh 0 -0.08rem#fff,90.4vw 71.3vh 0 -0.21rem#fff,24.6vw 52.3vh 0 -0.16rem#fff,35.7vw 50.5vh 0 -0.3rem#fff,68.4vw 67.7vh 0 -0.34rem#fff,67.1vw 33.6vh 0 -0.47rem#fff,90.8vw 98.7vh 0 -0.24rem#fff,15.6vw 86.9vh 0 -0.34rem#fff,48.6vw 65vh 0 -0.37rem#fff,38.8vw 2.6vh 0 -0.36rem#fff,55.5vw 16.2vh 0 -0.07rem#fff,58.4vw 29.6vh 0 -0.29rem#fff,35.8vw 1.1vh 0 -0.42rem#fff,61.1vw 69.5vh 0 -0.38rem#fff;
|
||||||
animation-duration: 30s; }
|
animation-duration: 30s; }
|
||||||
|
|
||||||
.layer3.a {
|
.layer3.a {
|
||||||
|
@ -531,4 +530,4 @@ footer > hr {
|
||||||
100% {
|
100% {
|
||||||
opacity: 0.8; } }
|
opacity: 0.8; } }
|
||||||
|
|
||||||
/*# sourceMappingURL=kerstmis.css.map */
|
/*# sourceMappingURL=christmas_heavy.css.map */
|
7
app/static/css/themes/christmas_heavy.css.map
Normal file
|
@ -9,23 +9,23 @@ Enige discretie is aangeraden.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*high performance kerstmis*/
|
|
||||||
:root {
|
:root {
|
||||||
/*Darkmode colors*/
|
--gray0: #F28705;
|
||||||
--dGray0:#F28705;
|
--gray1: white;
|
||||||
--dGray1:white;
|
--gray2: #590212;
|
||||||
--dGray2:#590212;
|
--gray3: #590212;
|
||||||
--dGray3:#590212;
|
--gray4: #274001;
|
||||||
--dGray4:#274001;
|
--gray5: #274001;
|
||||||
--dGray5:#274001;
|
--gray6: #F2778D;
|
||||||
--dGray6:#F2778D;
|
--accent: #F2778D;
|
||||||
--dBlue:#F2778D;
|
|
||||||
|
--bg: #2F0000;
|
||||||
|
--navbarBg: #F2778D;
|
||||||
|
|
||||||
|
--fontFamily: Radikal, Optima, Segoe, Segoe UI, Candara, Calibri, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
body{
|
body{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: Radikal,Optima,Segoe,Segoe UI,Candara,Calibri,Arial,sans-serif;
|
|
||||||
background-color: #2F0000;
|
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Radikal;
|
font-family: Radikal;
|
|
@ -9,22 +9,21 @@ Enige discretie is aangeraden.
|
||||||
---=§[ Arnhoudt ]§=---
|
---=§[ Arnhoudt ]§=---
|
||||||
|
|
||||||
*/
|
*/
|
||||||
/*low performance kerstmis*/
|
|
||||||
:root {
|
:root {
|
||||||
/*Darkmode colors*/
|
--gray0: #F28705;
|
||||||
--dGray0:#F28705;
|
--gray1: white;
|
||||||
--dGray1:white;
|
--gray2: #590212;
|
||||||
--dGray2:#590212;
|
--gray3: #590212;
|
||||||
--dGray3:#590212;
|
--gray4: #274001;
|
||||||
--dGray4:#274001;
|
--gray5: #274001;
|
||||||
--dGray5:#274001;
|
--gray6: #F2778D;
|
||||||
--dGray6:#F2778D;
|
--accent: #F2778D;
|
||||||
--dBlue:#F2778D; }
|
--bg: #2F0000;
|
||||||
|
--navbarBg: #F2778D;
|
||||||
|
--fontFamily: Radikal, Optima, Segoe, Segoe UI, Candara, Calibri, Arial, sans-serif; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%; }
|
||||||
font-family: Radikal,Optima,Segoe,Segoe UI,Candara,Calibri,Arial,sans-serif;
|
|
||||||
background-color: #2F0000; }
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Radikal;
|
font-family: Radikal;
|
||||||
|
@ -439,4 +438,4 @@ footer > hr {
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(-3deg); } }
|
transform: rotate(-3deg); } }
|
||||||
|
|
||||||
/*# sourceMappingURL=kerstmis.css.map */
|
/*# sourceMappingURL=christmas_lightweight.css.map */
|
7
app/static/css/themes/christmas_lightweight.css.map
Normal file
|
@ -9,23 +9,23 @@ Enige discretie is aangeraden.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*low performance kerstmis*/
|
|
||||||
:root {
|
:root {
|
||||||
/*Darkmode colors*/
|
--gray0: #F28705;
|
||||||
--dGray0:#F28705;
|
--gray1: white;
|
||||||
--dGray1:white;
|
--gray2: #590212;
|
||||||
--dGray2:#590212;
|
--gray3: #590212;
|
||||||
--dGray3:#590212;
|
--gray4: #274001;
|
||||||
--dGray4:#274001;
|
--gray5: #274001;
|
||||||
--dGray5:#274001;
|
--gray6: #F2778D;
|
||||||
--dGray6:#F2778D;
|
--accent: #F2778D;
|
||||||
--dBlue:#F2778D;
|
|
||||||
|
--bg: #2F0000;
|
||||||
|
--navbarBg: #F2778D;
|
||||||
|
|
||||||
|
--fontFamily: Radikal, Optima, Segoe, Segoe UI, Candara, Calibri, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
body{
|
body{
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: Radikal,Optima,Segoe,Segoe UI,Candara,Calibri,Arial,sans-serif;
|
|
||||||
background-color: #2F0000;
|
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Radikal;
|
font-family: Radikal;
|
4
app/static/css/themes/dataPrivacy.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.background {
|
||||||
|
background-image: url("https://kelder.zeus.ugent.be/webcam/video/mjpg.cgi?profileid=2");
|
||||||
|
background-size: contain;
|
||||||
|
}
|
17
app/static/css/themes/halloween.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
:root {
|
||||||
|
--gray0: #ffeb65;
|
||||||
|
--gray1: #f28705;
|
||||||
|
--gray2: #f25c05;
|
||||||
|
--gray3: #f27405;
|
||||||
|
--gray4: #8c3d0f;
|
||||||
|
--gray5: #260101;
|
||||||
|
--gray6: #260101;
|
||||||
|
--accent: #d91604;
|
||||||
|
|
||||||
|
--bg: #260101;
|
||||||
|
--navbarBg: #260101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover{
|
||||||
|
background-image: url("static/images/themes/halloween/Halloween.jpeg");
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
/*Darkmode*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#D0D0D8;
|
|
||||||
--dGray1:#8E8E93;
|
|
||||||
--dGray2:#636366;
|
|
||||||
--dGray3:#48484A;
|
|
||||||
--dGray4:#3A3A3C;
|
|
||||||
--dGray5:#2C2C2E;
|
|
||||||
--dGray6:#1C1C1E;
|
|
||||||
--dBlue:#0A84FF;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
.background {
|
|
||||||
background-image: url("https://kelder.zeus.ugent.be/webcam/video/mjpg.cgi?profileid=2");
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*halloween*/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#FFEB65;
|
|
||||||
--dGray1:#F28705;
|
|
||||||
--dGray2:#F25C05;
|
|
||||||
--dGray3:#F27405;
|
|
||||||
--dGray4:#8C3D0F;
|
|
||||||
--dGray5:#260101;
|
|
||||||
--dGray6:#260101;
|
|
||||||
--dBlue:#D91604;
|
|
||||||
}
|
|
||||||
.table-hover tbody tr:hover{
|
|
||||||
background-image: url("static/images/themes/halloween/Halloween.jpeg");
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*lightmode*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#444444;
|
|
||||||
--dGray1:#666666;
|
|
||||||
--dGray2:#212121;
|
|
||||||
--dGray3:#ffffff;
|
|
||||||
--dGray4:#f9f9f9;
|
|
||||||
--dGray5:#ffffff;
|
|
||||||
--dGray6:#ffffff;
|
|
||||||
--dBlue:#0A84FF;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*sinterklaas*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#F2EB80;
|
|
||||||
--dGray1:#F2EF05;
|
|
||||||
--dGray2:#F2EF05;
|
|
||||||
--dGray3:#177EBF;
|
|
||||||
--dGray4:#0C6AA6;
|
|
||||||
--dGray5:#F20505;
|
|
||||||
--dGray6:#F50B00;
|
|
||||||
--dBlue:#35F546;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background{
|
|
||||||
background-image: url("static/images/themes/sinterklaas/Sinterklaas.jpg");
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*Darkmode*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#D0D0D8;
|
|
||||||
--dGray1:#8E8E93;
|
|
||||||
--dGray2:#636366;
|
|
||||||
--dGray3:#48484A;
|
|
||||||
--dGray4:#3A3A3C;
|
|
||||||
--dGray5:#2C2C2E;
|
|
||||||
--dGray6:#1C1C1E;
|
|
||||||
--dBlue:#0A84FF;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
.background {
|
|
||||||
background-image: url("https://kelder.zeus.ugent.be/webcam/video/mjpg.cgi?profileid=2");
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*halloween*/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#FFEB65;
|
|
||||||
--dGray1:#F28705;
|
|
||||||
--dGray2:#F25C05;
|
|
||||||
--dGray3:#F27405;
|
|
||||||
--dGray4:#8C3D0F;
|
|
||||||
--dGray5:#260101;
|
|
||||||
--dGray6:#260101;
|
|
||||||
--dBlue:#D91604;
|
|
||||||
}
|
|
||||||
.table-hover tbody tr:hover{
|
|
||||||
background-image: url("static/images/themes/halloween/Halloween.jpeg");
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*lightmode*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#444444;
|
|
||||||
--dGray1:#666666;
|
|
||||||
--dGray2:#212121;
|
|
||||||
--dGray3:#ffffff;
|
|
||||||
--dGray4:#f9f9f9;
|
|
||||||
--dGray5:#ffffff;
|
|
||||||
--dGray6:#ffffff;
|
|
||||||
--dBlue:#0A84FF;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*sinterklaas*/
|
|
||||||
:root {
|
|
||||||
/*Darkmode colors*/
|
|
||||||
--dGray0:#F2EB80;
|
|
||||||
--dGray1:#F2EF05;
|
|
||||||
--dGray2:#F2EF05;
|
|
||||||
--dGray3:#177EBF;
|
|
||||||
--dGray4:#0C6AA6;
|
|
||||||
--dGray5:#F20505;
|
|
||||||
--dGray6:#F50B00;
|
|
||||||
--dBlue:#35F546;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background{
|
|
||||||
background-image: url("static/images/themes/sinterklaas/Sinterklaas.jpg");
|
|
||||||
}
|
|
13
app/static/css/themes/plain_darkmode.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
:root {
|
||||||
|
--gray0: #d0d0d8;
|
||||||
|
--gray1: #8e8e93;
|
||||||
|
--gray2: #636366;
|
||||||
|
--gray3: #48484a;
|
||||||
|
--gray4: #3a3a3c;
|
||||||
|
--gray5: #2c2c2e;
|
||||||
|
--gray6: #1c1c1e;
|
||||||
|
--accent: #0a84ff;
|
||||||
|
|
||||||
|
--bg: #222224;
|
||||||
|
--navbarBg: #1c1c1e;
|
||||||
|
}
|
13
app/static/css/themes/plain_lightmode.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
:root {
|
||||||
|
--gray0: #212121;
|
||||||
|
--gray1: #444444;
|
||||||
|
--gray2: #666666;
|
||||||
|
--gray3: #cccccc;
|
||||||
|
--gray4: #f1f1f1;
|
||||||
|
--gray5: #f8f8f8;
|
||||||
|
--gray6: #ffffff;
|
||||||
|
--accent: #0a84ff;
|
||||||
|
|
||||||
|
--bg: #ffffff;
|
||||||
|
--navbarBg: #f8f8f8;
|
||||||
|
}
|
14
app/static/css/themes/sinterklaas.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
:root {
|
||||||
|
--gray0: #f2eb80;
|
||||||
|
--gray1: #f2ef05;
|
||||||
|
--gray2: #f2ef05;
|
||||||
|
--gray3: #177ebf;
|
||||||
|
--gray4: #0c6aa6;
|
||||||
|
--gray5: #f20505;
|
||||||
|
--gray6: #f50b00;
|
||||||
|
--accent: #35f546;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background{
|
||||||
|
background-image: url("static/images/themes/sinterklaas/Sinterklaas.jpg");
|
||||||
|
}
|
BIN
app/static/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/static/icons/android-chrome-256x256.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
app/static/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/static/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
app/static/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
app/static/icons/favicon.ico
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/static/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
32
app/static/icons/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="282.000000pt" height="282.000000pt" viewBox="0 0 282.000000 282.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,282.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M0 1410 l0 -1410 1410 0 1410 0 0 1410 0 1410 -312 0 -313 -1 -254
|
||||||
|
-409 c-140 -226 -257 -410 -261 -410 -4 0 -11 15 -17 33 -47 140 -103 288
|
||||||
|
-103 271 0 -11 -9 -47 -20 -79 -11 -31 -17 -61 -14 -66 3 -5 1 -9 -4 -9 -19 0
|
||||||
|
-20 -43 -6 -138 8 -54 18 -104 21 -112 3 -8 5 -19 4 -24 -3 -11 59 -397 66
|
||||||
|
-416 3 -8 5 -19 4 -24 -3 -12 18 -146 26 -166 4 -8 5 -19 4 -24 -3 -11 59
|
||||||
|
-397 66 -416 3 -8 5 -19 4 -24 -4 -16 22 -161 30 -170 4 -5 -14 -27 -41 -50
|
||||||
|
-27 -22 -103 -90 -169 -149 l-120 -108 -99 90 c-55 50 -102 87 -106 83 -4 -4
|
||||||
|
-6 -2 -5 3 2 6 -22 35 -54 65 l-57 55 15 94 c9 51 15 98 14 105 -1 6 1 18 4
|
||||||
|
26 7 19 49 284 46 296 -1 5 1 16 4 24 7 19 49 284 46 296 -1 5 1 16 4 24 7 19
|
||||||
|
49 284 46 296 -1 5 1 16 4 24 3 8 16 78 28 155 l21 139 -28 94 c-15 51 -31 89
|
||||||
|
-35 85 -4 -4 -30 -69 -57 -143 -26 -74 -52 -135 -56 -135 -5 0 -122 182 -260
|
||||||
|
405 l-253 405 -316 3 -317 2 0 -1410z"/>
|
||||||
|
<path d="M978 2675 c183 -80 338 -145 343 -145 5 0 9 -8 10 -17 0 -11 3 -13 6
|
||||||
|
-5 3 8 29 12 78 12 49 0 75 -4 78 -12 4 -9 6 -8 6 2 1 10 117 65 336 159 184
|
||||||
|
80 335 146 335 148 0 2 -343 3 -762 3 l-763 0 333 -145z"/>
|
||||||
|
<path d="M1260 1084 l29 -5 4 -92 3 -92 0 95 -1 95 -32 3 c-32 2 -32 2 -3 -4z"/>
|
||||||
|
<path d="M1550 1084 l30 -5 3 -227 3 -227 -1 230 0 230 -32 3 c-32 2 -32 2 -3
|
||||||
|
-4z"/>
|
||||||
|
<path d="M1294 740 c0 -58 1 -81 3 -52 2 28 2 76 0 105 -2 28 -3 5 -3 -53z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |