Merge branch 'staging' into 'master'
Automated Deployment & Release Management See merge request s3lph/matemat!55
This commit is contained in:
commit
7cb1bbe24a
36 changed files with 760 additions and 238 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,4 +10,4 @@
|
|||
*.sqlite3
|
||||
*.db
|
||||
static/upload/
|
||||
**/matemat.conf
|
||||
./matemat.conf
|
||||
|
|
122
.gitlab-ci.yml
122
.gitlab-ci.yml
|
@ -1,39 +1,126 @@
|
|||
---
|
||||
image: s3lph/matemat-ci:20180802-02
|
||||
image: s3lph/matemat-ci:20181107-02
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- staging
|
||||
- deploy
|
||||
|
||||
|
||||
|
||||
before_script:
|
||||
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
||||
|
||||
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- pip3 install -r requirements.txt
|
||||
- sudo -u matemat python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
||||
- sudo -u matemat python3 -m coverage combine
|
||||
- sudo -u matemat python3 -m coverage report --rcfile=setup.cfg
|
||||
- pip3 install -e .
|
||||
- sudo -u matemat python3.6 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
|
||||
- sudo -u matemat python3.6 -m coverage combine
|
||||
- sudo -u matemat python3.6 -m coverage report --rcfile=setup.cfg
|
||||
|
||||
codestyle:
|
||||
stage: test
|
||||
script:
|
||||
- pip3 install -r requirements.txt
|
||||
- pip3 install -e .
|
||||
- sudo -u matemat pycodestyle matemat
|
||||
|
||||
build:
|
||||
|
||||
|
||||
build_docker:
|
||||
stage: build
|
||||
script:
|
||||
- docker build -t "registry.gitlab.com/s3lph/matemat:$(git rev-parse HEAD)" .
|
||||
- docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:latest-$([[ $CI_COMMIT_REF_NAME == "master" ]] && echo stable || echo staging)"
|
||||
- docker build -t "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" -f package/docker/Dockerfile .
|
||||
- docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_REF_NAME"
|
||||
- if [[ -n "$CI_COMMIT_TAG" ]]; then docker tag "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA" "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_TAG"; fi
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_TOKEN registry.gitlab.com
|
||||
- docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_SHA"
|
||||
- docker push registry.gitlab.com/s3lph/matemat:latest-$([[ $CI_COMMIT_REF_NAME == "master" ]] && echo stable || echo staging)
|
||||
- docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_REF_NAME"
|
||||
- if [[ -n "$CI_COMMIT_TAG" ]]; then docker push "registry.gitlab.com/s3lph/matemat:$CI_COMMIT_TAG"; fi
|
||||
only:
|
||||
- staging
|
||||
- master
|
||||
- tags
|
||||
|
||||
build_wheel:
|
||||
stage: build
|
||||
script:
|
||||
- python3.6 setup.py egg_info bdist_wheel
|
||||
- cd dist
|
||||
- sha256sum *.whl > SHA256SUMS
|
||||
artifacts:
|
||||
paths:
|
||||
- "dist/*.whl"
|
||||
- dist/SHA256SUMS
|
||||
only:
|
||||
- tags
|
||||
|
||||
build_debian:
|
||||
stage: build
|
||||
script:
|
||||
# The Python package name provided by the python3-magic Debian package is "python-magic" rather than "file-magic".
|
||||
- sed -re 's/file-magic/python-magic/' -i setup.py
|
||||
- echo -n > package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
- |
|
||||
for version in "$(cat CHANGELOG.md | grep '<!-- BEGIN CHANGES' | cut -d ' ' -f 4)"; do
|
||||
echo "matemat (${version}-1); urgency=medium\n" >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
cat CHANGELOG.md | grep -A 1000 "<"'!'"-- BEGIN CHANGES ${version} -->" | grep -B 1000 "<"'!'"-- END CHANGES ${version} -->" | tail -n +2 | head -n -1 | sed -re 's/^-/ */g' >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
echo "\n -- ${PACKAGE_AUTHOR} $(date -R)\n" >> package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
done
|
||||
- gzip -9n package/debian/matemat/usr/share/doc/matemat/changelog
|
||||
- cp -r static/ package/debian/matemat/usr/lib/matemat/static/
|
||||
- cp -r templates/ package/debian/matemat/usr/lib/matemat/templates/
|
||||
- python3.6 setup.py egg_info install --root=package/debian/matemat/ --prefix=/usr --optimize=1
|
||||
- cd package/debian
|
||||
- mkdir -p matemat/usr/lib/python3/dist-packages/
|
||||
- rsync -a matemat/usr/lib/python3.6/site-packages/ matemat/usr/lib/python3/dist-packages/
|
||||
- rm -rf matemat/usr/lib/python3.6/
|
||||
- find matemat/usr/lib/python3/dist-packages -name __pycache__ -exec rm -r {} \; 2>/dev/null || true
|
||||
- find matemat/usr/lib/python3/dist-packages -name '*.pyc' -exec rm {} \;
|
||||
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||
- rm -rf matemat/usr/bin
|
||||
- sed -re 's$#!/usr/local/bin/python3.6$#!/usr/bin/python3$' -i matemat/usr/lib/matemat/matemat
|
||||
- find matemat -type f -exec chmod 0644 {} \;
|
||||
- find matemat -type d -exec chmod 755 {} \;
|
||||
- chmod +x matemat/usr/lib/matemat/matemat matemat/DEBIAN/postinst matemat/DEBIAN/prerm matemat/DEBIAN/postrm
|
||||
- dpkg-deb --build matemat
|
||||
- mv matemat.deb "matemat_${MATEMAT_VERSION}-1_all.deb"
|
||||
- sudo -u nobody lintian "matemat_${MATEMAT_VERSION}-1_all.deb"
|
||||
- sha256sum *.deb > SHA256SUMS
|
||||
artifacts:
|
||||
paths:
|
||||
- "package/debian/*.deb"
|
||||
- package/debian/SHA256SUMS
|
||||
only:
|
||||
- tags
|
||||
|
||||
build_archlinux:
|
||||
stage: build
|
||||
image: archlinux/base:latest # Use an archlinux image instead of the customized debian image.
|
||||
script:
|
||||
- pacman -Sy --noconfirm python python-setuptools python-pip python-wheel python-jinja python-pillow python-magic base-devel
|
||||
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
||||
- cp -r static/ package/archlinux/matemat/usr/lib/matemat/static/
|
||||
- cp -r templates/ package/archlinux/matemat/usr/lib/matemat/templates/
|
||||
- python3 setup.py egg_info -d -b +master install --root=package/archlinux/matemat/ --prefix=/usr --optimize=1
|
||||
- cd package/archlinux
|
||||
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
||||
- rm -rf matemat/usr/bin
|
||||
- sed -re "s/__VERSION__/${MATEMAT_VERSION}/g" -i PKGBUILD
|
||||
- sudo -u nobody makepkg -c
|
||||
- sha256sum *.pkg.tar.xz > SHA256SUMS
|
||||
artifacts:
|
||||
paths:
|
||||
- "package/archlinux/*.pkg.tar.xz"
|
||||
- package/archlinux/SHA256SUMS
|
||||
only:
|
||||
- tags
|
||||
|
||||
|
||||
|
||||
staging:
|
||||
stage: staging
|
||||
stage: deploy
|
||||
script:
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add - <<<"$STAGING_SSH_PRIVATE_KEY"
|
||||
|
@ -43,3 +130,12 @@ staging:
|
|||
url: https://matemat.kernelpanic.lol/
|
||||
only:
|
||||
- staging
|
||||
|
||||
|
||||
|
||||
release:
|
||||
stage: deploy
|
||||
script:
|
||||
- python package/release.py
|
||||
only:
|
||||
- tags
|
||||
|
|
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Matemat Changelog
|
|
@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to
|
|||
|
||||
## Dependencies
|
||||
|
||||
- Python 3 (>=3.7)
|
||||
- Python 3 (>=3.6)
|
||||
- Python dependencies:
|
||||
- file-magic
|
||||
- jinja2
|
||||
|
|
2
doc
2
doc
|
@ -1 +1 @@
|
|||
Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364
|
||||
Subproject commit 0fcf4244f90d93cbc7be8e670aeaa30288d24ae3
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '2.0'
|
||||
__version__ = '0.1'
|
||||
|
|
|
@ -3,14 +3,15 @@ from typing import Any, Dict, Iterable, Union
|
|||
|
||||
import sys
|
||||
|
||||
from matemat.webserver import MatematWebserver
|
||||
from matemat.webserver import parse_config_file
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Those imports are actually needed, as they implicitly register pagelets.
|
||||
# noinspection PyUnresolvedReferences
|
||||
from matemat.webserver.pagelets import *
|
||||
from matemat.webserver import MatematWebserver
|
||||
|
||||
|
||||
def main():
|
||||
# Use config file name from command line, if present
|
||||
configfile: Union[str, Iterable[str]] = '/etc/matemat.conf'
|
||||
if len(sys.argv) > 1:
|
||||
|
@ -21,3 +22,7 @@ if __name__ == '__main__':
|
|||
|
||||
# Start the web server
|
||||
MatematWebserver(**config).start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
|
||||
import crypt
|
||||
|
@ -39,7 +38,7 @@ class MatematDatabase(object):
|
|||
"""
|
||||
self.db: DatabaseWrapper = DatabaseWrapper(filename)
|
||||
|
||||
def __enter__(self) -> MatematDatabase:
|
||||
def __enter__(self) -> 'MatematDatabase':
|
||||
# Pass context manager stuff through to the database wrapper
|
||||
self.db.__enter__()
|
||||
return self
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
"""
|
||||
Representation of a product offered by the Matemat, with a name, prices for users, and the number of items
|
||||
currently in stock.
|
||||
|
||||
:param id: The product ID in the database.
|
||||
:param _id: The product ID in the database.
|
||||
:param name: The product's name.
|
||||
:param price_member: The price of a unit of this product for users marked as "members".
|
||||
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
|
||||
:param stock: The number of items of this product currently in stock.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
price_member: int
|
||||
price_non_member: int
|
||||
stock: int
|
||||
def __init__(self, _id: int, name: str, price_member: int, price_non_member: int, stock: int) -> None:
|
||||
self.id: int = _id
|
||||
self.name: str = name
|
||||
self.price_member: int = price_member
|
||||
self.price_non_member: int = price_non_member
|
||||
self.stock: int = stock
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Product):
|
||||
return False
|
||||
return self.id == other.id and \
|
||||
self.name == other.name and \
|
||||
self.price_member == other.price_member and \
|
||||
self.price_non_member == other.price_non_member and \
|
||||
self.stock == other.stock
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.name, self.price_member, self.price_non_member, self.stock))
|
||||
|
|
|
@ -1,17 +1,42 @@
|
|||
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from matemat.db.primitives import User, Transaction
|
||||
|
||||
|
||||
@dataclass
|
||||
class Receipt:
|
||||
"""
|
||||
Representation of a receipt for a user and a given timespan.
|
||||
|
||||
id: int
|
||||
transactions: List[Transaction]
|
||||
user: User
|
||||
from_date: datetime
|
||||
to_date: datetime
|
||||
:param _id: The receipt ID in the database.
|
||||
:param transactions: The list of transactions on this receipt.
|
||||
:param user: The user for whom this receipt was issued.
|
||||
:param from_date: The beginning of the time span this receipt covers.
|
||||
:param to_date: The end of the time span this receipt covers.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
_id: int,
|
||||
transactions: List[Transaction],
|
||||
user: User,
|
||||
from_date: datetime,
|
||||
to_date: datetime) -> None:
|
||||
self.id: int = _id
|
||||
self.transactions: List[Transaction] = transactions
|
||||
self.user: User = user
|
||||
self.from_date: datetime = from_date
|
||||
self.to_date: datetime = to_date
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Receipt):
|
||||
return False
|
||||
return self.id == other.id and \
|
||||
self.transactions == other.transactions and \
|
||||
self.user == other.user and \
|
||||
self.from_date == other.from_date and \
|
||||
self.to_date == other.to_date
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.transactions, self.user, self.from_date, self.to_date))
|
||||
|
|
|
@ -11,7 +11,7 @@ class ReceiptPreference(Enum):
|
|||
A user's preference for the frequency of receiving receipts.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
def __new__(cls, *args, **kwargs) -> 'ReceiptPreference':
|
||||
e = object.__new__(cls)
|
||||
# The enum's internal value
|
||||
e._value_: int = args[0]
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
@ -8,14 +7,23 @@ from matemat.db.primitives import User
|
|||
from matemat.util.currency_format import format_chf
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Transaction:
|
||||
"""
|
||||
Representation of a generic transaction involving an user and an amount of money.
|
||||
|
||||
id: int
|
||||
user: User
|
||||
value: int
|
||||
old_balance: int
|
||||
date: datetime
|
||||
:param _id: The transaction ID in the database.
|
||||
:param user: The user affected by this transaction.
|
||||
:param value: The monetary value of this transaction.
|
||||
:param old_balance: The balance on the user's account before this transaction.
|
||||
:param date: The date of this transaction.
|
||||
"""
|
||||
|
||||
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime) -> None:
|
||||
self.id: int = _id
|
||||
self.user: User = user
|
||||
self.value: int = value
|
||||
self.old_balance: int = old_balance
|
||||
self.date: datetime = date
|
||||
|
||||
@property
|
||||
def receipt_date(self) -> str:
|
||||
|
@ -37,30 +45,101 @@ class Transaction:
|
|||
def receipt_message(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Transaction):
|
||||
return False
|
||||
return self.id == other.id and \
|
||||
self.user == other.user and \
|
||||
self.value == other.value and \
|
||||
self.old_balance == other.old_balance and \
|
||||
self.date == other.date
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.user, self.value, self.old_balance, self.date))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Consumption(Transaction):
|
||||
"""
|
||||
Representation of a consumption involving an user, a product and an amount of money.
|
||||
|
||||
product: str
|
||||
:param _id: The transaction ID in the database.
|
||||
:param user: The user affected by this transaction.
|
||||
:param value: The (negative) price of the product that was bought.
|
||||
:param old_balance: The balance on the user's account before this transaction.
|
||||
:param date: The date of this transaction.
|
||||
:param product: The name of the product that was bought.
|
||||
"""
|
||||
|
||||
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime, product: str) -> None:
|
||||
super().__init__(_id, user, value, old_balance, date)
|
||||
self.product: str = product
|
||||
|
||||
@property
|
||||
def receipt_description(self) -> str:
|
||||
return self.product
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Consumption):
|
||||
return False
|
||||
return super().__eq__(other) and \
|
||||
self.product == other.product
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((super().__hash__(), self.product))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Deposit(Transaction):
|
||||
"""
|
||||
Representation of a deposit involving an user and an amount of money.
|
||||
|
||||
:param _id: The transaction ID in the database.
|
||||
:param user: The user affected by this transaction.
|
||||
:param value: The amount of money that was deposited on the account.
|
||||
:param old_balance: The balance on the user's account before this transaction.
|
||||
:param date: The date of this transaction.
|
||||
"""
|
||||
|
||||
def __init__(self, _id: int, user: User, value: int, old_balance: int, date: datetime) -> None:
|
||||
super().__init__(_id, user, value, old_balance, date)
|
||||
|
||||
@property
|
||||
def receipt_description(self) -> str:
|
||||
return 'Deposit'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Deposit):
|
||||
return False
|
||||
return super().__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return super().__hash__()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Modification(Transaction):
|
||||
"""
|
||||
Representation of a administrative account balance modification. Involves the affected user, the agent that
|
||||
performed the modification and optionally a message for the reason of the modification.
|
||||
|
||||
agent: str
|
||||
reason: Optional[str]
|
||||
:param _id: The transaction ID in the database.
|
||||
:param user: The user affected by this transaction.
|
||||
:param value: The amount of money that was deposited on the account.
|
||||
:param old_balance: The balance on the user's account before this transaction.
|
||||
:param date: The date of this transaction.
|
||||
:param agent: The username of the agent performing the modification.
|
||||
:param reason: The reason for this modification, as provided by the agent.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
_id: int,
|
||||
user: User,
|
||||
value: int,
|
||||
old_balance: int,
|
||||
date: datetime,
|
||||
agent: str,
|
||||
reason: Optional[str]) -> None:
|
||||
super().__init__(_id, user, value, old_balance, date)
|
||||
self.agent: str = agent
|
||||
self.reason: Optional[str] = reason
|
||||
|
||||
@property
|
||||
def receipt_description(self) -> str:
|
||||
|
@ -72,3 +151,13 @@ class Modification(Transaction):
|
|||
return None
|
||||
else:
|
||||
return f'Reason: «{self.reason}»'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, Modification):
|
||||
return False
|
||||
return super().__eq__(other) and \
|
||||
self.agent == other.agent and \
|
||||
self.reason == other.reason
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((super().__hash__(), self.agent, self.reason))
|
||||
|
|
|
@ -1,30 +1,50 @@
|
|||
|
||||
from typing import Optional
|
||||
|
||||
from dataclasses import dataclass
|
||||
from matemat.db.primitives.ReceiptPreference import ReceiptPreference
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""
|
||||
Representation of a user registered with the Matemat, with a name, e-mail address (optional), whether the user is a
|
||||
member of the organization the Matemat instance is used in, whether the user is an administrator, and the user's
|
||||
account balance.
|
||||
|
||||
:param id: The user ID in the database.
|
||||
:param username: The user's name.
|
||||
:param _id: The user ID in the database.
|
||||
:param name: The user's name.
|
||||
:param balance: The balance of the user's account.
|
||||
:param email: The user's e-mail address (optional).
|
||||
:param admin: Whether the user is an administrator.
|
||||
:param member: Whether the user is a member.
|
||||
:param is_admin: Whether the user is an administrator.
|
||||
:param is_member: Whether the user is a member.
|
||||
:param receipt_pref: The user's preference on how often to receive transaction receipts.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
balance: int
|
||||
email: Optional[str] = None
|
||||
is_admin: bool = False
|
||||
is_member: bool = False
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference.NONE
|
||||
def __init__(self,
|
||||
_id: int,
|
||||
name: str,
|
||||
balance: int,
|
||||
email: Optional[str] = None,
|
||||
is_admin: bool = False,
|
||||
is_member: bool = False,
|
||||
receipt_pref: ReceiptPreference = ReceiptPreference.NONE) -> None:
|
||||
self.id: int = _id
|
||||
self.name: str = name
|
||||
self.balance: int = balance
|
||||
self.email: Optional[str] = email
|
||||
self.is_admin: bool = is_admin
|
||||
self.is_member: bool = is_member
|
||||
self.receipt_pref: ReceiptPreference = receipt_pref
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, User):
|
||||
return False
|
||||
return self.id == other.id and \
|
||||
self.name == other.name and \
|
||||
self.balance == other.balance and \
|
||||
self.email == other.email and \
|
||||
self.is_admin == other.is_admin and \
|
||||
self.is_member == other.is_member and \
|
||||
self.receipt_pref == other.receipt_pref
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref))
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Optional
|
||||
|
||||
from matemat.exceptions import DatabaseConsistencyError
|
||||
|
@ -47,7 +46,7 @@ class DatabaseWrapper(object):
|
|||
self._filename: str = filename
|
||||
self._sqlite_db: Optional[sqlite3.Connection] = None
|
||||
|
||||
def __enter__(self) -> DatabaseWrapper:
|
||||
def __enter__(self) -> 'DatabaseWrapper':
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
|
|
|
@ -20,10 +20,13 @@ def add_months(d: datetime, months: int) -> datetime:
|
|||
days: int = 0
|
||||
# Iterate the months between the passed date and the target month
|
||||
for i in range(months):
|
||||
days += calendar.monthlen(*nextmonth)
|
||||
nextmonth = calendar.nextmonth(*nextmonth)
|
||||
days += calendar.monthrange(*nextmonth)[1]
|
||||
if nextmonth[1] == 12:
|
||||
nextmonth = nextmonth[0] + 1, 1
|
||||
else:
|
||||
nextmonth = nextmonth[0], nextmonth[1] + 1
|
||||
# Set the day of month temporarily to 1, then add the day offset to reach the 1st of the target month
|
||||
newdate: datetime = d.replace(day=1) + timedelta(days=days)
|
||||
# Re-set the day of month to the intended value, but capped by the max. day in the target month
|
||||
newdate = newdate.replace(day=min(d.day, calendar.monthlen(newdate.year, newdate.month)))
|
||||
newdate = newdate.replace(day=min(d.day, calendar.monthrange(newdate.year, newdate.month)[1]))
|
||||
return newdate
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Iterator, List, Tuple, Union
|
||||
|
||||
|
||||
|
@ -25,7 +24,7 @@ class RequestArguments(object):
|
|||
"""
|
||||
self.__container: Dict[str, RequestArgument] = dict()
|
||||
|
||||
def __getitem__(self, key: str) -> RequestArgument:
|
||||
def __getitem__(self, key: str) -> 'RequestArgument':
|
||||
"""
|
||||
Retrieve the argument for the given name, creating it on the fly, if it doesn't exist.
|
||||
|
||||
|
@ -41,7 +40,7 @@ class RequestArguments(object):
|
|||
# Return the argument for the name
|
||||
return self.__container[key]
|
||||
|
||||
def __getattr__(self, key: str) -> RequestArgument:
|
||||
def __getattr__(self, key: str) -> 'RequestArgument':
|
||||
"""
|
||||
Syntactic sugar for accessing values with a name that can be used in Python attributes. The value will be
|
||||
returned as an immutable view.
|
||||
|
@ -279,7 +278,7 @@ class RequestArgument(object):
|
|||
# Yield an immutable scalar view for each (ctype, value) element in the array
|
||||
yield _View(self.__name, v)
|
||||
|
||||
def __getitem__(self, index: Union[int, slice]) -> RequestArgument:
|
||||
def __getitem__(self, index: Union[int, slice]) -> 'RequestArgument':
|
||||
"""
|
||||
Index the argument with either an int or a slice. The returned values are represented as immutable
|
||||
RequestArgument views.
|
||||
|
|
29
package/archlinux/PKGBUILD
Normal file
29
package/archlinux/PKGBUILD
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
# Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||
|
||||
pkgname=matemat
|
||||
pkgver=0.1
|
||||
pkgrel=1
|
||||
arch=('any')
|
||||
|
||||
pkgdesc='A soda machine stock-keeping webservice'
|
||||
url='https://gitlab.com/s3lph/matemat'
|
||||
licence=('MIT')
|
||||
|
||||
depends=(
|
||||
'python'
|
||||
'python-jinja'
|
||||
'python-pillow'
|
||||
'python-magic'
|
||||
'file'
|
||||
)
|
||||
|
||||
backup=(
|
||||
'etc/matemat.conf'
|
||||
)
|
||||
|
||||
install=$pkgname.install
|
||||
|
||||
package() {
|
||||
cp -r ../matemat/* ../pkg/matemat/
|
||||
}
|
31
package/archlinux/matemat.install
Executable file
31
package/archlinux/matemat.install
Executable file
|
@ -0,0 +1,31 @@
|
|||
|
||||
post_install() {
|
||||
|
||||
if ! getent group matemat >/dev/null; then
|
||||
groupadd --system matemat
|
||||
fi
|
||||
|
||||
if ! getent passwd matemat >/dev/null; then
|
||||
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/bin/nologin matemat
|
||||
fi
|
||||
|
||||
chown matemat:matemat /var/lib/matemat
|
||||
chmod 0750 /var/lib/matemat
|
||||
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
}
|
||||
|
||||
pre_remove() {
|
||||
|
||||
systemctl stop matemat.service
|
||||
userdel matemat
|
||||
|
||||
}
|
||||
|
||||
post_remove() {
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
}
|
32
package/archlinux/matemat/etc/matemat.conf
Normal file
32
package/archlinux/matemat/etc/matemat.conf
Normal file
|
@ -0,0 +1,32 @@
|
|||
[Matemat]
|
||||
|
||||
# The IP address to listen on
|
||||
Address=::
|
||||
# The TCP port to listen on
|
||||
Port=80
|
||||
|
||||
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
LogLevel=DEBUG
|
||||
|
||||
[Pagelets]
|
||||
|
||||
# Name of the Matemat instance, shown in the web app
|
||||
InstanceName=Matemat
|
||||
|
||||
#
|
||||
# Configure SMTP credentials
|
||||
#
|
||||
# SmtpFrom=matemat@example.com
|
||||
# SmtpSubj=Matemat Receipt
|
||||
# SmtpHost=exmaple.com
|
||||
# SmtpPort=587
|
||||
# SmtpUser=matemat@example.com
|
||||
# SmtpPass=supersecurepassword
|
||||
|
||||
#
|
||||
# Enable to allow users to receive receipts via email
|
||||
#
|
||||
# SmtpSendReceipts=1
|
||||
|
||||
# Add static HTTP headers in this section
|
||||
# [HttpHeaders]
|
11
package/archlinux/matemat/usr/lib/matemat/matemat.conf
Normal file
11
package/archlinux/matemat/usr/lib/matemat/matemat.conf
Normal file
|
@ -0,0 +1,11 @@
|
|||
[Matemat]
|
||||
|
||||
StaticPath=/usr/lib/matemat/static
|
||||
TemplatePath=/usr/lib/matemat/templates
|
||||
|
||||
LogTarget=stdout
|
||||
|
||||
[Pagelets]
|
||||
|
||||
UploadDir=/var/lib/matemat/upload
|
||||
DatabaseFile=/var/lib/matemat/matemat.db
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=matemat
|
||||
After=networking.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
|
||||
User=matemat
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
1
package/debian/matemat/DEBIAN/conffiles
Normal file
1
package/debian/matemat/DEBIAN/conffiles
Normal file
|
@ -0,0 +1 @@
|
|||
/etc/matemat.conf
|
11
package/debian/matemat/DEBIAN/control
Normal file
11
package/debian/matemat/DEBIAN/control
Normal file
|
@ -0,0 +1,11 @@
|
|||
Package: matemat
|
||||
Version: 0.1
|
||||
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||
Section: web
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Depends: python3 (>= 3.6), python3-jinja2, python3-magic, python3-pil
|
||||
Description: Soda machine stock-keeping webservice
|
||||
A web service for automated stock-keeping of a soda machine written in Python.
|
||||
It provides a touch-input-friendly user interface (as most input happens
|
||||
through the soda machine's touch screen).
|
21
package/debian/matemat/DEBIAN/postinst
Executable file
21
package/debian/matemat/DEBIAN/postinst
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$1" == "configure" ]]; then
|
||||
|
||||
if ! getent group matemat >/dev/null; then
|
||||
groupadd --system matemat
|
||||
fi
|
||||
|
||||
if ! getent passwd matemat >/dev/null; then
|
||||
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/sbin/nologin matemat
|
||||
fi
|
||||
|
||||
chown matemat:matemat /var/lib/matemat
|
||||
chmod 0750 /var/lib/matemat
|
||||
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
|
||||
|
||||
systemctl daemon-reload || true
|
||||
|
||||
fi
|
9
package/debian/matemat/DEBIAN/postrm
Executable file
9
package/debian/matemat/DEBIAN/postrm
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$1" == "remove" ]]; then
|
||||
|
||||
systemctl daemon-reload || true
|
||||
|
||||
fi
|
9
package/debian/matemat/DEBIAN/prerm
Executable file
9
package/debian/matemat/DEBIAN/prerm
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$1" == "remove" ]]; then
|
||||
|
||||
userdel matemat
|
||||
|
||||
fi
|
32
package/debian/matemat/etc/matemat.conf
Normal file
32
package/debian/matemat/etc/matemat.conf
Normal file
|
@ -0,0 +1,32 @@
|
|||
[Matemat]
|
||||
|
||||
# The IP address to listen on
|
||||
Address=::
|
||||
# The TCP port to listen on
|
||||
Port=80
|
||||
|
||||
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
LogLevel=DEBUG
|
||||
|
||||
[Pagelets]
|
||||
|
||||
# Name of the Matemat instance, shown in the web app
|
||||
InstanceName=Matemat
|
||||
|
||||
#
|
||||
# Configure SMTP credentials
|
||||
#
|
||||
# SmtpFrom=matemat@example.com
|
||||
# SmtpSubj=Matemat Receipt
|
||||
# SmtpHost=exmaple.com
|
||||
# SmtpPort=587
|
||||
# SmtpUser=matemat@example.com
|
||||
# SmtpPass=supersecurepassword
|
||||
|
||||
#
|
||||
# Enable to allow users to receive receipts via email
|
||||
#
|
||||
# SmtpSendReceipts=1
|
||||
|
||||
# Add static HTTP headers in this section
|
||||
# [HttpHeaders]
|
12
package/debian/matemat/lib/systemd/system/matemat.service
Normal file
12
package/debian/matemat/lib/systemd/system/matemat.service
Normal file
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=matemat
|
||||
After=networking.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
|
||||
User=matemat
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
11
package/debian/matemat/usr/lib/matemat/matemat.conf
Normal file
11
package/debian/matemat/usr/lib/matemat/matemat.conf
Normal file
|
@ -0,0 +1,11 @@
|
|||
[Matemat]
|
||||
|
||||
StaticPath=/usr/lib/matemat/static
|
||||
TemplatePath=/usr/lib/matemat/templates
|
||||
|
||||
LogTarget=stdout
|
||||
|
||||
[Pagelets]
|
||||
|
||||
UploadDir=/var/lib/matemat/upload
|
||||
DatabaseFile=/var/lib/matemat/matemat.db
|
16
package/debian/matemat/usr/share/doc/matemat/copyright
Normal file
16
package/debian/matemat/usr/share/doc/matemat/copyright
Normal file
|
@ -0,0 +1,16 @@
|
|||
Copyright 2018 s3lph
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -4,9 +4,10 @@ FROM python:3.7-alpine
|
|||
ADD . /
|
||||
RUN mkdir -p /var/matemat/db /var/matemat/upload \
|
||||
&& apk --update add libmagic zlib jpeg zlib-dev jpeg-dev build-base \
|
||||
&& pip3 install -r /requirements.txt \
|
||||
&& pip3 install -e . \
|
||||
&& apk del zlib-dev jpeg-dev build-base \
|
||||
&& rm -rf /var/cache/apk /root/.cache/pip
|
||||
&& rm -rf /var/cache/apk /root/.cache/pip \
|
||||
&& rm -rf /package
|
||||
|
||||
EXPOSE 80/tcp
|
||||
CMD [ "/run.sh" ]
|
168
package/release.py
Executable file
168
package/release.py
Executable file
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
import http.client
|
||||
from urllib.error import HTTPError
|
||||
|
||||
|
||||
def parse_changelog(tag: str) -> Optional[str]:
|
||||
release_changelog: str = ''
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
in_target: bool = False
|
||||
done: bool = False
|
||||
for line in f.readlines():
|
||||
if in_target:
|
||||
if f'<!-- END RELEASE {tag} -->' in line:
|
||||
done = True
|
||||
break
|
||||
release_changelog += line
|
||||
elif f'<!-- BEGIN RELEASE {tag} -->' in line:
|
||||
in_target = True
|
||||
continue
|
||||
if not done:
|
||||
return None
|
||||
return release_changelog
|
||||
|
||||
|
||||
def fetch_job_ids(project_id: int, pipeline_id: int, api_token: str) -> Dict[str, str]:
|
||||
url: str = f'https://gitlab.com/api/v4/projects/{project_id}/pipelines/{pipeline_id}/jobs'
|
||||
headers: Dict[str, str] = {
|
||||
'Private-Token': api_token
|
||||
}
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
try:
|
||||
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
||||
except HTTPError as e:
|
||||
print(e.read().decode())
|
||||
sys.exit(1)
|
||||
resp_data: bytes = resp.read()
|
||||
joblist: List[Dict[str, Any]] = json.loads(resp_data.decode())
|
||||
|
||||
jobidmap: Dict[str, str] = {}
|
||||
for job in joblist:
|
||||
name: str = job['name']
|
||||
job_id: str = job['id']
|
||||
jobidmap[name] = job_id
|
||||
return jobidmap
|
||||
|
||||
|
||||
def fetch_single_shafile(url: str) -> str:
|
||||
req = urllib.request.Request(url)
|
||||
try:
|
||||
resp: http.client.HTTPResponse = urllib.request.urlopen(req)
|
||||
except HTTPError as e:
|
||||
print(e.read().decode())
|
||||
sys.exit(1)
|
||||
resp_data: bytes = resp.readline()
|
||||
shafile: str = resp_data.decode()
|
||||
filename: str = shafile.strip().split(' ')[-1].strip()
|
||||
return filename
|
||||
|
||||
|
||||
def fetch_wheel_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||
mybase: str = f'{base_url}/jobs/{job_ids["build_wheel"]}/artifacts/raw'
|
||||
wheel_sha_url: str = f'{mybase}/dist/SHA256SUMS'
|
||||
wheel_filename: str = fetch_single_shafile(wheel_sha_url)
|
||||
wheel_url: str = f'{mybase}/dist/{wheel_filename}'
|
||||
return wheel_url, wheel_sha_url
|
||||
|
||||
|
||||
def fetch_debian_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||
mybase: str = f'{base_url}/jobs/{job_ids["build_debian"]}/artifacts/raw'
|
||||
debian_sha_url: str = f'{mybase}/package/debian/SHA256SUMS'
|
||||
debian_filename: str = fetch_single_shafile(debian_sha_url)
|
||||
debian_url: str = f'{mybase}/package/debian/{debian_filename}'
|
||||
return debian_url, debian_sha_url
|
||||
|
||||
|
||||
def fetch_arch_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
||||
mybase: str = f'{base_url}/jobs/{job_ids["build_archlinux"]}/artifacts/raw'
|
||||
arch_sha_url: str = f'{mybase}/package/archlinux/SHA256SUMS'
|
||||
arch_filename: str = fetch_single_shafile(arch_sha_url)
|
||||
arch_url: str = f'{mybase}/package/archlinux/{arch_filename}'
|
||||
return arch_url, arch_sha_url
|
||||
|
||||
|
||||
def main():
|
||||
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
||||
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
||||
project_name: Optional[str] = os.getenv('CI_PROJECT_PATH')
|
||||
project_id: Optional[str] = os.getenv('CI_PROJECT_ID')
|
||||
pipeline_id: Optional[str] = os.getenv('CI_PIPELINE_ID')
|
||||
if api_token is None:
|
||||
print('GITLAB_API_TOKEN is not set.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if release_tag is None:
|
||||
print('CI_COMMIT_TAG is not set.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if project_name is None:
|
||||
print('CI_PROJECT_PATH is not set.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if project_id is None:
|
||||
print('CI_PROJECT_ID is not set.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if pipeline_id is None:
|
||||
print('CI_PIPELINE_ID is not set.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
changelog: Optional[str] = parse_changelog(release_tag)
|
||||
if changelog is None:
|
||||
print('Changelog could not be parsed.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
job_ids: Dict[str, str] = fetch_job_ids(project_id, pipeline_id, api_token)
|
||||
|
||||
base_url: str = f'https://gitlab.com/{project_name}/-'
|
||||
|
||||
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
||||
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
||||
arch_url, arch_sha_url = fetch_arch_url(base_url, job_ids)
|
||||
|
||||
augmented_changelog = f'''{changelog.strip()}
|
||||
|
||||
### Download
|
||||
|
||||
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
||||
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
|
||||
- [Arch Linux Package]({arch_url}) ([sha256]({arch_sha_url}))
|
||||
- Docker image: registry.gitlab.com/{project_name}:{release_tag}'''
|
||||
|
||||
post_body: str = json.dumps({'description': augmented_changelog})
|
||||
|
||||
gitlab_release_api_url: str = \
|
||||
f'https://gitlab.com/api/v4/projects/{project_id}/repository/tags/{release_tag}/release'
|
||||
headers: Dict[str, str] = {
|
||||
'Private-Token': api_token,
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
|
||||
request = urllib.request.Request(
|
||||
gitlab_release_api_url,
|
||||
post_body.encode('utf-8'),
|
||||
headers=headers,
|
||||
method='POST'
|
||||
)
|
||||
try:
|
||||
response: http.client.HTTPResponse = urllib.request.urlopen(request)
|
||||
except HTTPError as e:
|
||||
print(e.read().decode())
|
||||
sys.exit(1)
|
||||
response_bytes: bytes = response.read()
|
||||
response_str: str = response_bytes.decode()
|
||||
response_data: Dict[str, Any] = json.loads(response_str)
|
||||
|
||||
if response_data['tag_name'] != release_tag:
|
||||
print('Something went wrong...', file=sys.stderr)
|
||||
print(response_str, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(response_data['description'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,3 +1 @@
|
|||
file-magic
|
||||
jinja2
|
||||
Pillow
|
||||
.
|
||||
|
|
32
setup.py
32
setup.py
|
@ -1,12 +1,32 @@
|
|||
from setuptools import setup
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
from matemat import __version__
|
||||
|
||||
setup(
|
||||
name='matemat',
|
||||
version='2.0',
|
||||
packages=['matemat'],
|
||||
url='',
|
||||
license='',
|
||||
version=__version__,
|
||||
url='https://gitlab.com/s3lph/matemat',
|
||||
license='MIT',
|
||||
author='s3lph',
|
||||
author_email='',
|
||||
description='Replacement for the original Ruby matemat software.'
|
||||
description='Soda machine stock-keeping webservice',
|
||||
long_description='''
|
||||
A web service for automated stock-keeping of a soda machine written in Python.
|
||||
It provides a touch-input-friendly user interface (as most input happens through the
|
||||
soda machine's touch screen).
|
||||
''',
|
||||
|
||||
packages=find_packages(exclude=['*.test']),
|
||||
python_requires='>=3.6',
|
||||
install_requires=[
|
||||
'file-magic',
|
||||
'jinja2',
|
||||
'Pillow'
|
||||
],
|
||||
test_loader='unittest:TestLoader',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'matemat = matemat.__main__:main'
|
||||
]
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
# There is no buster image yet and stretch doesn't have a docker package. So let's just "upgrade" the image to buster.
|
||||
FROM python:3.7-stretch
|
||||
FROM python:3.6-stretch
|
||||
|
||||
RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
|
||||
&& useradd -d /home/matemat -m matemat \
|
||||
|
@ -8,8 +8,8 @@ RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \
|
|||
&& chown matemat:matemat -R /var/matemat/db \
|
||||
&& chown matemat:matemat -R /var/matemat/upload \
|
||||
&& apt-get update -qy \
|
||||
&& apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential \
|
||||
&& python3.7 -m pip install coverage wheel pycodestyle mypy \
|
||||
&& apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential lintian rsync \
|
||||
&& python3.6 -m pip install coverage wheel pycodestyle mypy \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /home/matemat
|
||||
|
|
147
touchkey.html
147
touchkey.html
|
@ -1,147 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
svg {
|
||||
width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Welcome, {{username}}</h1>
|
||||
|
||||
<svg id="svg" width="400" height="400">
|
||||
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
</svg>
|
||||
|
||||
<script>
|
||||
|
||||
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||
|
||||
mouseDown = false;
|
||||
currentStroke = null;
|
||||
strokeId = 0;
|
||||
doneMap = {};
|
||||
enteredKey = '';
|
||||
|
||||
svg = document.getElementById('svg');
|
||||
|
||||
drawLine = (fromX, fromY, toX, toY) => {
|
||||
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
var id = 'l-' + (strokeId++);
|
||||
var idAttr = document.createAttribute('id');
|
||||
var classAttr = document.createAttribute('class');
|
||||
var x1attr = document.createAttribute('x1');
|
||||
var y1attr = document.createAttribute('y1');
|
||||
var x2attr = document.createAttribute('x2');
|
||||
var y2attr = document.createAttribute('y2');
|
||||
var styleAttr = document.createAttribute('style');
|
||||
idAttr.value = id;
|
||||
classAttr.value = 'l';
|
||||
x1attr.value = fromX;
|
||||
y1attr.value = fromY;
|
||||
x2attr.value = toX;
|
||||
y2attr.value = toY;
|
||||
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
||||
line.setAttributeNode(idAttr);
|
||||
line.setAttributeNode(classAttr);
|
||||
line.setAttributeNode(x1attr);
|
||||
line.setAttributeNode(y1attr);
|
||||
line.setAttributeNode(x2attr);
|
||||
line.setAttributeNode(y2attr);
|
||||
line.setAttributeNode(styleAttr);
|
||||
svg.appendChild(line);
|
||||
return id;
|
||||
};
|
||||
|
||||
svg.onmousedown = (ev) => {
|
||||
var svgrect = svg.getBoundingClientRect();
|
||||
var minId = '';
|
||||
var minDist = Infinity;
|
||||
var minx = 0;
|
||||
var miny = 0;
|
||||
doneMap = {};
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
var minNode = svg.getElementById(minId);
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
};
|
||||
|
||||
svg.onmouseup = (ev) => {
|
||||
currentStroke = null;
|
||||
doneMap = {};
|
||||
console.log(enteredKey);
|
||||
enteredKey = '';
|
||||
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
||||
svg.removeChild(line);
|
||||
});
|
||||
};
|
||||
|
||||
svg.onmousemove = (ev) => {
|
||||
if (currentStroke != null) {
|
||||
var svgrect = svg.getBoundingClientRect();
|
||||
var minId = '';
|
||||
var minDist = Infinity;
|
||||
var minx = 0;
|
||||
var miny = 0;
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
if (minDist < 2000 && !(minId in doneMap)) {
|
||||
var line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', minx);
|
||||
line.setAttribute('y2', miny);
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
}
|
||||
var line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', ev.offsetX);
|
||||
line.setAttribute('y2', ev.offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue