Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3639638b1c | |||
| eed6bbf3a2 | |||
| bee306895f | |||
| a6006f45ee | |||
| 07ec42da98 | |||
| 555a006042 | |||
| f4e9c06fed | |||
| 4e146d7ffb | |||
| 3d5dd5c571 | |||
| 6112353bb9 | |||
| 2694ef1f75 | |||
| 3905ef4bbd | |||
| 92e6a8965d | |||
| 5ad049f618 | |||
| 99965fff55 | |||
| 65d608a254 | |||
| 1d11626304 | |||
| d22c94fef5 | |||
| e18d4ccc22 | |||
| 409fc76101 | |||
| 9cc1b62a5c | |||
| 96b821d01a | |||
| af9bfcdddc | |||
| 0056feaa7f | |||
| b8da995cd9 | |||
| b290fe35d3 | |||
| 287a441f40 | |||
| a5bfc88554 | |||
| 3a19db0b2d | |||
| 7070e2a273 | |||
| d6f7cdabfe | |||
| f52eae82ab | |||
| e2ce3fa797 | |||
| b5992560e8 | |||
| 03e5d2dcea | |||
| af414d11e1 | |||
| 3d818ad417 | |||
| 93a4971d79 | |||
| 450280e9b9 | |||
| de6e9ba4f0 | |||
| e6d950fe56 | |||
| 7bd3c6ffef | |||
| 435cddc4e4 | |||
| e6986e3587 | |||
| e85e53a48e | |||
| cae77c2d05 | |||
| fde8c33de0 | |||
| 23a45580ee | |||
| b0867f0e44 | |||
| 5fea1e358a | |||
| 962e0d2e41 | |||
| ab54ab981a | |||
| 9b578637ba | |||
| 5bf6040d2c | |||
| 674811b77f | |||
| 3293a10abe | |||
| eaad39b05b | |||
| 7f6416c41a | |||
| 21ab9c7e8b | |||
| ea7585f078 | |||
| e95c6264a0 | |||
| f1c5d83495 | |||
| 7531969627 | |||
| b6da0405fc | |||
| 09f6e1a250 | |||
| 63c487e548 | |||
| d31bf56096 | |||
| 5261a325f3 | |||
| 5fab9cae2f | |||
| c2f2e9ab89 | |||
| 85fe290e83 | |||
| 1c75ecc9a1 | |||
| 9f4d778eb2 | |||
| 21dbce7488 | |||
| 5289cc3698 | |||
| ec86069ec7 | |||
| 8311053d08 | |||
| 6867e71d7c | |||
| 3c4a25e5ad | |||
| 637b00261a | |||
| a06db608bc | |||
| 83cd8725c3 | |||
| 6874f4d5ca | |||
| 281c10ec3f | |||
| 8e27669e03 | |||
| d1e924d638 | |||
| afd9aea763 | |||
| e64d4035a0 | |||
| ad8c1cf115 | |||
| 3064dca6ca | |||
| 6cd8341a2f | |||
| 20bcb896bc | |||
| 03b411e433 | |||
| 18b879be14 | |||
| 68408d0372 | |||
| e0c9f335fc | |||
| feb280ed8c | |||
| cedac41b10 | |||
| 8ebed3cda6 | |||
| 47bda266a4 | |||
| 8c5d4d7301 | |||
| e9ad960971 | |||
| f05bfb8383 | |||
| 3e15b83d2a | |||
| d7f8933f77 | |||
| f2581445d2 | |||
| 5316a3771b | |||
| 26808631ac | |||
| 04ce4dfbb7 | |||
| 885996f25c | |||
| 7deccf7425 | |||
| 7cbcb0b5cb | |||
| 7a8e086073 | |||
| d723c2bbd3 | |||
| d740801eed | |||
| c5ae33f738 | |||
| c6cf9ac795 | |||
| 39e547c303 | |||
| f7bd755f33 | |||
| 1933452338 | |||
| 15ebe08bd5 | |||
| 537940381b | |||
| c0666262be | |||
| ae195d9826 | |||
| 7bbe99852d | |||
| 0f55ec361f | |||
| f00db85541 | |||
| 17c302f999 | |||
| 657d5400f8 | |||
| 6d61e7c41f | |||
| 6a1dbeab90 | |||
| db9c5437dd | |||
| a590d7c72c | |||
| f90af4fbad | |||
| f2d95da063 | |||
| 7049e9e905 | |||
| 5848b8df8e | |||
| 41911a7589 | |||
| 5c894d7957 | |||
| 167c84645b | |||
| 0ef2280564 | |||
| 90a117ef9a | |||
| 37095bbe79 | |||
| 6237b30e6f | |||
| aa11b1f1c2 | |||
| 08bd67f17f | |||
| 2b08f01e8f | |||
| b9722ffd47 | |||
| 4b71710dc8 | |||
| c149a44d6d | |||
| d40cef9a25 | |||
| beb84a70f4 | |||
| 0a153e72fb | |||
| fc438a8ed2 | |||
| 11ab7b67c3 | |||
| 5c934e51fd | |||
| 18ad5870b7 | |||
| 49c4083882 | |||
| 890e18e9d5 | |||
| d4dd8ac307 | |||
| 1782537006 | |||
| cbf47e7f78 | |||
| b2f9cb596f | |||
| 38e632e938 | |||
| e19aae121b | |||
| ffab88e369 | |||
| 6db8fac605 | |||
| c9307935db | |||
| ecda6c3217 | |||
| 019c78f2a1 | |||
| 212d5a9442 | |||
| 893e096ce6 | |||
| 91344ab775 | |||
| 99b2eb3628 | |||
| a525c4de19 | |||
| 4ec6c58b08 | |||
| 538659c634 | |||
| deabd382be | |||
| 0f57aa267c | |||
| 20b01caf86 | |||
| ac00f6ad33 | |||
| cf84a58577 | |||
| 0548b50b73 | |||
| a571a78215 | |||
| f29a53be94 | |||
| 2d29502e5f | |||
| f9accbffef | |||
| 6681a721a2 | |||
| a0bb9938fa | |||
| 9c49c842f6 | |||
| 89cb8c0943 | |||
| e3fd4f20c9 | |||
| a116d5bc01 | |||
| 7441a80217 | |||
| 451bf1915e | |||
| 66fa62d6be | |||
| 4e2fca2937 |
@@ -1,25 +1,14 @@
|
||||
name: docker
|
||||
name: Docker Image CI
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.0"
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: 1.2.2
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
- name: Build project
|
||||
run: poetry build
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
|
||||
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
|
||||
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
|
||||
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
|
||||
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
|
||||
@@ -1,31 +0,0 @@
|
||||
name: pytest
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.11.0]
|
||||
poetry-version: [1.2.2]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
- name: Pytest and Coverage
|
||||
run: |
|
||||
poetry run coverage run -m --source=stacosys pytest tests
|
||||
poetry run coverage report
|
||||
- name: Send report to Coveralls
|
||||
run: poetry run coveralls
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
@@ -70,6 +70,4 @@ stacosys/lib64
|
||||
workspace.code-workspace
|
||||
*.sqlite
|
||||
config-server.ini
|
||||
config-dev.ini
|
||||
.idea/
|
||||
.python-version
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
#errors-only=
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold under which the program will exit with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regular expressions patterns to the
|
||||
# ignore-list. The regex matches against paths and can be in Posix or Windows
|
||||
# format. Because '\' represents the directory delimiter on Windows systems, it
|
||||
# can't be used as an escape character.
|
||||
ignore-paths=
|
||||
|
||||
# Files or directories matching the regular expression patterns are skipped.
|
||||
# The regex matches against base names, not paths. The default value ignores
|
||||
# Emacs file locks
|
||||
ignore-patterns=^\.#
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.11
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
too-few-public-methods
|
||||
|
||||
# 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
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[METHOD_ARGS]
|
||||
|
||||
# List of qualified names (i.e., library.method) which require a timeout
|
||||
# parameter e.g. 'requests.api.get,requests.api.post'
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=yes
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the 'python-enchant' package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
+5
-13
@@ -1,24 +1,16 @@
|
||||
FROM python:3.11.0-alpine
|
||||
FROM python:3.9-alpine
|
||||
|
||||
ARG STACOSYS_VERSION=3.3
|
||||
ARG STACOSYS_VERSION=2.0
|
||||
ARG STACOSYS_FILENAME=stacosys-${STACOSYS_VERSION}-py3-none-any.whl
|
||||
|
||||
RUN apk update && apk add bash && apk add wget
|
||||
|
||||
# Timezone
|
||||
RUN apk add tzdata
|
||||
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
|
||||
RUN echo "Europe/Paris" > /etc/timezone
|
||||
|
||||
# Clean apk cache
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
RUN apk update && apk add bash && apk add wget && rm -rf /var/cache/apk/*
|
||||
|
||||
COPY docker/docker-init.sh /usr/local/bin/
|
||||
RUN chmod +x usr/local/bin/docker-init.sh
|
||||
|
||||
RUN cd /
|
||||
COPY dist/${STACOSYS_FILENAME} /
|
||||
#RUN wget https://github.com/kianby/stacosys/releases/download/${STACOSYS_VERSION}/${STACOSYS_FILENAME}
|
||||
#COPY ${STACOSYS_FILENAME} /
|
||||
RUN wget https://github.com/kianby/stacosys/releases/download/${STACOSYS_VERSION}/${STACOSYS_FILENAME}
|
||||
RUN python3 -m pip install ${STACOSYS_FILENAME} --target /stacosys
|
||||
RUN rm -f ${STACOSYS_FILENAME}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
all: black test typehint lint
|
||||
|
||||
black:
|
||||
poetry run isort --multi-line 3 --profile black stacosys/ tests/
|
||||
poetry run black --target-version py311 stacosys/ tests/
|
||||
|
||||
test:
|
||||
poetry run coverage run -m --source=stacosys pytest
|
||||
poetry run coverage report
|
||||
|
||||
typehint:
|
||||
poetry run mypy --ignore-missing-imports stacosys/ tests/
|
||||
|
||||
lint:
|
||||
poetry run pylint stacosys/
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[](https://gitlicense.com/license/kianby/stacosys)
|
||||
[](https://www.python.org/) [](https://github.com/psf/black) [](https://flask.palletsprojects.com) [](https://docs.peewee-orm.com/)
|
||||
|
||||
[](https://github.com/kianby/stacosys) [](https://coveralls.io/github/kianby/stacosys?branch=main) [](https://hub.docker.com/r/kianby/stacosys)
|
||||
|
||||
## Stacosys
|
||||
|
||||
Stacosys (aka STAtic blog COmment SYStem) is a fork of [Pecosys](http://github.com/kianby/pecosys) trying to fix Pecosys design drawbacks and to provide a basic alternative to comment hosting services like Disqus. Stacosys works with any static blog or even a simple HTML page.
|
||||
Stacosys (aka STAtic blog COmment SYStem) is a fork of [Pecosys](http://github.com/kianby/pecosys) trying to fix Pecosys design drawbacks and to provide an humble alternative to comment hosting services like Disqus. Stacosys protects your readers's privacy.
|
||||
|
||||
Stacosys works with any static blog or even a simple HTML page. It uses e-mails to communicate with the blog administrator. It doesn't sound *hype* but I'm an old-school guy. E-mails are reliable and an universal way to communicate. You can answer from any device using an e-mail client.
|
||||
|
||||
### Features overview
|
||||
|
||||
@@ -14,24 +11,25 @@ Stacosys main feature is comment management.
|
||||
Here is the workflow:
|
||||
|
||||
- Readers submit comments via a comment form embedded in blog pages
|
||||
- Blog administrator receives an e-mail notification from Stacosys when a
|
||||
- Blog administrator receives an email notification from Stacosys when a
|
||||
comment is submitted
|
||||
- Blog administrator can approve or drop the comment through a simple web admin interface
|
||||
- Blog administrator can approve or drop the comment by replying to e-mail
|
||||
- Stacosys stores approved comment in its database.
|
||||
|
||||
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is optionally requested in submission form to resolve gravatar id but never sent to Stacosys.
|
||||
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is requested in submission form (but optional) to resolve gravatar id and it it not sent to stacosys.
|
||||
|
||||
Stacosys is more or less localized (english and french).
|
||||
Stacosys is localized (english and french).
|
||||
|
||||
### Technically speaking, how does it work?
|
||||
|
||||
Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a request allows retrieving comments for a given page. Similarly, a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose an SMTP configuration is needed.
|
||||
Stacosys can be hosted on the same server or on a different server than the blog. Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a simple request allows to retrieve comments for a given page. Similarly a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose a dedicated email is assigned to Stacosys.
|
||||
|
||||
|
||||
### Little FAQ
|
||||
|
||||
*How do you block spammers?*
|
||||
|
||||
- Current comment form is basic: no captcha support but protected by a honeypot.
|
||||
- Current comment form is basic: no captcha support but a honey pot.
|
||||
|
||||
*Which database is used?*
|
||||
|
||||
@@ -39,15 +37,15 @@ Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-ba
|
||||
|
||||
*Which technologies are used?*
|
||||
|
||||
- [Python](https://www.python.org)
|
||||
- [Python 3.9](https://www.python.org)
|
||||
- [Flask](http://flask.pocoo.org)
|
||||
- [Peewee ORM](http://docs.peewee-orm.com)
|
||||
- [Markdown](http://daringfireball.net/projects/markdown)
|
||||
|
||||
### Installation
|
||||
|
||||
Build and Dependency management relies on [Poetry](https://python-poetry.org/), but you can also use [published releases](https://github.com/kianby/stacosys/releases) or [Docker image](https://hub.docker.com/r/kianby/stacosys).
|
||||
Build is based on [Poetry](https://python-poetry.org/) but you can also use [published releases on GitHub](https://github.com/kianby/stacosys/releases) or the [Docker image](https://hub.docker.com/repository/docker/kianby/stacosys).
|
||||
|
||||
### Improvements
|
||||
|
||||
Stacosys fits my needs, and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork and enhance the project if you need additional features.
|
||||
Stacosys fits my needs and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork the project and enhance the project if you need more features.
|
||||
|
||||
+16
-11
@@ -2,29 +2,34 @@
|
||||
; Default configuration
|
||||
[main]
|
||||
lang = fr
|
||||
db = sqlite://db.sqlite
|
||||
db_sqlite_file = db.sqlite
|
||||
newcomment_polling = 60
|
||||
|
||||
[site]
|
||||
name = "My blog"
|
||||
proto = https
|
||||
url = https://blog.mydomain.com
|
||||
url = http://blog.mydomain.com
|
||||
admin_email = admin@mydomain.com
|
||||
redirect = /redirect
|
||||
|
||||
[http]
|
||||
host = 127.0.0.1
|
||||
port = 8100
|
||||
|
||||
[rss]
|
||||
proto = https
|
||||
file = comments.xml
|
||||
|
||||
[smtp]
|
||||
host = smtp.mail.com
|
||||
port = 465
|
||||
[imap]
|
||||
polling = 120
|
||||
host = mail.gandi.net
|
||||
ssl = false
|
||||
port = 993
|
||||
login = blog@mydomain.com
|
||||
password = MYPASSWORD
|
||||
|
||||
[web]
|
||||
username = admin
|
||||
; SHA-256 hashed password (https://coding.tools/sha256)
|
||||
password = 8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918
|
||||
[smtp]
|
||||
host = mail.gandi.net
|
||||
starttls = true
|
||||
ssl = false
|
||||
port = 587
|
||||
login = blog@mydomain.com
|
||||
password = MYPASSWORD
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
connection = sqlite3.connect("db.sqlite")
|
||||
cursor = connection.cursor()
|
||||
|
||||
script = """
|
||||
CREATE TABLE comment (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
, ulid INTEGER);
|
||||
"""
|
||||
|
||||
cursor.executescript(script)
|
||||
connection.close()
|
||||
@@ -6,30 +6,27 @@ import sqlite3
|
||||
connection = sqlite3.connect("db.sqlite")
|
||||
cursor = connection.cursor()
|
||||
|
||||
# What script performs:
|
||||
# - first, remove site table: crash here if table doesn't exist
|
||||
# (compatibility test without effort)
|
||||
# - remove site_id column from comment table
|
||||
# What script performs:
|
||||
# - first, remove site table: crash here if table doesn't exist (compatibility test without effort)
|
||||
# - remove site_id colum from comment table
|
||||
script = """
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE site;
|
||||
ALTER TABLE comment RENAME TO _comment_old;
|
||||
CREATE TABLE comment (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO comment (id, url, notified, created, published,
|
||||
author_name, author_site, author_gravatar, content)
|
||||
SELECT id, url, notified, created, published,
|
||||
author_name, author_site, author_gravatar, content
|
||||
INSERT INTO comment (id, url, notified, created, published, author_name, author_site, author_gravatar, content)
|
||||
SELECT id, url, notified, created, published, author_name, author_site, author_gravatar, content
|
||||
FROM _comment_old;
|
||||
DROP TABLE _comment_old;
|
||||
COMMIT;
|
||||
@@ -37,4 +34,4 @@ PRAGMA foreign_keys = ON;
|
||||
"""
|
||||
|
||||
cursor.executescript(script)
|
||||
connection.close()
|
||||
connection.close()
|
||||
@@ -1,4 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
extend-ignore = E203
|
||||
spellcheck-targets=comments
|
||||
Generated
+426
-610
File diff suppressed because it is too large
Load Diff
+15
-16
@@ -1,29 +1,28 @@
|
||||
[tool.poetry]
|
||||
name = "stacosys"
|
||||
version = "3.3"
|
||||
version = "2.0b4"
|
||||
description = "STAtic COmmenting SYStem"
|
||||
authors = ["Yax"]
|
||||
readme = "README.md"
|
||||
include = ["run.py"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "~3.11"
|
||||
python = "^3.9"
|
||||
apscheduler = "^3.6.3"
|
||||
pyrss2gen = "^1.1"
|
||||
profig = "^0.5.1"
|
||||
markdown = "^3.1.1"
|
||||
flask_apscheduler = "^1.11.0"
|
||||
Flask = "^2.0.1"
|
||||
peewee = "^3.14.0"
|
||||
requests = "^2.25.1"
|
||||
coverage = "^6.5"
|
||||
background = "^0.2.1"
|
||||
Flask = "^2.1.1"
|
||||
types-markdown = "^3.4.2.1"
|
||||
pydal = "^20221110.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pylint = "^2.15"
|
||||
mypy = "^0.991"
|
||||
pytest = "^7.2.0"
|
||||
coveralls = "^3.3.1"
|
||||
pytest-cov = "^4.0.0"
|
||||
black = "^22.10.0"
|
||||
[tool.poetry.dev-dependencies]
|
||||
rope = "^0.16.0"
|
||||
mypy = "^0.790"
|
||||
flake8-black = "^0.2.1"
|
||||
black = "^20.8b1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from stacosys.conf.config import Config, ConfigParameter
|
||||
from stacosys.db import database
|
||||
from stacosys.core.rss import Rss
|
||||
from stacosys.core.mailer import Mailer
|
||||
from stacosys.interface import app
|
||||
from stacosys.interface import api
|
||||
from stacosys.interface import form
|
||||
from stacosys.interface import scheduler
|
||||
|
||||
|
||||
# configure logging
|
||||
def configure_logging(level):
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(level)
|
||||
# create formatter
|
||||
formatter = logging.Formatter("[%(asctime)s] %(name)s %(levelname)s %(message)s")
|
||||
# add formatter to ch
|
||||
ch.setFormatter(formatter)
|
||||
# add ch to logger
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
|
||||
def stacosys_server(config_pathname):
|
||||
|
||||
# configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
configure_logging(logging.INFO)
|
||||
logging.getLogger("werkzeug").level = logging.WARNING
|
||||
logging.getLogger("apscheduler.executors").level = logging.WARNING
|
||||
|
||||
# check config file exists
|
||||
if not os.path.isfile(config_pathname):
|
||||
logger.error(f"Configuration file '{config_pathname}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
# initialize config
|
||||
conf = Config.load(config_pathname)
|
||||
logger.info(conf.__repr__())
|
||||
|
||||
# check database file exists (prevents from creating a fresh db)
|
||||
db_pathname = conf.get(ConfigParameter.DB_SQLITE_FILE)
|
||||
if not os.path.isfile(db_pathname):
|
||||
logger.error(f"Database file '{db_pathname}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
# initialize database
|
||||
db = database.Database()
|
||||
db.setup(db_pathname)
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
|
||||
# generate RSS for all sites
|
||||
rss = Rss(
|
||||
conf.get(ConfigParameter.LANG),
|
||||
conf.get(ConfigParameter.RSS_FILE),
|
||||
conf.get(ConfigParameter.RSS_PROTO),
|
||||
conf.get(ConfigParameter.SITE_NAME),
|
||||
conf.get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
rss.generate()
|
||||
|
||||
# configure mailer
|
||||
mailer = Mailer(
|
||||
conf.get(ConfigParameter.IMAP_HOST),
|
||||
conf.get_int(ConfigParameter.IMAP_PORT),
|
||||
conf.get_bool(ConfigParameter.IMAP_SSL),
|
||||
conf.get(ConfigParameter.IMAP_LOGIN),
|
||||
conf.get(ConfigParameter.IMAP_PASSWORD),
|
||||
conf.get(ConfigParameter.SMTP_HOST),
|
||||
conf.get_int(ConfigParameter.SMTP_PORT),
|
||||
conf.get_bool(ConfigParameter.SMTP_STARTTLS),
|
||||
conf.get_bool(ConfigParameter.SMTP_SSL),
|
||||
conf.get(ConfigParameter.SMTP_LOGIN),
|
||||
conf.get(ConfigParameter.SMTP_PASSWORD),
|
||||
conf.get(ConfigParameter.SITE_ADMIN_EMAIL)
|
||||
)
|
||||
|
||||
# configure mailer logger
|
||||
mail_handler = mailer.get_error_handler()
|
||||
logger.addHandler(mail_handler)
|
||||
app.logger.addHandler(mail_handler)
|
||||
|
||||
# configure scheduler
|
||||
conf.set(ConfigParameter.SITE_TOKEN, hashlib.sha1(conf.get(ConfigParameter.SITE_NAME)).hexdigest())
|
||||
scheduler.configure(
|
||||
conf.get_int(ConfigParameter.IMAP_POLLING),
|
||||
conf.get_int(ConfigParameter.COMMENT_POLLING),
|
||||
conf.get(ConfigParameter.LANG),
|
||||
conf.get(ConfigParameter.SITE_NAME),
|
||||
conf.get(ConfigParameter.SITE_TOKEN),
|
||||
conf.get(ConfigParameter.SITE_ADMIN_EMAIL),
|
||||
mailer,
|
||||
rss,
|
||||
)
|
||||
|
||||
# inject config parameters into flask
|
||||
app.config.update(SITE_TOKEN=conf.get(ConfigParameter.SITE_TOKEN))
|
||||
logger.info(f"start interfaces {api} {form}")
|
||||
|
||||
# start Flask
|
||||
app.run(
|
||||
host=conf.get(ConfigParameter.HTTP_HOST),
|
||||
port=conf.get(ConfigParameter.HTTP_PORT),
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config", help="config path name")
|
||||
args = parser.parse_args()
|
||||
stacosys_server(args.config)
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": ".",
|
||||
"folder_exclude_patterns": ["__pycache__",".pytest_cache", ".mypy_cache"],
|
||||
"file_exclude_patterns": [".coverage"]
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"LSP": {
|
||||
"LSP-pyright": {
|
||||
"settings": {
|
||||
"python.analysis.diagnosticMode": "workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
{
|
||||
"auto_complete":
|
||||
{
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"init",
|
||||
"init_config"
|
||||
],
|
||||
[
|
||||
"author",
|
||||
"author_name="
|
||||
],
|
||||
[
|
||||
"autho",
|
||||
"author_name"
|
||||
],
|
||||
[
|
||||
"au",
|
||||
"author_gravatar="
|
||||
],
|
||||
[
|
||||
"auth",
|
||||
"author_site="
|
||||
],
|
||||
[
|
||||
"db",
|
||||
"db_uri"
|
||||
],
|
||||
[
|
||||
"db_",
|
||||
"db_uri"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"rss"
|
||||
],
|
||||
[
|
||||
"c",
|
||||
"check"
|
||||
],
|
||||
[
|
||||
"con",
|
||||
"configure_destination"
|
||||
],
|
||||
[
|
||||
"EXP",
|
||||
"EXPECTED_HTTP_PORT"
|
||||
],
|
||||
[
|
||||
"l",
|
||||
"login"
|
||||
],
|
||||
[
|
||||
"assertI",
|
||||
"assertIsNot"
|
||||
],
|
||||
[
|
||||
"Confi",
|
||||
"ConfigParameter"
|
||||
],
|
||||
[
|
||||
"S",
|
||||
"SITE_URL"
|
||||
],
|
||||
[
|
||||
"Config",
|
||||
"ConfigParameter"
|
||||
],
|
||||
[
|
||||
"s",
|
||||
"SITE_REDIRECT"
|
||||
],
|
||||
[
|
||||
"get",
|
||||
"get_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
"buffers":
|
||||
[
|
||||
{
|
||||
"file": "stacosys/service/rssfeed.py",
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 1754,
|
||||
"line_ending": "Unix"
|
||||
}
|
||||
}
|
||||
],
|
||||
"build_system": "",
|
||||
"build_system_choices":
|
||||
[
|
||||
],
|
||||
"build_varint": "",
|
||||
"command_palette":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"dia",
|
||||
"LSP: Toggle Diagnostics Panel"
|
||||
],
|
||||
[
|
||||
"black",
|
||||
"python-black: Create Black Configuration File"
|
||||
],
|
||||
[
|
||||
"insta",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"break",
|
||||
"Python Breakpoints: Toggle"
|
||||
],
|
||||
[
|
||||
"togg",
|
||||
"Python Breakpoints: Toggle"
|
||||
],
|
||||
[
|
||||
"diag",
|
||||
"LSP: Toggle Diagnostics Panel"
|
||||
],
|
||||
[
|
||||
"lsp",
|
||||
"LSP: Toggle Diagnostics Panel"
|
||||
],
|
||||
[
|
||||
"comment",
|
||||
"Toggle Comment"
|
||||
],
|
||||
[
|
||||
"install",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"rename",
|
||||
"LSP: Rename"
|
||||
],
|
||||
[
|
||||
"brea",
|
||||
"Python Breakpoints: Clear All"
|
||||
],
|
||||
[
|
||||
"comm",
|
||||
"Toggle Comment"
|
||||
],
|
||||
[
|
||||
"move",
|
||||
"File: Move…"
|
||||
],
|
||||
[
|
||||
"remove",
|
||||
"Package Control: Remove Package"
|
||||
],
|
||||
[
|
||||
"make",
|
||||
"Build With: Make"
|
||||
],
|
||||
[
|
||||
"python",
|
||||
"Python Breakpoints: Toggle"
|
||||
],
|
||||
[
|
||||
"docstr",
|
||||
"AutoDocstring: Current"
|
||||
],
|
||||
[
|
||||
"Package Control: ",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"distr",
|
||||
"View: Toggle Distraction Free"
|
||||
],
|
||||
[
|
||||
"upg",
|
||||
"Package Control: Upgrade Package"
|
||||
],
|
||||
[
|
||||
"form",
|
||||
"LSP: Format Document"
|
||||
],
|
||||
[
|
||||
"format",
|
||||
"LSP: Format Document"
|
||||
],
|
||||
[
|
||||
"lspc",
|
||||
"LSP: Clear Diagnostics"
|
||||
],
|
||||
[
|
||||
"ren",
|
||||
"LSP: Rename Symbol"
|
||||
],
|
||||
[
|
||||
"instal",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"package re",
|
||||
"Package Control: Remove Package"
|
||||
],
|
||||
[
|
||||
"paka",
|
||||
"Package Control: Remove Package"
|
||||
],
|
||||
[
|
||||
"inst",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"pac",
|
||||
"Package Control: Remove Package"
|
||||
]
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"console":
|
||||
{
|
||||
"height": 164.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"distraction_free":
|
||||
{
|
||||
"menu_visible": true,
|
||||
"show_minimap": false,
|
||||
"show_open_files": false,
|
||||
"show_tabs": false,
|
||||
"side_bar_visible": false,
|
||||
"status_bar_visible": false
|
||||
},
|
||||
"expanded_folders":
|
||||
[
|
||||
"/home/yannic/work/stacosys",
|
||||
"/home/yannic/work/stacosys/dbmigration",
|
||||
"/home/yannic/work/stacosys/docker",
|
||||
"/home/yannic/work/stacosys/stacosys",
|
||||
"/home/yannic/work/stacosys/stacosys/db",
|
||||
"/home/yannic/work/stacosys/stacosys/interface",
|
||||
"/home/yannic/work/stacosys/stacosys/interface/web",
|
||||
"/home/yannic/work/stacosys/stacosys/model",
|
||||
"/home/yannic/work/stacosys/stacosys/service",
|
||||
"/home/yannic/work/stacosys/tests"
|
||||
],
|
||||
"file_history":
|
||||
[
|
||||
"/home/yannic/work/stacosys/stacosys/db/__init__.py",
|
||||
"/home/yannic/work/stacosys/tests/test_api.py",
|
||||
"/home/yannic/work/stacosys/tests/test_db.py",
|
||||
"/home/yannic/work/stacosys/stacosys/service/configuration.py",
|
||||
"/home/yannic/work/stacosys/Makefile",
|
||||
"/home/yannic/work/stacosys/stacosys/db/dao.py",
|
||||
"/home/yannic/work/stacosys/pyproject.toml",
|
||||
"/home/yannic/work/stacosys/stacosys/run.py",
|
||||
"/home/yannic/work/stacosys/config-dev.ini",
|
||||
"/home/yannic/work/stacosys/dbmigration/migrate_from_3.3_to_4.0.py",
|
||||
"/home/yannic/work/stacosys/stacosys/service/rssfeed.py",
|
||||
"/home/yannic/work/stacosys/stacosys/model/comment.py",
|
||||
"/home/yannic/work/stacosys/tests/test_form.py",
|
||||
"/home/yannic/work/stacosys/config.ini",
|
||||
"/home/yannic/work/stacosys/tests/test_config.py",
|
||||
"/home/yannic/work/stacosys/dbmigration/migrate_from_1.1_to_2.0.py",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/pydal/objects.py",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/markdown/extensions/def_list.py",
|
||||
"/home/yannic/work/stacosys/stacosys/db/database.py",
|
||||
"/home/yannic/work/stacosys/tests/test_rssfeed.py",
|
||||
"/home/yannic/work/stacosys/stacosys/service/mail.py",
|
||||
"/home/yannic/work/stacosys/stacosys/interface/form.py",
|
||||
"/home/yannic/work/stacosys/stacosys/interface/__init__.py",
|
||||
"/home/yannic/work/stacosys/stacosys/interface/api.py",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/peewee.py",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/background.py",
|
||||
"/home/yannic/work/stacosys/tests/test_mail.py",
|
||||
"/home/yannic/.cache/sublime-text-3/Package Storage/LSP-pyright/18.7.0/language-server/node_modules/pyright/dist/typeshed-fallback/stdlib/builtins.pyi",
|
||||
"/home/yannic/work/stacosys/.venv/lib64/python3.11/site-packages/mypy/typeshed/stdlib/socket.pyi",
|
||||
"/usr/lib64/python3.11/smtplib.py",
|
||||
"/home/yannic/work/stacosys/stacosys/interface/web/admin.py",
|
||||
"/home/yannic/work/stacosys/comments.xml",
|
||||
"/home/yannic/work/stacosys/stacosys/service/__init__.py",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/PyRSS2Gen.py",
|
||||
"/home/yannic/work/blog/README.md",
|
||||
"/home/yannic/work/blog/Dockerfile",
|
||||
"/usr/lib64/python3.11/logging/__init__.py",
|
||||
"/home/yannic/work/stacosys/stacosys/core/__init__.py",
|
||||
"/home/yannic/work/stacosys/stacosys/core/configuration.py",
|
||||
"/home/yannic/work/stacosys/.pylintrc",
|
||||
"/home/yannic/work/stacosys/stacosys/core/mailer.py",
|
||||
"/home/yannic/work/stacosys/stacosys/conf/config.py",
|
||||
"/home/yannic/work/stacosys/stacosys/core/rss.py",
|
||||
"/home/yannic/work/stacosys/run.sh",
|
||||
"/home/yannic/work/stacosys/stacosys.sublime-project",
|
||||
"/home/yannic/work/stacosys/pylintrc",
|
||||
"/home/yannic/work/stacosys/.venv/lib/python3.11/site-packages/flask/app.py",
|
||||
"/home/yannic/.cache/sublime-text-3/Package Storage/LSP-pyright/18.7.0/language-server/node_modules/pyright/dist/typeshed-fallback/stdlib/sys.pyi",
|
||||
"/home/yannic/work/stacosys/flake8.ini",
|
||||
"/home/yannic/work/stacosys/stacosys/__init__.py"
|
||||
],
|
||||
"find":
|
||||
{
|
||||
"height": 28.0
|
||||
},
|
||||
"find_in_files":
|
||||
{
|
||||
"height": 104.0,
|
||||
"where_history":
|
||||
[
|
||||
""
|
||||
]
|
||||
},
|
||||
"find_state":
|
||||
{
|
||||
"case_sensitive": false,
|
||||
"find_history":
|
||||
[
|
||||
"bla",
|
||||
"find_not_published_comments",
|
||||
"find_comment_by_id",
|
||||
"asdict",
|
||||
"def delete",
|
||||
"db_dal",
|
||||
"tox",
|
||||
"apscheduler",
|
||||
"_lang",
|
||||
"config",
|
||||
"SITE_TOKE",
|
||||
"app.conf",
|
||||
"disable",
|
||||
"background"
|
||||
],
|
||||
"highlight": true,
|
||||
"in_selection": false,
|
||||
"preserve_case": false,
|
||||
"regex": false,
|
||||
"replace_history":
|
||||
[
|
||||
],
|
||||
"reverse": false,
|
||||
"scrollbar_highlights": true,
|
||||
"show_context": true,
|
||||
"use_buffer2": true,
|
||||
"use_gitignore": true,
|
||||
"whole_word": false,
|
||||
"wrap": true
|
||||
},
|
||||
"groups":
|
||||
[
|
||||
{
|
||||
"sheets":
|
||||
[
|
||||
{
|
||||
"buffer": 0,
|
||||
"file": "stacosys/service/rssfeed.py",
|
||||
"selected": true,
|
||||
"semi_transient": true,
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 1754,
|
||||
"regions":
|
||||
{
|
||||
},
|
||||
"selection":
|
||||
[
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"auto_complete_triggers":
|
||||
[
|
||||
{
|
||||
"characters": "<",
|
||||
"selector": "text.html, text.xml"
|
||||
},
|
||||
{
|
||||
"rhs_empty": true,
|
||||
"selector": "punctuation.accessor"
|
||||
},
|
||||
{
|
||||
"characters": ".[",
|
||||
"selector": "meta.tag, source - comment - string.quoted.double.block - string.quoted.single.block - string.unquoted.heredoc",
|
||||
"server": "LSP-pyright"
|
||||
}
|
||||
],
|
||||
"lsp_active": true,
|
||||
"lsp_hover_provider_count": 1,
|
||||
"lsp_uri": "file:///home/yannic/work/stacosys/stacosys/service/rssfeed.py",
|
||||
"show_definitions": false,
|
||||
"syntax": "Packages/Python/Python.sublime-syntax",
|
||||
"tab_size": 4,
|
||||
"translate_tabs_to_spaces": true
|
||||
},
|
||||
"translation.x": 0.0,
|
||||
"translation.y": 0.0,
|
||||
"zoom_level": 1.0
|
||||
},
|
||||
"stack_index": 0,
|
||||
"stack_multiselect": false,
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"incremental_find":
|
||||
{
|
||||
"height": 28.0
|
||||
},
|
||||
"input":
|
||||
{
|
||||
"height": 40.0
|
||||
},
|
||||
"layout":
|
||||
{
|
||||
"cells":
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
]
|
||||
],
|
||||
"cols":
|
||||
[
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"rows":
|
||||
[
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"menu_visible": true,
|
||||
"output.LSP Log Panel":
|
||||
{
|
||||
"height": 0.0
|
||||
},
|
||||
"output.diagnostics":
|
||||
{
|
||||
"height": 261.0
|
||||
},
|
||||
"output.exec":
|
||||
{
|
||||
"height": 132.0
|
||||
},
|
||||
"output.find_results":
|
||||
{
|
||||
"height": 0.0
|
||||
},
|
||||
"output.language servers":
|
||||
{
|
||||
"height": 132.0
|
||||
},
|
||||
"output.mdpopups":
|
||||
{
|
||||
"height": 0.0
|
||||
},
|
||||
"output.references":
|
||||
{
|
||||
"height": 208.0
|
||||
},
|
||||
"pinned_build_system": "",
|
||||
"project": "stacosys.sublime-project",
|
||||
"replace":
|
||||
{
|
||||
"height": 52.0
|
||||
},
|
||||
"save_all_on_build": true,
|
||||
"select_file":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"def del",
|
||||
".venv/lib/python3.11/site-packages/markdown/extensions/def_list.py"
|
||||
],
|
||||
[
|
||||
"socket.p",
|
||||
".venv/lib64/python3.11/site-packages/mypy/typeshed/stdlib/socket.pyi"
|
||||
],
|
||||
[
|
||||
"mail",
|
||||
"stacosys/service/mail.py"
|
||||
],
|
||||
[
|
||||
"conf",
|
||||
"stacosys/service/configuration.py"
|
||||
]
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"select_project":
|
||||
{
|
||||
"height": 500.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"",
|
||||
"~/work/blog/blog.sublime-project"
|
||||
]
|
||||
],
|
||||
"width": 380.0
|
||||
},
|
||||
"select_symbol":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"selected_group": 0,
|
||||
"settings":
|
||||
{
|
||||
},
|
||||
"show_minimap": true,
|
||||
"show_open_files": false,
|
||||
"show_tabs": true,
|
||||
"side_bar_visible": true,
|
||||
"side_bar_width": 299.0,
|
||||
"status_bar_visible": true,
|
||||
"template_settings":
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "2.0"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import profig
|
||||
|
||||
|
||||
class ConfigParameter(Enum):
|
||||
DB_SQLITE_FILE = "main.db_sqlite_file"
|
||||
LANG = "main.lang"
|
||||
COMMENT_POLLING = "main.newcomment_polling"
|
||||
|
||||
HTTP_HOST = "http.host"
|
||||
HTTP_PORT = "http.port"
|
||||
|
||||
RSS_PROTO = "rss.proto"
|
||||
RSS_FILE = "rss.file"
|
||||
|
||||
IMAP_POLLING = "imap.polling"
|
||||
IMAP_SSL = "imap.ssl"
|
||||
IMAP_HOST = "imap.host"
|
||||
IMAP_PORT = "imap.port"
|
||||
IMAP_LOGIN = "imap.login"
|
||||
IMAP_PASSWORD = "imap.password"
|
||||
|
||||
SMTP_STARTTLS = "smtp.starttls"
|
||||
SMTP_SSL = "smtp.ssl"
|
||||
SMTP_HOST = "smtp.host"
|
||||
SMTP_PORT = "smtp.port"
|
||||
SMTP_LOGIN = "smtp.login"
|
||||
SMTP_PASSWORD = "smtp.password"
|
||||
|
||||
SITE_NAME = "site.name"
|
||||
SITE_URL = "site.url"
|
||||
SITE_TOKEN = "site.token"
|
||||
SITE_ADMIN_EMAIL = "site.admin_email"
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self._params = dict()
|
||||
|
||||
@classmethod
|
||||
def load(cls, config_pathname):
|
||||
cfg = profig.Config(config_pathname)
|
||||
cfg.sync()
|
||||
config = cls()
|
||||
config._params.update(cfg)
|
||||
return config
|
||||
|
||||
def exists(self, key: ConfigParameter):
|
||||
return key.value in self._params
|
||||
|
||||
def get(self, key: ConfigParameter):
|
||||
return self._params[key.value] if key.value in self._params else None
|
||||
|
||||
def put(self, key: ConfigParameter, value):
|
||||
self._params[key.value] = value
|
||||
|
||||
def get_int(self, key: ConfigParameter):
|
||||
return int(self._params[key.value])
|
||||
|
||||
def get_bool(self, key: ConfigParameter):
|
||||
value = self._params[key.value].lower()
|
||||
assert value in ("yes", "true", "no", "false")
|
||||
return value in ("yes", "true")
|
||||
|
||||
def __repr__(self):
|
||||
return self._params.__repr__()
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from stacosys.core.mailer import Mailer
|
||||
from stacosys.core.rss import Rss
|
||||
from stacosys.core.templater import Templater, Template
|
||||
from stacosys.db import dao
|
||||
from stacosys.model.email import Email
|
||||
|
||||
REGEX_EMAIL_SUBJECT = r".*STACOSYS.*\[(\d+)\:(\w+)\]"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
|
||||
templater = Templater(template_path)
|
||||
|
||||
|
||||
def fetch_mail_answers(lang, mailer: Mailer, rss: Rss, site_token):
|
||||
for msg in mailer.fetch():
|
||||
# filter stacosys e-mails
|
||||
m = re.search(REGEX_EMAIL_SUBJECT, msg.subject, re.DOTALL)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
comment_id = int(m.group(1))
|
||||
submitted_token = m.group(2)
|
||||
|
||||
# validate token
|
||||
if submitted_token != site_token:
|
||||
logger.warning("ignore corrupted email. Unknown token %d" % comment_id)
|
||||
continue
|
||||
|
||||
if not msg.plain_text_content:
|
||||
logger.warning("ignore empty email")
|
||||
continue
|
||||
|
||||
_reply_comment_email(lang, mailer, rss, msg, comment_id)
|
||||
mailer.delete(msg.id)
|
||||
|
||||
|
||||
def _reply_comment_email(lang, mailer: Mailer, rss: Rss, email: Email, comment_id):
|
||||
# retrieve comment
|
||||
comment = dao.find_comment_by_id(comment_id)
|
||||
if not comment:
|
||||
logger.warning("unknown comment %d" % comment_id)
|
||||
return
|
||||
|
||||
if comment.published:
|
||||
logger.warning("ignore already published email. token %d" % comment_id)
|
||||
return
|
||||
|
||||
# safe logic: no answer or unknown answer is a go for publishing
|
||||
if email.plain_text_content[:2].upper() == "NO":
|
||||
logger.info("discard comment: %d" % comment_id)
|
||||
dao.delete_comment(comment)
|
||||
new_email_body = templater.get_template(lang, Template.DROP_COMMENT).render(
|
||||
original=email.plain_text_content
|
||||
)
|
||||
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
|
||||
logger.warning("minor failure. cannot send rejection mail " + email.subject)
|
||||
else:
|
||||
# save publishing datetime
|
||||
dao.publish_comment(comment)
|
||||
logger.info("commit comment: %d" % comment_id)
|
||||
|
||||
# rebuild RSS
|
||||
rss.generate()
|
||||
|
||||
# send approval confirmation email to admin
|
||||
new_email_body = templater.get_template(lang, Template.APPROVE_COMMENT).render(
|
||||
original=email.plain_text_content
|
||||
)
|
||||
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
|
||||
logger.warning("minor failure. cannot send approval email " + email.subject)
|
||||
|
||||
|
||||
def submit_new_comment(lang, site_name, site_token, site_admin_email, mailer):
|
||||
for comment in dao.find_not_notified_comments():
|
||||
comment_list = (
|
||||
"author: %s" % comment.author_name,
|
||||
"site: %s" % comment.author_site,
|
||||
"date: %s" % comment.created,
|
||||
"url: %s" % comment.url,
|
||||
"",
|
||||
"%s" % comment.content,
|
||||
"",
|
||||
)
|
||||
comment_text = "\n".join(comment_list)
|
||||
email_body = templater.get_template(lang, Template.NEW_COMMENT).render(
|
||||
url=comment.url, comment=comment_text
|
||||
)
|
||||
|
||||
# send email to notify admin
|
||||
subject = "STACOSYS %s: [%d:%s]" % (site_name, comment.id, site_token)
|
||||
if mailer.send(site_admin_email, subject, email_body):
|
||||
logger.debug("new comment processed ")
|
||||
|
||||
# save notification datetime
|
||||
dao.notify_comment(comment)
|
||||
else:
|
||||
logger.warning("rescheduled. send mail failure " + subject)
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import re
|
||||
from email.message import Message
|
||||
|
||||
from stacosys.model.email import Attachment, Email, Part
|
||||
|
||||
filename_re = re.compile('filename="(.+)"|filename=([^;\n\r"\']+)', re.I | re.S)
|
||||
|
||||
|
||||
class Mailbox(object):
|
||||
def __init__(self, host, port, ssl, login, password):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl = ssl
|
||||
self.login = login
|
||||
self.password = password
|
||||
|
||||
def __enter__(self):
|
||||
if self.ssl:
|
||||
self.imap = imaplib.IMAP4_SSL(self.host, self.port)
|
||||
else:
|
||||
self.imap = imaplib.IMAP4(self.host, self.port)
|
||||
self.imap.login(self.login, self.password)
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.imap.close()
|
||||
self.imap.logout()
|
||||
|
||||
def get_count(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
return sum(1 for num in data[0].split())
|
||||
|
||||
def fetch_raw_message(self, num):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.fetch(str(num), "(RFC822)")
|
||||
email_msg = email.message_from_bytes(data[0][1])
|
||||
return email_msg
|
||||
|
||||
def fetch_message(self, num):
|
||||
raw_msg = self.fetch_raw_message(num)
|
||||
|
||||
parts = []
|
||||
attachments = []
|
||||
plain_text_content = "no plain-text part"
|
||||
for part in raw_msg.walk():
|
||||
if part.is_multipart():
|
||||
continue
|
||||
|
||||
content_disposition = part.get("Content-Disposition", None)
|
||||
if content_disposition:
|
||||
# we have attachment
|
||||
r = filename_re.findall(content_disposition)
|
||||
if r:
|
||||
filename = sorted(r[0])[1]
|
||||
else:
|
||||
filename = "undefined"
|
||||
content = base64.b64encode(part.get_payload(decode=True))
|
||||
content = content.decode()
|
||||
attachments.append(
|
||||
Attachment(
|
||||
filename=email_nonascii_to_uft8(filename),
|
||||
content=content,
|
||||
content_type=part.get_content_type(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
content = to_plain_text_content(part)
|
||||
except Exception:
|
||||
logging.exception("cannot extract content from mail part")
|
||||
|
||||
parts.append(
|
||||
Part(content=content, content_type=part.get_content_type())
|
||||
)
|
||||
|
||||
if part.get_content_type() == "text/plain":
|
||||
plain_text_content = content
|
||||
|
||||
return Email(
|
||||
id=num,
|
||||
encoding="UTF-8",
|
||||
date=parse_date(raw_msg["Date"]).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
from_addr=raw_msg["From"],
|
||||
to_addr=raw_msg["To"],
|
||||
subject=email_nonascii_to_uft8(raw_msg["Subject"]),
|
||||
parts=parts,
|
||||
attachments=attachments,
|
||||
plain_text_content=plain_text_content,
|
||||
)
|
||||
|
||||
def delete_message(self, num):
|
||||
self.imap.select("Inbox")
|
||||
self.imap.store(str(num), "+FLAGS", r"\Deleted")
|
||||
self.imap.expunge()
|
||||
|
||||
def delete_all(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
for num in data[0].split():
|
||||
self.imap.store(num, "+FLAGS", r"\Deleted")
|
||||
self.imap.expunge()
|
||||
|
||||
def print_msgs(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
for num in reversed(data[0].split()):
|
||||
status, data = self.imap.fetch(num, "(RFC822)")
|
||||
self.logger.debug("Message %s\n%s\n" % (num, data[0][1]))
|
||||
|
||||
|
||||
def parse_date(v):
|
||||
if v is None:
|
||||
return datetime.datetime.now()
|
||||
|
||||
tt = email.utils.parsedate_tz(v)
|
||||
|
||||
if tt is None:
|
||||
return datetime.datetime.now()
|
||||
|
||||
timestamp = email.utils.mktime_tz(tt)
|
||||
date = datetime.datetime.fromtimestamp(timestamp)
|
||||
return date
|
||||
|
||||
|
||||
def to_utf8(string, charset):
|
||||
return string.decode(charset).encode("UTF-8").decode("UTF-8")
|
||||
|
||||
|
||||
def email_nonascii_to_uft8(string):
|
||||
|
||||
# RFC 1342 is a recommendation that provides a way to represent non ASCII
|
||||
# characters inside e-mail in a way that won’t confuse e-mail servers
|
||||
subject = ""
|
||||
for v, charset in email.header.decode_header(string):
|
||||
if charset is None:
|
||||
if type(v) is bytes:
|
||||
v = v.decode()
|
||||
subject = subject + v
|
||||
else:
|
||||
subject = subject + to_utf8(v, charset)
|
||||
return subject
|
||||
|
||||
|
||||
def to_plain_text_content(part: Message) -> str:
|
||||
content = part.get_payload(decode=True)
|
||||
charset = part.get_param("charset", None)
|
||||
if charset:
|
||||
content = to_utf8(content, charset)
|
||||
elif type(content) == bytes:
|
||||
content = content.decode("utf8")
|
||||
# RFC 3676: remove automatic word-wrapping
|
||||
return content.replace(" \r\n", " ")
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
import email.utils
|
||||
from email.mime.text import MIMEText
|
||||
from email.message import EmailMessage
|
||||
from logging.handlers import SMTPHandler
|
||||
|
||||
from stacosys.core import imap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailer:
|
||||
def __init__(
|
||||
self,
|
||||
imap_host,
|
||||
imap_port,
|
||||
imap_ssl,
|
||||
imap_login,
|
||||
imap_password,
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_starttls,
|
||||
smtp_ssl,
|
||||
smtp_login,
|
||||
smtp_password,
|
||||
site_admin_email,
|
||||
):
|
||||
self._imap_host = imap_host
|
||||
self._imap_port = imap_port
|
||||
self._imap_ssl = imap_ssl
|
||||
self._imap_login = imap_login
|
||||
self._imap_password = imap_password
|
||||
self._smtp_host = smtp_host
|
||||
self._smtp_port = smtp_port
|
||||
self._smtp_starttls = smtp_starttls
|
||||
self._smtp_ssl = smtp_ssl
|
||||
self._smtp_login = smtp_login
|
||||
self._smtp_password = smtp_password
|
||||
self._site_admin_email = site_admin_email
|
||||
|
||||
def _open_mailbox(self):
|
||||
return imap.Mailbox(
|
||||
self._imap_host,
|
||||
self._imap_port,
|
||||
self._imap_ssl,
|
||||
self._imap_login,
|
||||
self._imap_password,
|
||||
)
|
||||
|
||||
def fetch(self):
|
||||
msgs = []
|
||||
try:
|
||||
with self._open_mailbox() as mbox:
|
||||
count = mbox.get_count()
|
||||
for num in range(count):
|
||||
msgs.append(mbox.fetch_message(num + 1))
|
||||
except Exception:
|
||||
logger.exception("fetch mail exception")
|
||||
return msgs
|
||||
|
||||
def send(self, to_email, subject, message):
|
||||
|
||||
# Create the container (outer) email message.
|
||||
msg = MIMEText(message)
|
||||
msg["Subject"] = subject
|
||||
msg["To"] = to_email
|
||||
msg["From"] = self._smtp_login
|
||||
|
||||
success = True
|
||||
try:
|
||||
if self._smtp_ssl:
|
||||
s = smtplib.SMTP_SSL(self._smtp_host, self._smtp_port)
|
||||
else:
|
||||
s = smtplib.SMTP(self._smtp_host, self._smtp_port)
|
||||
if self._smtp_starttls:
|
||||
s.starttls()
|
||||
if self._smtp_login:
|
||||
s.login(self._smtp_login, self._smtp_password)
|
||||
s.send_message(msg)
|
||||
s.quit()
|
||||
except Exception:
|
||||
logger.exception("send mail exception")
|
||||
success = False
|
||||
return success
|
||||
|
||||
def delete(self, id):
|
||||
try:
|
||||
with self._open_mailbox() as mbox:
|
||||
mbox.delete_message(id)
|
||||
except Exception:
|
||||
logger.exception("delete mail exception")
|
||||
|
||||
def get_error_handler(self):
|
||||
if self._smtp_ssl:
|
||||
mail_handler = SSLSMTPHandler(
|
||||
mailhost=(
|
||||
self._smtp_host,
|
||||
self._smtp_port,
|
||||
),
|
||||
credentials=(
|
||||
self._smtp_login,
|
||||
self._smtp_password,
|
||||
),
|
||||
fromaddr=self._smtp_login,
|
||||
toaddrs=self._site_admin_email,
|
||||
subject="Stacosys error",
|
||||
)
|
||||
else:
|
||||
mail_handler = SMTPHandler(
|
||||
mailhost=(
|
||||
self._smtp_host,
|
||||
self._smtp_port,
|
||||
),
|
||||
credentials=(
|
||||
self._smtp_login,
|
||||
self._smtp_password,
|
||||
),
|
||||
fromaddr=self._smtp_login,
|
||||
toaddrs=self._site_admin_email,
|
||||
subject="Stacosys error",
|
||||
)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
return mail_handler
|
||||
|
||||
|
||||
class SSLSMTPHandler(SMTPHandler):
|
||||
def emit(self, record):
|
||||
"""
|
||||
Emit a record.
|
||||
|
||||
Format the record and send it to the specified addressees.
|
||||
"""
|
||||
try:
|
||||
smtp = smtplib.SMTP_SSL(self.mailhost, self.mailport)
|
||||
msg = EmailMessage()
|
||||
msg["From"] = self.fromaddr
|
||||
msg["To"] = ",".join(self.toaddrs)
|
||||
msg["Subject"] = self.getSubject(record)
|
||||
msg["Date"] = email.utils.localtime()
|
||||
msg.set_content(self.format(record))
|
||||
if self.username:
|
||||
smtp.login(self.username, self.password)
|
||||
smtp.send_message(msg)
|
||||
smtp.quit()
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import markdown
|
||||
import PyRSS2Gen
|
||||
|
||||
from stacosys.core.templater import Templater, Template
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
class Rss:
|
||||
def __init__(
|
||||
self,
|
||||
lang,
|
||||
rss_file,
|
||||
rss_proto,
|
||||
site_name,
|
||||
site_url,
|
||||
):
|
||||
self._lang = lang
|
||||
self._rss_file = rss_file
|
||||
self._rss_proto = rss_proto
|
||||
self._site_name = site_name
|
||||
self._site_url = site_url
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
|
||||
self._templater = Templater(template_path)
|
||||
|
||||
def generate(self):
|
||||
rss_title = self._templater.get_template(
|
||||
self._lang, Template.RSS_TITLE_MESSAGE
|
||||
).render(site=self._site_name)
|
||||
md = markdown.Markdown()
|
||||
|
||||
items = []
|
||||
for row in (
|
||||
Comment.select()
|
||||
.where(Comment.published)
|
||||
.order_by(-Comment.published)
|
||||
.limit(10)
|
||||
):
|
||||
item_link = "%s://%s%s" % (self._rss_proto, self._site_url, row.url)
|
||||
items.append(
|
||||
PyRSS2Gen.RSSItem(
|
||||
title="%s - %s://%s%s"
|
||||
% (self._rss_proto, row.author_name, self._site_url, row.url),
|
||||
link=item_link,
|
||||
description=md.convert(row.content),
|
||||
guid=PyRSS2Gen.Guid("%s/%d" % (item_link, row.id)),
|
||||
pubDate=row.published,
|
||||
)
|
||||
)
|
||||
|
||||
rss = PyRSS2Gen.RSS2(
|
||||
title=rss_title,
|
||||
link="%s://%s" % (self._rss_proto, self._site_url),
|
||||
description='Commentaires du site "%s"' % self._site_name,
|
||||
lastBuildDate=datetime.now(),
|
||||
items=items,
|
||||
)
|
||||
rss.write_xml(open(self._rss_file, "w"), encoding="utf-8")
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import Enum
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
class Template(Enum):
|
||||
DROP_COMMENT = "drop_comment"
|
||||
APPROVE_COMMENT = "approve_comment"
|
||||
NEW_COMMENT = "new_comment"
|
||||
NOTIFY_MESSAGE = "notify_message"
|
||||
RSS_TITLE_MESSAGE = "rss_title_message"
|
||||
|
||||
|
||||
class Templater:
|
||||
def __init__(self, template_path):
|
||||
self._env = Environment(loader=FileSystemLoader(template_path))
|
||||
|
||||
def get_template(self, lang, template: Template):
|
||||
return self._env.get_template(lang + "/" + template.value + ".tpl")
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydal import DAL, Field
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
db_dal = DAL()
|
||||
|
||||
def configure(self, db_uri):
|
||||
self.db_dal = DAL(db_uri, migrate=db_uri.startswith("sqlite:memory"))
|
||||
self.db_dal.define_table(
|
||||
"comment",
|
||||
Field("url"),
|
||||
Field("created", type="datetime"),
|
||||
Field("notified", type="datetime"),
|
||||
Field("published", type="datetime"),
|
||||
Field("author_name"),
|
||||
Field("author_site"),
|
||||
Field("author_gravatar"),
|
||||
Field("content", type="text"),
|
||||
)
|
||||
|
||||
def get(self):
|
||||
return self.db_dal
|
||||
|
||||
|
||||
database = Database()
|
||||
db = database.get
|
||||
|
||||
+18
-44
@@ -1,80 +1,54 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=singleton-comparison
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from stacosys.db import db
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
def find_comment_by_id(comment_id):
|
||||
return db().comment(comment_id)
|
||||
def find_comment_by_id(id):
|
||||
return Comment.get_by_id(id)
|
||||
|
||||
|
||||
def notify_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).update(notified=datetime.now())
|
||||
db().commit()
|
||||
comment.notified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment.save()
|
||||
|
||||
|
||||
def publish_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).update(published=datetime.now())
|
||||
db().commit()
|
||||
comment.published = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment.save()
|
||||
|
||||
|
||||
def delete_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).delete()
|
||||
db().commit()
|
||||
comment.delete_instance()
|
||||
|
||||
|
||||
def find_not_notified_comments():
|
||||
return db()(db().comment.notified == None).select()
|
||||
|
||||
|
||||
def find_not_published_comments():
|
||||
return db()(db().comment.published == None).select()
|
||||
return Comment.select().where(Comment.notified.is_null())
|
||||
|
||||
|
||||
def find_published_comments_by_url(url):
|
||||
return db()((db().comment.url == url) & (db().comment.published != None)).select(
|
||||
orderby=db().comment.published
|
||||
)
|
||||
return Comment.select(Comment).where((Comment.url == url) & (Comment.published.is_null(False))).order_by(
|
||||
+Comment.published)
|
||||
|
||||
|
||||
def count_published_comments(url):
|
||||
return (
|
||||
db()((db().comment.url == url) & (db().comment.published != None)).count()
|
||||
if url
|
||||
else db()(db().comment.published != None).count()
|
||||
)
|
||||
|
||||
|
||||
def find_recent_published_comments():
|
||||
return db()(db().comment.published != None).select(
|
||||
orderby=~db().comment.published, limitby=(0, 10)
|
||||
)
|
||||
return Comment.select(Comment).where(
|
||||
(Comment.url == url) & (Comment.published.is_null(False))).count() if url else Comment.select(Comment).where(
|
||||
Comment.published.is_null(False)).count()
|
||||
|
||||
|
||||
def create_comment(url, author_name, author_site, author_gravatar, message):
|
||||
row = db().comment.insert(
|
||||
created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment = Comment(
|
||||
url=url,
|
||||
author_name=author_name,
|
||||
author_site=author_site,
|
||||
author_gravatar=author_gravatar,
|
||||
content=message,
|
||||
created=datetime.now(),
|
||||
created=created,
|
||||
notified=None,
|
||||
published=None,
|
||||
)
|
||||
db().commit()
|
||||
return Comment(
|
||||
id=row.id,
|
||||
url=row.url,
|
||||
author_name=row.author_name,
|
||||
author_site=row.author_site,
|
||||
author_gravatar=row.author_gravatar,
|
||||
content=row.content,
|
||||
created=row.created,
|
||||
notified=row.notified,
|
||||
published=row.published,
|
||||
)
|
||||
comment.save()
|
||||
return comment
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from peewee import Model
|
||||
from playhouse.db_url import SqliteDatabase
|
||||
|
||||
db = SqliteDatabase(None)
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Database:
|
||||
def get_db(self):
|
||||
return db
|
||||
|
||||
def setup(self, db_url):
|
||||
db.init(db_url)
|
||||
db.connect()
|
||||
|
||||
from stacosys.model.comment import Comment
|
||||
db.create_tables([Comment], safe=True)
|
||||
@@ -1,43 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
import background
|
||||
from flask import Flask
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.service import config, mailer
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Set the secret key to some random bytes. Keep this really secret!
|
||||
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@background.task
|
||||
def submit_new_comment(comment):
|
||||
site_url = config.get(ConfigParameter.SITE_URL)
|
||||
comment_list = (
|
||||
f"Web admin interface: {site_url}/web/admin",
|
||||
"",
|
||||
f"author: {comment.author_name}",
|
||||
f"site: {comment.author_site}",
|
||||
f"date: {comment.created}",
|
||||
f"url: {comment.url}",
|
||||
"",
|
||||
comment.content,
|
||||
"",
|
||||
)
|
||||
email_body = "\n".join(comment_list)
|
||||
|
||||
# send email to notify admin
|
||||
site_name = config.get(ConfigParameter.SITE_NAME)
|
||||
subject = f"STACOSYS {site_name}"
|
||||
if mailer.send(subject, email_body):
|
||||
logger.debug("new comment processed")
|
||||
# save notification datetime
|
||||
dao.notify_comment(comment)
|
||||
|
||||
@@ -6,40 +6,37 @@ import logging
|
||||
from flask import jsonify, request
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app, submit_new_comment
|
||||
from stacosys.interface import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.route("/api/ping", methods=["GET"])
|
||||
@app.route("/ping", methods=["GET"])
|
||||
def ping():
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.route("/api/comments", methods=["GET"])
|
||||
@app.route("/comments", methods=["GET"])
|
||||
def query_comments():
|
||||
comments = []
|
||||
url = request.args.get("url", "")
|
||||
|
||||
logger.info("retrieve comments for url %s", url)
|
||||
logger.info("retrieve comments for url %s" % url)
|
||||
for comment in dao.find_published_comments_by_url(url):
|
||||
comment_dto = {
|
||||
d = {
|
||||
"author": comment.author_name,
|
||||
"content": comment.content,
|
||||
"avatar": comment.author_gravatar,
|
||||
"date": comment.published.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
if comment.author_site:
|
||||
comment_dto["site"] = comment.author_site
|
||||
logger.debug(comment_dto)
|
||||
comments.append(comment_dto)
|
||||
d["site"] = comment.author_site
|
||||
logger.debug(d)
|
||||
comments.append(d)
|
||||
return jsonify({"data": comments})
|
||||
|
||||
|
||||
@app.route("/api/comments/count", methods=["GET"])
|
||||
@app.route("/comments/count", methods=["GET"])
|
||||
def get_comments_count():
|
||||
# send notification for pending e-mails asynchronously
|
||||
for comment in dao.find_not_notified_comments():
|
||||
submit_new_comment(comment)
|
||||
url = request.args.get("url", "")
|
||||
return jsonify({"count": dao.count_published_comments(url)})
|
||||
|
||||
+13
-16
@@ -1,26 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from flask import abort, redirect, request
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app, submit_new_comment
|
||||
from stacosys.service import config
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
from stacosys.interface import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.route("/newcomment", methods=["POST"])
|
||||
def new_form_comment():
|
||||
|
||||
data = request.form
|
||||
logger.info("form data %s", str(data))
|
||||
logger.info("form data " + str(data))
|
||||
|
||||
# honeypot for spammers
|
||||
captcha = data.get("remarque", "")
|
||||
if captcha:
|
||||
logger.warning("discard spam: data %s", data)
|
||||
logger.warning("discard spam: data %s" % data)
|
||||
abort(400)
|
||||
|
||||
url = data.get("url", "")
|
||||
@@ -33,24 +33,21 @@ def new_form_comment():
|
||||
|
||||
# anti-spam again
|
||||
if not url or not author_name or not message:
|
||||
logger.warning("empty field: data %s", data)
|
||||
logger.warning("empty field: data %s" % data)
|
||||
abort(400)
|
||||
if not check_form_data(data.to_dict()):
|
||||
logger.warning("additional field: data %s", data)
|
||||
logger.warning("additional field: data %s" % data)
|
||||
abort(400)
|
||||
|
||||
# add a row to Comment table
|
||||
comment = dao.create_comment(
|
||||
url, author_name, author_site, author_gravatar, message
|
||||
)
|
||||
dao.create_comment(url, author_name, author_site, author_gravatar, message)
|
||||
|
||||
# send notification e-mail asynchronously
|
||||
submit_new_comment(comment)
|
||||
|
||||
return redirect(config.get(ConfigParameter.SITE_REDIRECT), code=302)
|
||||
return redirect("/redirect/", code=302)
|
||||
|
||||
|
||||
def check_form_data(posted_comment):
|
||||
def check_form_data(d):
|
||||
fields = ["url", "message", "site", "remarque", "author", "token", "email"]
|
||||
filtered = dict(filter(lambda x: x[0] not in fields, posted_comment.items()))
|
||||
filtered = dict(filter(lambda x: x[0] not in fields, d.items()))
|
||||
return not filtered
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask_apscheduler import APScheduler
|
||||
from stacosys.interface import app
|
||||
|
||||
|
||||
class JobConfig(object):
|
||||
|
||||
JOBS: list = []
|
||||
|
||||
SCHEDULER_EXECUTORS = {"default": {"type": "threadpool", "max_workers": 4}}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
imap_polling_seconds,
|
||||
new_comment_polling_seconds,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
):
|
||||
self.JOBS = [
|
||||
{
|
||||
"id": "fetch_mail",
|
||||
"func": "stacosys.core.cron:fetch_mail_answers",
|
||||
"args": [lang, mailer, rss, site_token],
|
||||
"trigger": "interval",
|
||||
"seconds": imap_polling_seconds,
|
||||
},
|
||||
{
|
||||
"id": "submit_new_comment",
|
||||
"func": "stacosys.core.cron:submit_new_comment",
|
||||
"args": [lang, site_name, site_token, site_admin_email, mailer],
|
||||
"trigger": "interval",
|
||||
"seconds": new_comment_polling_seconds,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def configure(
|
||||
imap_polling,
|
||||
comment_polling,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
):
|
||||
app.config.from_object(
|
||||
JobConfig(
|
||||
imap_polling,
|
||||
comment_polling,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
)
|
||||
)
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
@@ -1,64 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Modération des commentaires</h2>
|
||||
<nav>
|
||||
<a href="/web/logout">Déconnecter</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Auteur</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Article</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comment in comments %}
|
||||
<tr>
|
||||
<td>{{ comment.created }}</td>
|
||||
<td>{{ comment.author_name }}</td>
|
||||
<td>{{ comment.content }}</td>
|
||||
<td><a href="{{ baseurl + comment.url }}">{{ comment.url }}</a></td>
|
||||
<td>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="APPROVE">
|
||||
<button type="submit">Accepter</button>
|
||||
</form>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="REJECT">
|
||||
<button type="submit">Rejeter</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
<footer>
|
||||
<p>Cette page a été conçue par Yax avec <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
form {
|
||||
width: 350px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Modération des commentaires</h2>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form action="/web/login" method="POST">
|
||||
<p><label>Utilisateur </label></p>
|
||||
<p><input type="text" name="username" /></p>
|
||||
<p><label>Mot de passe </label></p>
|
||||
<p><input type="password" name="password" /></p>
|
||||
<input type="submit" value="Connecter" />
|
||||
</form>
|
||||
</main>
|
||||
<footer>
|
||||
<p>Cette page a été conçue avec <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from flask import flash, redirect, render_template, request, session
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app
|
||||
from stacosys.service import config, rss
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app.add_url_rule("/web", endpoint="index")
|
||||
app.add_url_rule("/web/", endpoint="index")
|
||||
|
||||
|
||||
@app.endpoint("index")
|
||||
def index():
|
||||
return redirect("/web/admin")
|
||||
|
||||
|
||||
def is_login_ok(username, password):
|
||||
hashed = hashlib.sha256(password.encode()).hexdigest().upper()
|
||||
return (
|
||||
config.get(ConfigParameter.WEB_USERNAME) == username
|
||||
and config.get(ConfigParameter.WEB_PASSWORD) == hashed
|
||||
)
|
||||
|
||||
|
||||
@app.route("/web/login", methods=["POST", "GET"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
if is_login_ok(username, password):
|
||||
session["user"] = username
|
||||
return redirect("/web/admin")
|
||||
# TODO localization
|
||||
flash("Identifiant ou mot de passe incorrect")
|
||||
return redirect("/web/login")
|
||||
# GET
|
||||
return render_template("login_" + config.get(ConfigParameter.LANG) + ".html")
|
||||
|
||||
|
||||
@app.route("/web/logout", methods=["GET"])
|
||||
def logout():
|
||||
session.pop("user")
|
||||
return redirect("/web/admin")
|
||||
|
||||
|
||||
@app.route("/web/admin", methods=["GET"])
|
||||
def admin_homepage():
|
||||
if not (
|
||||
"user" in session
|
||||
and session["user"] == config.get(ConfigParameter.WEB_USERNAME)
|
||||
):
|
||||
# TODO localization
|
||||
flash("Vous avez été déconnecté.")
|
||||
return redirect("/web/login")
|
||||
|
||||
comments = dao.find_not_published_comments()
|
||||
return render_template(
|
||||
"admin_" + config.get(ConfigParameter.LANG) + ".html",
|
||||
comments=comments,
|
||||
baseurl=config.get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/web/admin", methods=["POST"])
|
||||
def admin_action():
|
||||
comment = dao.find_comment_by_id(request.form.get("comment"))
|
||||
if comment is None:
|
||||
# TODO localization
|
||||
flash("Commentaire introuvable")
|
||||
elif request.form.get("action") == "APPROVE":
|
||||
dao.publish_comment(comment)
|
||||
rss.generate()
|
||||
# TODO localization
|
||||
flash("Commentaire publié")
|
||||
else:
|
||||
dao.delete_comment(comment)
|
||||
# TODO localization
|
||||
flash("Commentaire supprimé")
|
||||
return redirect("/web/admin")
|
||||
+14
-14
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from peewee import CharField
|
||||
from peewee import DateTimeField
|
||||
from peewee import TextField
|
||||
|
||||
from stacosys.db.database import BaseModel
|
||||
|
||||
|
||||
@dataclass
|
||||
class Comment:
|
||||
id: int = 0
|
||||
url: str = ""
|
||||
created: Optional[datetime] = None
|
||||
notified: Optional[datetime] = None
|
||||
published: Optional[datetime] = None
|
||||
author_name: str = ""
|
||||
author_site: str = ""
|
||||
author_gravatar: str = ""
|
||||
content: str = ""
|
||||
class Comment(BaseModel):
|
||||
url = CharField()
|
||||
created = DateTimeField()
|
||||
notified = DateTimeField(null=True, default=None)
|
||||
published = DateTimeField(null=True, default=None)
|
||||
author_name = CharField()
|
||||
author_site = CharField(default="")
|
||||
author_gravatar = CharField(default="")
|
||||
content = TextField()
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from stacosys.db import database
|
||||
from stacosys.interface import api, app, form
|
||||
from stacosys.interface.web import admin
|
||||
from stacosys.service import config, mailer, rss
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
|
||||
# configure logging
|
||||
def configure_logging(level):
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(level)
|
||||
formatter = logging.Formatter("[%(asctime)s] %(name)s %(levelname)s %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
|
||||
def stacosys_server(config_pathname):
|
||||
# configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
configure_logging(logging.INFO)
|
||||
logging.getLogger("werkzeug").level = logging.WARNING
|
||||
|
||||
# check config file exists
|
||||
if not os.path.isfile(config_pathname):
|
||||
logger.error("Configuration file '%s' not found.", config_pathname)
|
||||
sys.exit(1)
|
||||
|
||||
# load and check config
|
||||
config.load(config_pathname)
|
||||
is_config_ok, erreur_config = config.check()
|
||||
if not is_config_ok:
|
||||
logger.error("Configuration incorrecte '%s'", erreur_config)
|
||||
sys.exit(1)
|
||||
logger.info(config)
|
||||
|
||||
# initialize database
|
||||
database.configure(config.get(ConfigParameter.DB))
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
|
||||
# generate RSS
|
||||
rss.configure(
|
||||
config.get(ConfigParameter.RSS_FILE),
|
||||
config.get(ConfigParameter.SITE_NAME),
|
||||
config.get(ConfigParameter.SITE_PROTO),
|
||||
config.get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
rss.generate()
|
||||
|
||||
# configure mailer
|
||||
mailer.configure_smtp(
|
||||
config.get(ConfigParameter.SMTP_HOST),
|
||||
config.get_int(ConfigParameter.SMTP_PORT),
|
||||
config.get(ConfigParameter.SMTP_LOGIN),
|
||||
config.get(ConfigParameter.SMTP_PASSWORD),
|
||||
)
|
||||
mailer.configure_destination(config.get(ConfigParameter.SITE_ADMIN_EMAIL))
|
||||
mailer.check()
|
||||
|
||||
logger.info("start interfaces %s %s %s", api, form, admin)
|
||||
|
||||
# start Flask
|
||||
app.run(
|
||||
host=config.get(ConfigParameter.HTTP_HOST),
|
||||
port=config.get_int(ConfigParameter.HTTP_PORT),
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config", help="config path name")
|
||||
args = parser.parse_args()
|
||||
stacosys_server(args.config)
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .configuration import Config
|
||||
from .mail import Mailer
|
||||
from .rssfeed import Rss
|
||||
|
||||
config = Config()
|
||||
mailer = Mailer()
|
||||
rss = Rss()
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import configparser
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ConfigParameter(Enum):
|
||||
DB = "main.db"
|
||||
LANG = "main.lang"
|
||||
|
||||
HTTP_HOST = "http.host"
|
||||
HTTP_PORT = "http.port"
|
||||
|
||||
RSS_FILE = "rss.file"
|
||||
|
||||
SMTP_HOST = "smtp.host"
|
||||
SMTP_PORT = "smtp.port"
|
||||
SMTP_LOGIN = "smtp.login"
|
||||
SMTP_PASSWORD = "smtp.password"
|
||||
|
||||
SITE_PROTO = "site.proto"
|
||||
SITE_NAME = "site.name"
|
||||
SITE_URL = "site.url"
|
||||
SITE_ADMIN_EMAIL = "site.admin_email"
|
||||
SITE_REDIRECT = "site.redirect"
|
||||
|
||||
WEB_USERNAME = "web.username"
|
||||
WEB_PASSWORD = "web.password"
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
_cfg = configparser.ConfigParser()
|
||||
|
||||
def load(self, config_pathname):
|
||||
self._cfg.read(config_pathname)
|
||||
|
||||
def _split_key(self, key: ConfigParameter):
|
||||
section, param = str(key.value).split(".")
|
||||
if not param:
|
||||
param = section
|
||||
section = ""
|
||||
return (section, param)
|
||||
|
||||
def exists(self, key: ConfigParameter):
|
||||
section, param = self._split_key(key)
|
||||
return self._cfg.has_option(section, param)
|
||||
|
||||
def get(self, key: ConfigParameter) -> str:
|
||||
section, param = self._split_key(key)
|
||||
return (
|
||||
self._cfg.get(section, param)
|
||||
if self._cfg.has_option(section, param)
|
||||
else ""
|
||||
)
|
||||
|
||||
def put(self, key: ConfigParameter, value):
|
||||
section, param = self._split_key(key)
|
||||
if section and not self._cfg.has_section(section):
|
||||
self._cfg.add_section(section)
|
||||
self._cfg.set(section, param, str(value))
|
||||
|
||||
def get_int(self, key: ConfigParameter) -> int:
|
||||
value = self.get(key)
|
||||
return int(value) if value else 0
|
||||
|
||||
def get_bool(self, key: ConfigParameter) -> bool:
|
||||
value = self.get(key)
|
||||
assert value in (
|
||||
"yes",
|
||||
"true",
|
||||
"no",
|
||||
"false",
|
||||
), f"Parameètre booléen incorrect {key.value}"
|
||||
return value in ("yes", "true")
|
||||
|
||||
def check(self):
|
||||
for key in ConfigParameter:
|
||||
if not self.get(key):
|
||||
return (False, key.value)
|
||||
return (True, None)
|
||||
|
||||
def __repr__(self):
|
||||
dict_repr = {}
|
||||
for section in self._cfg.sections():
|
||||
for option in self._cfg.options(section):
|
||||
dict_repr[".".join([section, option])] = self._cfg.get(section, option)
|
||||
return str(dict_repr)
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailer:
|
||||
def __init__(self) -> None:
|
||||
self._smtp_host: str = ""
|
||||
self._smtp_port: int = 0
|
||||
self._smtp_login: str = ""
|
||||
self._smtp_password: str = ""
|
||||
self._site_admin_email: str = ""
|
||||
|
||||
def configure_smtp(
|
||||
self,
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_login,
|
||||
smtp_password,
|
||||
) -> None:
|
||||
self._smtp_host = smtp_host
|
||||
self._smtp_port = smtp_port
|
||||
self._smtp_login = smtp_login
|
||||
self._smtp_password = smtp_password
|
||||
|
||||
def configure_destination(self, site_admin_email) -> None:
|
||||
self._site_admin_email = site_admin_email
|
||||
|
||||
def check(self):
|
||||
server = smtplib.SMTP_SSL(
|
||||
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
|
||||
)
|
||||
server.login(self._smtp_login, self._smtp_password)
|
||||
server.close()
|
||||
|
||||
def send(self, subject, message) -> bool:
|
||||
sender = self._smtp_login
|
||||
receivers = [self._site_admin_email]
|
||||
|
||||
msg = MIMEText(message)
|
||||
msg["Subject"] = subject
|
||||
msg["To"] = self._site_admin_email
|
||||
msg["From"] = sender
|
||||
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
server = smtplib.SMTP_SSL(
|
||||
self._smtp_host, self._smtp_port, context=ssl.create_default_context()
|
||||
)
|
||||
server.login(self._smtp_login, self._smtp_password)
|
||||
server.send_message(msg, sender, receivers)
|
||||
server.close()
|
||||
success = True
|
||||
except:
|
||||
success = False
|
||||
return success
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import markdown
|
||||
import PyRSS2Gen
|
||||
|
||||
from stacosys.db import dao
|
||||
|
||||
|
||||
class Rss:
|
||||
def __init__(self) -> None:
|
||||
self._rss_file: str = ""
|
||||
self._site_proto: str = ""
|
||||
self._site_name: str = ""
|
||||
self._site_url: str = ""
|
||||
|
||||
def configure(
|
||||
self,
|
||||
rss_file,
|
||||
site_name,
|
||||
site_proto,
|
||||
site_url,
|
||||
) -> None:
|
||||
self._rss_file = rss_file
|
||||
self._site_name = site_name
|
||||
self._site_proto = site_proto
|
||||
self._site_url = site_url
|
||||
|
||||
def generate(self) -> None:
|
||||
markdownizer = markdown.Markdown()
|
||||
|
||||
items = []
|
||||
for row in dao.find_recent_published_comments():
|
||||
item_link = f"{self._site_proto}://{self._site_url}{row.url}"
|
||||
items.append(
|
||||
PyRSS2Gen.RSSItem(
|
||||
title=f"{self._site_proto}://{self._site_url}{row.url} - {row.author_name}",
|
||||
link=item_link,
|
||||
description=markdownizer.convert(row.content),
|
||||
guid=PyRSS2Gen.Guid(f"{item_link}{row.id}"),
|
||||
pubDate=row.published,
|
||||
)
|
||||
)
|
||||
|
||||
rss_title = f"Commentaires du site {self._site_name}"
|
||||
rss = PyRSS2Gen.RSS2(
|
||||
title=rss_title,
|
||||
link=f"{self._site_proto}://{self._site_url}",
|
||||
description=rss_title,
|
||||
lastBuildDate=datetime.now(),
|
||||
items=items,
|
||||
)
|
||||
# pylint: disable=consider-using-with
|
||||
rss.write_xml(open(self._rss_file, "w", encoding="utf-8"), encoding="utf-8")
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
The comment should be published soon. It has been approved.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
The comment will not be published. It has been dropped.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
||||
@@ -0,0 +1,16 @@
|
||||
Hi,
|
||||
|
||||
A new comment has been submitted for post {{ url }}
|
||||
|
||||
You have two choices:
|
||||
- reject the comment by replying NO (or no),
|
||||
- accept the comment by sending back the email as it is.
|
||||
|
||||
If you choose the latter option, Stacosys is going to publish the commennt.
|
||||
|
||||
Please find comment details below:
|
||||
|
||||
{{ comment }}
|
||||
|
||||
--
|
||||
Stacosys
|
||||
@@ -0,0 +1 @@
|
||||
New comment
|
||||
@@ -0,0 +1 @@
|
||||
{{ site }} : comments
|
||||
@@ -0,0 +1,9 @@
|
||||
Bonjour,
|
||||
|
||||
Le commentaire sera bientôt publié. Il a été approuvé.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
||||
@@ -0,0 +1,9 @@
|
||||
Bonjour,
|
||||
|
||||
Le commentaire ne sera pas publié. Il a été rejeté.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
||||
@@ -0,0 +1,16 @@
|
||||
Bonjour,
|
||||
|
||||
Un nouveau commentaire a été posté pour l'article {{ url }}
|
||||
|
||||
Vous avez deux réponses possibles :
|
||||
- rejeter le commentaire en répondant NO (ou no),
|
||||
- accepter le commentaire en renvoyant cet email tel quel.
|
||||
|
||||
Si cette dernière option est choisie, Stacosys publiera le commentaire très bientôt.
|
||||
|
||||
Voici les détails concernant le commentaire :
|
||||
|
||||
{{ comment }}
|
||||
|
||||
--
|
||||
Stacosys
|
||||
@@ -0,0 +1 @@
|
||||
Nouveau commentaire
|
||||
@@ -0,0 +1 @@
|
||||
{{ site }} : commentaires
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import dao, database
|
||||
from stacosys.interface import api, app
|
||||
|
||||
|
||||
def init_test_db():
|
||||
c1 = dao.create_comment("/site1", "Bob", "/bob.site", "", "comment 1")
|
||||
c2 = dao.create_comment("/site2", "Bill", "/bill.site", "", "comment 2")
|
||||
c3 = dao.create_comment("/site3", "Jack", "/jack.site", "", "comment 3")
|
||||
dao.publish_comment(c1)
|
||||
dao.publish_comment(c3)
|
||||
assert c2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
logger = logging.getLogger(__name__)
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
init_test_db()
|
||||
logger.info(f"start interface {api}")
|
||||
return app.test_client()
|
||||
|
||||
|
||||
def test_api_ping(client):
|
||||
resp = client.get("/api/ping")
|
||||
assert resp.data == b"OK"
|
||||
|
||||
|
||||
def test_api_count_global(client):
|
||||
resp = client.get("/api/comments/count")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 2
|
||||
|
||||
|
||||
def test_api_count_url(client):
|
||||
resp = client.get("/api/comments/count?url=/site1")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 1
|
||||
resp = client.get("/api/comments/count?url=/site2")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 0
|
||||
|
||||
|
||||
def test_api_comment(client):
|
||||
resp = client.get("/api/comments?url=/site1")
|
||||
d = json.loads(resp.data)
|
||||
assert d and len(d["data"]) == 1
|
||||
comment = d["data"][0]
|
||||
assert comment["author"] == "Bob"
|
||||
assert comment["content"] == "comment 1"
|
||||
|
||||
|
||||
def test_api_comment_not_found(client):
|
||||
resp = client.get("/api/comments?url=/site2")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["data"] == []
|
||||
+36
-35
@@ -1,47 +1,48 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from stacosys.service import config
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
from stacosys.conf.config import Config, ConfigParameter
|
||||
|
||||
EXPECTED_DB = "sqlite://db.sqlite"
|
||||
EXPECTED_DB_SQLITE_FILE = "db.sqlite"
|
||||
EXPECTED_HTTP_PORT = 8080
|
||||
EXPECTED_LANG = "fr"
|
||||
EXPECTED_IMAP_PORT = "5000"
|
||||
EXPECTED_IMAP_LOGIN = "user"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def init_config():
|
||||
config.put(ConfigParameter.DB, EXPECTED_DB)
|
||||
config.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
|
||||
class ConfigTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.conf = Config()
|
||||
self.conf.put(ConfigParameter.DB_SQLITE_FILE, EXPECTED_DB_SQLITE_FILE)
|
||||
self.conf.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
|
||||
self.conf.put(ConfigParameter.IMAP_PORT, EXPECTED_IMAP_PORT)
|
||||
self.conf.put(ConfigParameter.SMTP_STARTTLS, "yes")
|
||||
self.conf.put(ConfigParameter.IMAP_SSL, "false")
|
||||
|
||||
def test_split_key():
|
||||
section, param = config._split_key(ConfigParameter.HTTP_PORT)
|
||||
assert section == "http" and param == "port"
|
||||
def test_exists(self):
|
||||
self.assertTrue(self.conf.exists(ConfigParameter.DB_SQLITE_FILE))
|
||||
self.assertFalse(self.conf.exists(ConfigParameter.IMAP_HOST))
|
||||
|
||||
def test_get(self):
|
||||
self.assertEqual(self.conf.get(ConfigParameter.DB_SQLITE_FILE), EXPECTED_DB_SQLITE_FILE)
|
||||
self.assertEqual(self.conf.get(ConfigParameter.HTTP_PORT), EXPECTED_HTTP_PORT)
|
||||
self.assertIsNone(self.conf.get(ConfigParameter.HTTP_HOST))
|
||||
self.assertEqual(self.conf.get(ConfigParameter.HTTP_PORT), EXPECTED_HTTP_PORT)
|
||||
self.assertEqual(self.conf.get(ConfigParameter.IMAP_PORT), EXPECTED_IMAP_PORT)
|
||||
self.assertEqual(self.conf.get_int(ConfigParameter.IMAP_PORT), int(EXPECTED_IMAP_PORT))
|
||||
self.assertEqual(self.conf.get_int(ConfigParameter.HTTP_PORT), 8080)
|
||||
self.assertTrue(self.conf.get_bool(ConfigParameter.SMTP_STARTTLS))
|
||||
self.assertFalse(self.conf.get_bool(ConfigParameter.IMAP_SSL))
|
||||
try:
|
||||
self.conf.get_bool(ConfigParameter.DB_SQLITE_FILE)
|
||||
self.assertTrue(False)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
def test_exists(init_config):
|
||||
assert config.exists(ConfigParameter.DB)
|
||||
|
||||
|
||||
def test_get(init_config):
|
||||
assert config.get(ConfigParameter.DB) == EXPECTED_DB
|
||||
assert config.get(ConfigParameter.HTTP_HOST) == ""
|
||||
assert config.get(ConfigParameter.HTTP_PORT) == str(EXPECTED_HTTP_PORT)
|
||||
assert config.get_int(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
|
||||
with pytest.raises(AssertionError):
|
||||
config.get_bool(ConfigParameter.DB)
|
||||
|
||||
|
||||
def test_put(init_config):
|
||||
assert not config.exists(ConfigParameter.LANG)
|
||||
config.put(ConfigParameter.LANG, EXPECTED_LANG)
|
||||
assert config.exists(ConfigParameter.LANG)
|
||||
assert config.get(ConfigParameter.LANG) == EXPECTED_LANG
|
||||
|
||||
|
||||
def test_check(init_config):
|
||||
success, error = config.check()
|
||||
assert not success and error
|
||||
def test_put(self):
|
||||
self.assertFalse(self.conf.exists(ConfigParameter.IMAP_LOGIN))
|
||||
self.conf.put(ConfigParameter.IMAP_LOGIN, EXPECTED_IMAP_LOGIN)
|
||||
self.assertTrue(self.conf.exists(ConfigParameter.IMAP_LOGIN))
|
||||
self.assertEqual(self.conf.get(ConfigParameter.IMAP_LOGIN), EXPECTED_IMAP_LOGIN)
|
||||
|
||||
+50
-119
@@ -1,124 +1,55 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
import unittest
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import dao, database
|
||||
from stacosys.model.comment import Comment
|
||||
from stacosys.db import dao
|
||||
from stacosys.db import database
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_db():
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
class DbTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
db = database.Database()
|
||||
db.setup(":memory:")
|
||||
|
||||
def test_dao_published(self):
|
||||
|
||||
# test count published
|
||||
self.assertEqual(0, dao.count_published_comments(""))
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
self.assertEqual(0, dao.count_published_comments(""))
|
||||
dao.publish_comment(c1)
|
||||
self.assertEqual(1, dao.count_published_comments(""))
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
dao.publish_comment(c2)
|
||||
self.assertEqual(2, dao.count_published_comments(""))
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
dao.publish_comment(c3)
|
||||
self.assertEqual(1, dao.count_published_comments("/post1"))
|
||||
self.assertEqual(2, dao.count_published_comments("/post2"))
|
||||
|
||||
# test find published
|
||||
self.assertEqual(0, len(dao.find_published_comments_by_url("/")))
|
||||
self.assertEqual(1, len(dao.find_published_comments_by_url("/post1")))
|
||||
self.assertEqual(2, len(dao.find_published_comments_by_url("/post2")))
|
||||
|
||||
dao.delete_comment(c1)
|
||||
self.assertEqual(0, len(dao.find_published_comments_by_url("/post1")))
|
||||
|
||||
def test_dao_notified(self):
|
||||
|
||||
# test count notified
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
self.assertEqual(1, len(dao.find_not_notified_comments()))
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
self.assertEqual(2, len(dao.find_not_notified_comments()))
|
||||
dao.notify_comment(c1)
|
||||
dao.notify_comment(c2)
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
self.assertEqual(1, len(dao.find_not_notified_comments()))
|
||||
dao.notify_comment(c3)
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
|
||||
|
||||
def equals_comment(comment: Comment, other):
|
||||
return (
|
||||
comment.id == other.id
|
||||
and comment.author_gravatar == other.author_gravatar
|
||||
and comment.author_name == other.author_name
|
||||
and comment.author_site == other.author_site
|
||||
and comment.content == other.content
|
||||
and comment.created == other.created
|
||||
and comment.notified == other.notified
|
||||
and comment.published == other.published
|
||||
)
|
||||
|
||||
|
||||
def test_find_comment_by_id(setup_db):
|
||||
assert dao.find_comment_by_id(1) is None
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert c1.id is not None
|
||||
find_c1 = dao.find_comment_by_id(c1.id)
|
||||
assert find_c1
|
||||
assert equals_comment(c1, find_c1)
|
||||
c1.id = find_c1.id
|
||||
dao.delete_comment(c1)
|
||||
assert dao.find_comment_by_id(c1.id) is None
|
||||
|
||||
|
||||
def test_dao_published(setup_db):
|
||||
assert 0 == dao.count_published_comments("")
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert 0 == dao.count_published_comments("")
|
||||
assert 1 == len(dao.find_not_published_comments())
|
||||
dao.publish_comment(c1)
|
||||
assert 1 == dao.count_published_comments("")
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
dao.publish_comment(c2)
|
||||
assert 2 == dao.count_published_comments("")
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
dao.publish_comment(c3)
|
||||
assert 0 == len(dao.find_not_published_comments())
|
||||
|
||||
# count published
|
||||
assert 1 == dao.count_published_comments("/post1")
|
||||
assert 2 == dao.count_published_comments("/post2")
|
||||
|
||||
# find published
|
||||
assert 0 == len(dao.find_published_comments_by_url("/"))
|
||||
assert 1 == len(dao.find_published_comments_by_url("/post1"))
|
||||
assert 2 == len(dao.find_published_comments_by_url("/post2"))
|
||||
|
||||
|
||||
def test_dao_notified(setup_db):
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert 1 == len(dao.find_not_notified_comments())
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
assert 2 == len(dao.find_not_notified_comments())
|
||||
dao.notify_comment(c1)
|
||||
dao.notify_comment(c2)
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
assert 1 == len(dao.find_not_notified_comments())
|
||||
dao.notify_comment(c3)
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
|
||||
|
||||
def create_comment(url, author_name, content):
|
||||
return dao.create_comment(url, author_name, "", "", content)
|
||||
|
||||
|
||||
def test_find_recent_published_comments(setup_db):
|
||||
|
||||
comments = []
|
||||
comments.append(create_comment("/post", "Adam", "Comment 1"))
|
||||
comments.append(create_comment("/post", "Arf", "Comment 2"))
|
||||
comments.append(create_comment("/post", "Arwin", "Comment 3"))
|
||||
comments.append(create_comment("/post", "Bill", "Comment 4"))
|
||||
comments.append(create_comment("/post", "Bo", "Comment 5"))
|
||||
comments.append(create_comment("/post", "Charles", "Comment 6"))
|
||||
comments.append(create_comment("/post", "Dan", "Comment 7"))
|
||||
comments.append(create_comment("/post", "Dwayne", "Comment 8"))
|
||||
comments.append(create_comment("/post", "Erl", "Comment 9"))
|
||||
comments.append(create_comment("/post", "Jay", "Comment 10"))
|
||||
comments.append(create_comment("/post", "Kenny", "Comment 11"))
|
||||
comments.append(create_comment("/post", "Lord", "Comment 12"))
|
||||
|
||||
rows = dao.find_recent_published_comments()
|
||||
assert len(rows) == 0
|
||||
|
||||
# publish every second
|
||||
for comment in comments:
|
||||
dao.publish_comment(comment)
|
||||
time.sleep(1)
|
||||
|
||||
rows = dao.find_recent_published_comments()
|
||||
assert len(rows) == 10
|
||||
|
||||
authors = [row.author_name for row in rows]
|
||||
assert authors == [
|
||||
"Lord",
|
||||
"Kenny",
|
||||
"Jay",
|
||||
"Erl",
|
||||
"Dwayne",
|
||||
"Dan",
|
||||
"Charles",
|
||||
"Bo",
|
||||
"Bill",
|
||||
"Arwin",
|
||||
]
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+17
-37
@@ -1,42 +1,22 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
import unittest
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import database
|
||||
from stacosys.interface import app, form
|
||||
from stacosys.interface import form
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
logger = logging.getLogger(__name__)
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
logger.info(f"start interface {form}")
|
||||
return app.test_client()
|
||||
class FormInterfaceTestCase(unittest.TestCase):
|
||||
|
||||
def test_check_form_data_ok(self):
|
||||
d = {"url": "/", "message": "", "site": "", "remarque": "", "author": "", "token": "", "email": ""}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
d = {"url": "/"}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
d = {}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
|
||||
def test_check_form_data_ko(self):
|
||||
d = {"url": "/", "message": "", "site": "", "remarque": "", "author": "", "token": "", "email": "", "bonus": ""}
|
||||
self.assertFalse(form.check_form_data(d))
|
||||
|
||||
|
||||
def test_new_comment_honeypot(client):
|
||||
resp = client.post(
|
||||
"/newcomment", content_type="multipart/form-data", data={"remarque": "trapped"}
|
||||
)
|
||||
assert resp.status == "400 BAD REQUEST"
|
||||
|
||||
|
||||
def test_new_comment_success(client):
|
||||
resp = client.post(
|
||||
"/newcomment",
|
||||
content_type="multipart/form-data",
|
||||
data={"author": "Jack", "url": "/site3", "message": "comment 3"},
|
||||
)
|
||||
assert resp.status == "302 FOUND"
|
||||
|
||||
|
||||
def test_check_form_data():
|
||||
from stacosys.interface.form import check_form_data
|
||||
|
||||
assert check_form_data({"author": "Jack", "url": "/site3", "message": "comment 3"})
|
||||
assert not check_form_data(
|
||||
{"author": "Jack", "url": "/site3", "message": "comment 3", "extra": "ball"}
|
||||
)
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.service import mailer
|
||||
|
||||
|
||||
def test_configure_and_check():
|
||||
mailer.configure_smtp("localhost", 2525, "admin", "admin")
|
||||
mailer.configure_destination("admin@mydomain.com")
|
||||
with pytest.raises(ConnectionRefusedError):
|
||||
mailer.check()
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from stacosys.service import rss
|
||||
|
||||
|
||||
def test_configure():
|
||||
rss.configure("comments.xml", "blog", "http", "blog.mydomain.com")
|
||||
@@ -0,0 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from stacosys import __version__
|
||||
|
||||
|
||||
class StacosysTestCase(unittest.TestCase):
|
||||
|
||||
def test_version(self):
|
||||
self.assertEqual("2.0", __version__)
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from stacosys.core.templater import Templater, Template
|
||||
|
||||
|
||||
class TemplateTestCase(unittest.TestCase):
|
||||
|
||||
def get_template_content(self, lang, template_name, **kwargs):
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../stacosys/templates"))
|
||||
template = Templater(template_path).get_template(lang, template_name)
|
||||
return template.render(kwargs)
|
||||
|
||||
def test_approve_comment(self):
|
||||
content = self.get_template_content("fr", Template.APPROVE_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nLe commentaire sera bientôt publié."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
content = self.get_template_content("en", Template.APPROVE_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nThe comment should be published soon."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
|
||||
def test_drop_comment(self):
|
||||
content = self.get_template_content("fr", Template.DROP_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nLe commentaire ne sera pas publié."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
content = self.get_template_content("en", Template.DROP_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nThe comment will not be published."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
|
||||
def test_new_comment(self):
|
||||
content = self.get_template_content("fr", Template.NEW_COMMENT, comment="[comment]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nUn nouveau commentaire a été posté"))
|
||||
self.assertTrue(content.endswith("[comment]\n\n--\nStacosys"))
|
||||
content = self.get_template_content("en", Template.NEW_COMMENT, comment="[comment]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nA new comment has been submitted"))
|
||||
self.assertTrue(content.endswith("[comment]\n\n--\nStacosys"))
|
||||
|
||||
def test_notify_message(self):
|
||||
content = self.get_template_content("fr", Template.NOTIFY_MESSAGE)
|
||||
self.assertEqual("Nouveau commentaire", content)
|
||||
content = self.get_template_content("en", Template.NOTIFY_MESSAGE)
|
||||
self.assertEqual("New comment", content)
|
||||
|
||||
def test_rss_title(self):
|
||||
content = self.get_template_content("fr", Template.RSS_TITLE_MESSAGE, site="[site]")
|
||||
self.assertEqual("[site] : commentaires", content)
|
||||
content = self.get_template_content("en", Template.RSS_TITLE_MESSAGE, site="[site]")
|
||||
self.assertEqual("[site] : comments", content)
|
||||
@@ -1 +0,0 @@
|
||||
RSS
|
||||
Reference in New Issue
Block a user