mirror of https://github.com/logos-co/open-law.git
initial commit
This commit is contained in:
parent
069a581bc9
commit
6147d924d7
|
@ -0,0 +1,22 @@
|
|||
**/__pycache__
|
||||
**/.pytest_cache
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
README.md
|
||||
TODO.md
|
||||
**/.venv
|
||||
/tests/
|
||||
/src/
|
||||
**/*.sqlite3
|
||||
**/package.json
|
||||
**/tox.ini
|
||||
**/yarn.lock
|
|
@ -0,0 +1,13 @@
|
|||
# These variables will be loaded into the python environment and used by flask to configure your app.
|
||||
|
||||
# This may be variables defined by flask, extensions or even your own environment variables.
|
||||
|
||||
# This requires that you have the python-dotenv package installed.
|
||||
|
||||
# NOTE: This file is placed under version control and should NOT include sensitive information such as passwords.
|
||||
|
||||
# For those cases, use the .env file.
|
||||
|
||||
FLASK_APP=wsgi:app
|
||||
APP_ENV=development
|
||||
FLASK_DEBUG=1
|
|
@ -0,0 +1,41 @@
|
|||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Simple Flask Application CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop, develop_tw]
|
||||
pull_request:
|
||||
branches: [main, develop, develop_tw]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: [3.11]
|
||||
poetry-version: [1.4]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Set up Poetry
|
||||
uses: abatilo/actions-poetry@v2.1.6
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
poetry run flake8 . --count --show-source --statistics
|
||||
poetry run flake8 . --count --exit-zero --max-complexity=10 --statistics
|
||||
- name: Run Flask tests
|
||||
run: |
|
||||
poetry run pytest
|
|
@ -1,129 +1,11 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
# ignore directories
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# ignore files
|
||||
.env
|
||||
*.pyc
|
||||
/*.sqlite3
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Flask",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "flask",
|
||||
"env": {
|
||||
"FLASK_APP": "wsgi:app",
|
||||
"APP_ENV": "development",
|
||||
"FLASK_DEBUG": "1"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
"--port",
|
||||
"5006"
|
||||
],
|
||||
"jinja": true,
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Docker: Python - Flask",
|
||||
"type": "docker",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "docker-run: debug",
|
||||
"python": {
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/app"
|
||||
}
|
||||
],
|
||||
"projectType": "flask"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/*/**": true,
|
||||
"**/.venv/*/**": true
|
||||
},
|
||||
"cSpell.words": [
|
||||
"flowbite",
|
||||
"jsonify",
|
||||
"pydantic",
|
||||
"pytest",
|
||||
"sqlalchemy",
|
||||
"tailwindcss",
|
||||
"werkzeug",
|
||||
"wrongpassword",
|
||||
"wsgi",
|
||||
"wtforms"
|
||||
],
|
||||
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "*test.py"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestArgs": ["tests"]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "docker-build",
|
||||
"label": "docker-build",
|
||||
"platform": "python",
|
||||
"dockerBuild": {
|
||||
"tag": "flaskapp:latest",
|
||||
"dockerfile": "${workspaceFolder}/Dockerfile",
|
||||
"context": "${workspaceFolder}",
|
||||
"pull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: debug",
|
||||
"dependsOn": [
|
||||
"docker-build"
|
||||
],
|
||||
"dockerRun": {
|
||||
"env": {
|
||||
"FLASK_APP": "wsgi.py"
|
||||
}
|
||||
},
|
||||
"python": {
|
||||
"args": [
|
||||
"run",
|
||||
"--no-debugger",
|
||||
"--no-reload",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"5000"
|
||||
],
|
||||
"module": "flask"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
FROM python:3.11-alpine
|
||||
|
||||
# install build utils and dependencies
|
||||
RUN apk update && apk upgrade
|
||||
RUN apk add --no-cache pkgconfig \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libffi-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
|
||||
# Add user app
|
||||
RUN python -m pip install -U pip
|
||||
RUN adduser -D app
|
||||
USER app
|
||||
WORKDIR /home/app
|
||||
|
||||
# set environment varibles
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONHASHSEED random
|
||||
ENV PIP_NO_CACHE_DIR off
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK on
|
||||
|
||||
# install poetry
|
||||
RUN pip install --user poetry
|
||||
ENV PATH="/home/app/.local/bin:${PATH}"
|
||||
|
||||
# install app dependencies
|
||||
COPY --chown=app:app poetry.lock .
|
||||
COPY --chown=app:app pyproject.toml .
|
||||
COPY --chown=app:app poetry.toml .
|
||||
|
||||
RUN poetry install --no-dev --no-interaction --no-ansi
|
||||
# add gunicorn
|
||||
RUN poetry add gunicorn
|
||||
|
||||
COPY --chown=app:app . .
|
|
@ -0,0 +1,64 @@
|
|||
import os
|
||||
|
||||
from flask import Flask, render_template
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from app.logger import log
|
||||
|
||||
# instantiate extensions
|
||||
login_manager = LoginManager()
|
||||
db = SQLAlchemy()
|
||||
migration = Migrate()
|
||||
|
||||
|
||||
def create_app(environment="development"):
|
||||
|
||||
from config import config
|
||||
from app.views import (
|
||||
main_blueprint,
|
||||
auth_blueprint,
|
||||
user_blueprint,
|
||||
)
|
||||
from app.models import (
|
||||
User,
|
||||
AnonymousUser,
|
||||
)
|
||||
|
||||
# Instantiate app.
|
||||
app = Flask(__name__)
|
||||
|
||||
# Set app config.
|
||||
env = os.environ.get("APP_ENV", environment)
|
||||
configuration = config(env)
|
||||
app.config.from_object(configuration)
|
||||
configuration.configure(app)
|
||||
log(log.INFO, "Configuration: [%s]", configuration.ENV)
|
||||
|
||||
# Set up extensions.
|
||||
db.init_app(app)
|
||||
migration.init_app(app, db)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Register blueprints.
|
||||
app.register_blueprint(auth_blueprint)
|
||||
app.register_blueprint(main_blueprint)
|
||||
app.register_blueprint(user_blueprint)
|
||||
|
||||
# Set up flask login.
|
||||
@login_manager.user_loader
|
||||
def get_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
||||
login_manager.login_view = "auth.login"
|
||||
login_manager.login_message_category = "info"
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
# Error handlers.
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_http_error(exc):
|
||||
return render_template("error.html", error=exc), exc.code
|
||||
|
||||
return app
|
|
@ -0,0 +1,38 @@
|
|||
import click
|
||||
from flask import Flask
|
||||
from app import models as m
|
||||
from app import db, forms
|
||||
from app import schema as s
|
||||
|
||||
|
||||
def init(app: Flask):
|
||||
|
||||
# flask cli context setup
|
||||
@app.shell_context_processor
|
||||
def get_context():
|
||||
"""Objects exposed here will be automatically available from the shell."""
|
||||
return dict(app=app, db=db, m=m, f=forms, s=s)
|
||||
|
||||
if app.config["ENV"] != "production":
|
||||
|
||||
@app.cli.command()
|
||||
@click.option("--count", default=100, type=int)
|
||||
def db_populate(count: int):
|
||||
"""Fill DB by dummy data."""
|
||||
from tests.db import populate
|
||||
|
||||
populate(count)
|
||||
print(f"DB populated by {count} instancies")
|
||||
|
||||
@app.cli.command("create-admin")
|
||||
def create_admin():
|
||||
"""Create super admin account"""
|
||||
if m.User.query.filter_by(email=app.config["ADMIN_EMAIL"]).first():
|
||||
print(f"User with e-mail: [{app.config['ADMIN_EMAIL']}] already exists")
|
||||
return
|
||||
m.User(
|
||||
username=app.config["ADMIN_USERNAME"],
|
||||
email=app.config["ADMIN_EMAIL"],
|
||||
password=app.config["ADMIN_PASSWORD"],
|
||||
).save()
|
||||
print("admin created")
|
|
@ -0,0 +1,2 @@
|
|||
# flake8: noqa F401
|
||||
from .pagination import create_pagination
|
|
@ -0,0 +1,36 @@
|
|||
from flask import request
|
||||
from flask import current_app as app
|
||||
from app import schema as s
|
||||
|
||||
|
||||
def create_pagination(total: int, page_size: int = 0) -> s.Pagination:
|
||||
"""create instance Pagination for current request"""
|
||||
q = request.args.get("q", type=str, default="")
|
||||
page = request.args.get("page", type=int, default=1)
|
||||
page_size = page_size or app.config["DEFAULT_PAGE_SIZE"]
|
||||
PAGE_LINKS_NUMBER: int = app.config["PAGE_LINKS_NUMBER"]
|
||||
|
||||
pages = (total // page_size + 1) if (total % page_size) else (total // page_size)
|
||||
|
||||
pages_for_links: list[int] = []
|
||||
if pages > PAGE_LINKS_NUMBER:
|
||||
if page <= PAGE_LINKS_NUMBER // 2:
|
||||
pages_for_links = [n + 1 for n in range(PAGE_LINKS_NUMBER)]
|
||||
elif pages - page <= PAGE_LINKS_NUMBER // 2:
|
||||
pages_for_links = [n + 1 for n in range(pages - PAGE_LINKS_NUMBER, pages)]
|
||||
else:
|
||||
pages_for_links = list(
|
||||
range(page - PAGE_LINKS_NUMBER // 2, page + PAGE_LINKS_NUMBER // 2)
|
||||
)
|
||||
else:
|
||||
pages_for_links = [n + 1 for n in range(pages)]
|
||||
|
||||
return s.Pagination(
|
||||
page=page,
|
||||
pages=pages,
|
||||
total=total,
|
||||
query=q,
|
||||
per_page=page_size,
|
||||
skip=(page - 1) * page_size,
|
||||
pages_for_links=pages_for_links,
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# flake8: noqa F401
|
||||
from .auth import LoginForm
|
||||
from .user import UserForm, NewUserForm
|
|
@ -0,0 +1,9 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
user_id = StringField("Username", [DataRequired()])
|
||||
password = PasswordField("Password", [DataRequired()])
|
||||
submit = SubmitField("Login")
|
|
@ -0,0 +1,69 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
PasswordField,
|
||||
SubmitField,
|
||||
ValidationError,
|
||||
BooleanField,
|
||||
)
|
||||
from wtforms.validators import DataRequired, Email, Length, EqualTo
|
||||
|
||||
from app import models as m
|
||||
|
||||
|
||||
class UserForm(FlaskForm):
|
||||
next_url = StringField("next_url")
|
||||
user_id = StringField("user_id", [DataRequired()])
|
||||
email = StringField("email", [DataRequired(), Email()])
|
||||
activated = BooleanField("activated")
|
||||
username = StringField("Username", [DataRequired()])
|
||||
password = PasswordField("Password", validators=[DataRequired(), Length(6, 30)])
|
||||
password_confirmation = PasswordField(
|
||||
"Confirm Password",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
EqualTo("password", message="Password do not match."),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
def validate_username(self, field):
|
||||
if (
|
||||
m.User.query.filter_by(username=field.data)
|
||||
.filter(m.User.id != int(self.user_id.data))
|
||||
.first()
|
||||
is not None
|
||||
):
|
||||
raise ValidationError("This username is taken.")
|
||||
|
||||
def validate_email(self, field):
|
||||
if (
|
||||
m.User.query.filter_by(email=field.data)
|
||||
.filter(m.User.id != int(self.user_id.data))
|
||||
.first()
|
||||
is not None
|
||||
):
|
||||
raise ValidationError("This email is already registered.")
|
||||
|
||||
|
||||
class NewUserForm(FlaskForm):
|
||||
email = StringField("email", [DataRequired(), Email()])
|
||||
activated = BooleanField("activated")
|
||||
username = StringField("Username", [DataRequired()])
|
||||
password = PasswordField("Password", validators=[DataRequired(), Length(6, 30)])
|
||||
password_confirmation = PasswordField(
|
||||
"Confirm Password",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
EqualTo("password", message="Password do not match."),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
def validate_username(self, field):
|
||||
if m.User.query.filter_by(username=field.data).first() is not None:
|
||||
raise ValidationError("This username is taken.")
|
||||
|
||||
def validate_email(self, field):
|
||||
if m.User.query.filter_by(email=field.data).first() is not None:
|
||||
raise ValidationError("This email is already registered.")
|
|
@ -0,0 +1,49 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
LOGGER_NAME = "SimpleAppLog"
|
||||
|
||||
|
||||
class Logger(object):
|
||||
# __created_std_out = False
|
||||
log_file = bytes
|
||||
EXCEPTION = 100
|
||||
CRITICAL = 50
|
||||
ERROR = 40
|
||||
WARNING = 30
|
||||
INFO = 20
|
||||
DEBUG = 10
|
||||
NOTSET = 0
|
||||
|
||||
def __init__(self):
|
||||
self.__log = logging.getLogger(LOGGER_NAME)
|
||||
# create formatter
|
||||
formatter = logging.Formatter("%(asctime)-15s [%(levelname)-8s] %(message)s")
|
||||
ch = logging.StreamHandler(sys.stderr)
|
||||
ch.setLevel(logging.DEBUG)
|
||||
ch.setFormatter(formatter)
|
||||
self.__log.addHandler(ch)
|
||||
|
||||
self.__log.setLevel(self.INFO)
|
||||
self.__methods_map = {
|
||||
self.DEBUG: self.__log.debug,
|
||||
self.INFO: self.__log.info,
|
||||
self.WARNING: self.__log.warning,
|
||||
self.ERROR: self.__log.error,
|
||||
self.CRITICAL: self.__log.critical,
|
||||
self.EXCEPTION: self.__log.exception,
|
||||
}
|
||||
|
||||
def __call__(self, lvl, msg, *args, **kwargs):
|
||||
if lvl in self.__methods_map:
|
||||
self.__methods_map[lvl](msg, *args, **kwargs)
|
||||
else:
|
||||
self.__log.log(lvl, msg, *args, **kwargs)
|
||||
|
||||
def set_level(self, level=None):
|
||||
if level is None:
|
||||
level = self.INFO
|
||||
self.__log.setLevel(level)
|
||||
|
||||
|
||||
log = Logger()
|
|
@ -0,0 +1,2 @@
|
|||
# flake8: noqa F401
|
||||
from .user import User, AnonymousUser, gen_password_reset_id
|
|
@ -0,0 +1,69 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from app import db
|
||||
from app.models.utils import ModelMixin
|
||||
from app.logger import log
|
||||
from app import schema as s
|
||||
|
||||
|
||||
def gen_password_reset_id() -> str:
|
||||
return str(uuid4())
|
||||
|
||||
|
||||
class User(db.Model, UserMixin, ModelMixin):
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(60), unique=True, nullable=False)
|
||||
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255), default="")
|
||||
activated = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
unique_id = db.Column(db.String(36), default=gen_password_reset_id)
|
||||
reset_password_uid = db.Column(db.String(64), default=gen_password_reset_id)
|
||||
|
||||
@hybrid_property
|
||||
def password(self):
|
||||
return self.password_hash
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
@classmethod
|
||||
def authenticate(cls, user_id, password):
|
||||
user = cls.query.filter(
|
||||
db.or_(
|
||||
func.lower(cls.username) == func.lower(user_id),
|
||||
func.lower(cls.email) == func.lower(user_id),
|
||||
)
|
||||
).first()
|
||||
if not user:
|
||||
log(log.WARNING, "user:[%s] not found", user_id)
|
||||
|
||||
if user is not None and check_password_hash(user.password, password):
|
||||
return user
|
||||
|
||||
def reset_password(self):
|
||||
self.password_hash = ""
|
||||
self.reset_password_uid = gen_password_reset_id()
|
||||
self.save()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.id}: {self.username},{self.email}>"
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
u = s.User.from_orm(self)
|
||||
return u.json()
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
pass
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
|
||||
|
||||
class ModelMixin(object):
|
||||
def save(self, commit=True):
|
||||
# Save this model to the database.
|
||||
db.session.add(self)
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return self
|
||||
|
||||
|
||||
# Add your own utility classes and functions here.
|
|
@ -0,0 +1,3 @@
|
|||
# flake8: noqa F401
|
||||
from .pagination import Pagination
|
||||
from .user import User
|
|
@ -0,0 +1,13 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Pagination(BaseModel):
|
||||
"""Pagination data"""
|
||||
|
||||
page: int # current page number
|
||||
total: int # total items
|
||||
query: str | None # query string
|
||||
per_page: int # number items on the page
|
||||
skip: int # number items on all previous pages
|
||||
pages: int # total pages
|
||||
pages_for_links: list[int] # number of links
|
|
@ -0,0 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
|
@ -0,0 +1,65 @@
|
|||
body {
|
||||
padding-top: 50px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.page-subnav,
|
||||
.pagination-nav {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: #EEE;
|
||||
background-repeat: repeat-x;
|
||||
background-image: -moz-linear-gradient(top, whiteSmoke 0%, #EEE 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, whiteSmoke), color-stop(100%, #EEE));
|
||||
background-image: -webkit-linear-gradient(top, whiteSmoke 0%, #EEE 100%);
|
||||
background-image: -ms-linear-gradient(top, whiteSmoke 0%, #EEE 100%);
|
||||
background-image: -o-linear-gradient(top, whiteSmoke 0%, #EEE 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#eeeeee', GradientType=0);
|
||||
background-image: linear-gradient(top, whiteSmoke 0%, #EEE 100%);
|
||||
border: 1px solid #E5E5E5;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination-fixed {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1020;
|
||||
border-color: #D5D5D5;
|
||||
border-width: 0 0 1px;
|
||||
-webkit-border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: inset 0 1px 0 white, 0 1px 5px rgba(0, 0, 0, .1);
|
||||
-moz-box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0, 0, 0, .1);
|
||||
box-shadow: inset 0 1px 0 white, 0 1px 5px rgba(0, 0, 0, .1);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 10px 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pagination-page-info {
|
||||
padding: .6em;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
width: 40em;
|
||||
margin: .5em;
|
||||
margin-left: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-page-info b {
|
||||
color: black;
|
||||
background: #6aa6ed;
|
||||
padding-left: 2px;
|
||||
padding: .1em .25em;
|
||||
font-size: 150%;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
import './styles.css';
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
export declare function initUsers(): void;
|
|
@ -0,0 +1 @@
|
|||
<!-- Put your reusable template code into macros here -->
|
|
@ -0,0 +1,53 @@
|
|||
{% extends "base.html" %}
|
||||
<!-- prettier-ignore -->
|
||||
{% block body %}
|
||||
|
||||
<!-- component -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 h-screen pt-20">
|
||||
<section>
|
||||
<div class="w-full lg:w-4/12 px-4 mx-auto pt-6">
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="w-full lg:max-w-xl p-6 space-y-8 sm:p-8 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||
<!-- prettier-ignore -->
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Sign in to {{ config.APP_NAME }}</h2>
|
||||
<!-- prettier-ignore -->
|
||||
<form class="mt-8 space-y-6 from" role="form" action="{{ url_for('auth.login') }}" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
{{form.user_id.label(class='block mb-2 text-sm font-medium text-gray-900 dark:text-white')}}
|
||||
{{form.user_id(class='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500')}}
|
||||
</div>
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
{{form.password.label(class='block mb-2 text-sm font-medium text-gray-900 dark:text-white')}}
|
||||
{{form.password(class='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500')}}
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<!-- prettier-ignore -->
|
||||
<input id="remember" aria-describedby="remember" name="remember" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" required/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<!-- prettier-ignore -->
|
||||
<label for="remember" class="font-medium text-gray-500 dark:text-gray-400">Remember this device</label>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('auth.forgot_pass') }}" class="ml-auto text-sm font-medium text-blue-600 hover:underline dark:text-blue-500">Lost Password?</>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 sm:w-auto dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Login to your account</button>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">Not registered yet?<a class="text-blue-600 hover:underline dark:text-blue-500" href="{{ url_for('auth.register') }}" >Create account</a></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<!-- prettier-ignore -->
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{ config.APP_NAME }}</title>
|
||||
<!-- meta -->
|
||||
<meta name="description" content="Simple2B Flask App" />
|
||||
<meta name="author" content="Simple2B" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
<!-- styles -->
|
||||
<!-- prettier-ignore -->
|
||||
<!-- <link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet"/> -->
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
if (
|
||||
localStorage.getItem("color-theme") === "dark" ||
|
||||
(!("color-theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
<!-- prettier-ignore -->
|
||||
{% block links %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-white dark:bg-gray-800">
|
||||
<!-- Header -->
|
||||
<!-- prettier-ignore -->
|
||||
{% include 'header.html' %}
|
||||
<!-- Flash Messages -->
|
||||
<!-- prettier-ignore -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<!-- Flash message markup -->
|
||||
<!-- prettier-ignore -->
|
||||
<div id="toast-{{category}}" class="absolute top-10 left-10 z-50 flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"role="alert">
|
||||
<!-- prettier-ignore -->
|
||||
{% if category == 'success' %}
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
|
||||
<span class="sr-only">Check icon</span>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% elif category == 'danger' %}
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
<span class="sr-only">Error icon</span>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% endif %}
|
||||
<div class="ml-3 text-sm font-normal">{{message}}</div>
|
||||
<!-- prettier-ignore -->
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#toast-{{category}}" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<!-- prettier-ignore -->
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<!-- SideBar -->
|
||||
<!-- prettier-ignore -->
|
||||
{% include 'sidebar.html' %}
|
||||
|
||||
<div class="sm:ml-64 mt-14 h-full overflow-x-scroll">
|
||||
<!-- Main Content -->
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
<!-- scripts -->
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<script src="{{ url_for('static', filename='js/flowbite.min.js') }}" defer></script>
|
||||
<!-- prettier-ignore -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}" type="text/javascript" defer></script>
|
||||
<!-- prettier-ignore -->
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
{% block content %}
|
||||
<div class="jumbotron my-4">
|
||||
<div class="text-center">
|
||||
<!-- prettier-ignore -->
|
||||
<h1>{{ '{} - {}'.format(error.code, error.name) }}</h1>
|
||||
<p>{{ error.description }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% endblock %}
|
|
@ -0,0 +1,358 @@
|
|||
<nav
|
||||
class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start">
|
||||
<button
|
||||
data-drawer-target="logo-sidebar"
|
||||
data-drawer-toggle="logo-sidebar"
|
||||
aria-controls="logo-sidebar"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<a href="https://simple2b.com/" class="flex ml-2 md:mr-24">
|
||||
<img
|
||||
src="{{url_for('static',filename='img/logo.svg')}}"
|
||||
class="h-8 mr-3"
|
||||
alt="Simple2b Logo" />
|
||||
<span
|
||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white"
|
||||
>Simple2B</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 mr-2 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg
|
||||
id="theme-toggle-dark-icon"
|
||||
class="hidden w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
<svg
|
||||
id="theme-toggle-light-icon"
|
||||
class="hidden w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="flex items-center ml-3">
|
||||
<button
|
||||
id="dropdownNotificationButton"
|
||||
data-dropdown-toggle="dropdownNotification"
|
||||
class="inline-flex items-center text-sm font-medium text-center text-gray-500 hover:text-gray-900 focus:outline-none dark:hover:text-white dark:text-gray-400"
|
||||
type="button">
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"></path>
|
||||
</svg>
|
||||
<div class="relative flex">
|
||||
<div
|
||||
class="relative inline-flex w-3 h-3 bg-red-500 border-2 border-white rounded-full -top-2 right-3 dark:border-gray-900"></div>
|
||||
</div>
|
||||
</button>
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
id="dropdownNotification"
|
||||
class="z-20 hidden w-full max-w-sm bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-800 dark:divide-gray-700"
|
||||
aria-labelledby="dropdownNotificationButton">
|
||||
<div
|
||||
class="block px-4 py-2 font-medium text-center text-gray-700 rounded-t-lg bg-gray-50 dark:bg-gray-800 dark:text-white">
|
||||
Notifications
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
class="rounded-full w-11 h-11"
|
||||
src="/docs/images/people/profile-picture-1.jpg"
|
||||
alt="Jese image" />
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-blue-600 border border-white rounded-full dark:border-gray-800">
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l2-2a1 1 0 00-1.414-1.414L11 7.586V3a1 1 0 10-2 0v4.586l-.293-.293z"></path>
|
||||
<path
|
||||
d="M3 5a2 2 0 012-2h1a1 1 0 010 2H5v7h2l1 2h4l1-2h2V5h-1a1 1 0 110-2h1a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-3">
|
||||
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
|
||||
New message from
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>Jese Leos</span
|
||||
>: "Hey, what's up? All set for the presentation?"
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-500">
|
||||
a few moments ago
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
class="rounded-full w-11 h-11"
|
||||
src="/docs/images/people/profile-picture-2.jpg"
|
||||
alt="Joseph image" />
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-gray-900 border border-white rounded-full dark:border-gray-800">
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-3">
|
||||
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>Joseph Mcfall</span
|
||||
>
|
||||
and
|
||||
<span class="font-medium text-gray-900 dark:text-white"
|
||||
>5 others</span
|
||||
>
|
||||
started following you.
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-500">
|
||||
10 minutes ago
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
class="rounded-full w-11 h-11"
|
||||
src="/docs/images/people/profile-picture-3.jpg"
|
||||
alt="Bonnie image" />
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-red-600 border border-white rounded-full dark:border-gray-800">
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-3">
|
||||
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>Bonnie Green</span
|
||||
>
|
||||
and
|
||||
<span class="font-medium text-gray-900 dark:text-white"
|
||||
>141 others</span
|
||||
>
|
||||
love your story. See it and view more stories.
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-500">
|
||||
44 minutes ago
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
class="rounded-full w-11 h-11"
|
||||
src="/docs/images/people/profile-picture-4.jpg"
|
||||
alt="Leslie image" />
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-green-400 border border-white rounded-full dark:border-gray-800">
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 13V5a2 2 0 00-2-2H4a2 2 0 00-2 2v8a2 2 0 002 2h3l3 3 3-3h3a2 2 0 002-2zM5 7a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1zm1 3a1 1 0 100 2h3a1 1 0 100-2H6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-3">
|
||||
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>Leslie Livingston</span
|
||||
>
|
||||
mentioned you in a comment:
|
||||
<span class="font-medium text-blue-500" href="#"
|
||||
>@bonnie.green</span
|
||||
>
|
||||
what do you say?
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-500">
|
||||
1 hour ago
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
class="rounded-full w-11 h-11"
|
||||
src="/docs/images/people/profile-picture-5.jpg"
|
||||
alt="Robert image" />
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-purple-500 border border-white rounded-full dark:border-gray-800">
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-3">
|
||||
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>Robert Brown</span
|
||||
>
|
||||
posted a new video: Glassmorphism - learn how to implement
|
||||
the new design trend.
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-500">
|
||||
3 hours ago
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 text-sm font-medium text-center text-gray-900 rounded-b-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-white">
|
||||
<div class="inline-flex items-center">
|
||||
<svg
|
||||
class="w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
View all
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 mr-2 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5"
|
||||
aria-expanded="false"
|
||||
data-dropdown-toggle="dropdown-user">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<!-- prettier-ignore -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
{{current_user.username}}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm font-medium text-gray-900 truncate dark:text-gray-300"
|
||||
role="none">
|
||||
{{current_user.email}}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem"
|
||||
>Dashboard</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem"
|
||||
data-modal-target="editCurrentUserModal"
|
||||
data-modal-show="editCurrentUserModal"
|
||||
>Settings</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="{{ url_for('auth.logout') }}"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem"
|
||||
>Sign out</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% include 'user/edit_current.html' %}
|
|
@ -0,0 +1,4 @@
|
|||
{% extends "base.html" %} {% block content %}
|
||||
<!-- component -->
|
||||
|
||||
{% endblock %} {% block scripts %} {% endblock %}
|
|
@ -0,0 +1,90 @@
|
|||
<aside
|
||||
id="logo-sidebar"
|
||||
class="fixed top-0 left-0 z-40 w-64 h-screen pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
|
||||
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
|
||||
</svg>
|
||||
<span class="ml-3">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="{{ url_for('user.get_all') }}"
|
||||
class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="flex-1 ml-3 whitespace-nowrap">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/Simple2B/flask.app"
|
||||
class="flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 496 512"
|
||||
>
|
||||
<path
|
||||
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="flex-1 ml-3 whitespace-nowrap">GitHub Repo</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="{{ url_for('auth.logout') }}"
|
||||
class="absolute bottom-3 left-3 flex items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="flex-shrink-0 w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="flex-1 ml-3 whitespace-nowrap">Sign Out</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
|
@ -0,0 +1,43 @@
|
|||
<!-- Adding user modal -->
|
||||
<!-- prettier-ignore-->
|
||||
<div id="add-user-modal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 items-center justify-center hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div class="relative w-full max-w-2xl max-h-full">
|
||||
<!-- Modal content -->
|
||||
<form action="{{url_for('user.create')}}" method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<!-- Modal header -->
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white"> Add new user </h3>
|
||||
<button id="modalAddCloseButton" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"> <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Name</label >
|
||||
<input type="text" name="username" id="user-add-username" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Name" required />
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Email</label >
|
||||
<input type="email" name="email" id="user-add-email" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="example@company.com" required />
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Password</label >
|
||||
<input type="password" name="password" id="user-add-password" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<label for="password_confirmation" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Confirm Password</label >
|
||||
<input type="password" name="password_confirmation" id="user-add-password_confirmation" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label for="activated" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Activated</label >
|
||||
<input type="checkbox" name="activated" id="user-add-activated" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal footer -->
|
||||
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<button name="submit" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> Save new user </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,147 @@
|
|||
<!-- prettier-ignore -->
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<div class="flex items-center justify-center py-4 bg-white dark:bg-gray-800">
|
||||
<label for="table-search" class="sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
|
||||
>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path></svg>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<input {% if search_query %}value={{ search_query }}{% endif %} type="text" id="table-search-users" class="block p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search for users"/>
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<button type="button"id="table-search-user-button" class="px-3 py-2 text-xs text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 ml-2 font-medium rounded-lg text-center inline-flex items-center mr-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"><svg class="w-5 h-5 text-white-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 ml-0 mr-1"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 15.75l-2.489-2.489m0 0a3.375 3.375 0 10-4.773-4.773 3.375 3.375 0 004.774 4.774zM21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>Search</button>
|
||||
<div class="flex items-center ml-auto space-x-2 sm:space-x-3">
|
||||
<button type="button" data-modal-toggle="add-user-modal" class="inline-flex items-center justify-center w-1/2 px-3 py-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 sm:w-auto dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"></path></svg>
|
||||
Add user
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">#</th> <th scope="col" class="px-6 py-3">Name</th> <th scope="col" class="px-6 py-3">Active</th> <th scope="col" class="px-6 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr
|
||||
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
>
|
||||
<td class="w-4 p-4">
|
||||
<div class="flex items-center">
|
||||
{{ loop.index + page.skip }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
scope="row"
|
||||
class="flex items-center px-6 py-4 text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<div class="pl-3">
|
||||
<div class="text-base font-semibold">{{ user.username }}</div>
|
||||
<div class="font-normal text-gray-500"><a class="hover:text-blue-500" href="mailto:{{ user.email }}">{{ user.email }}</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-4 text-base font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<div class="flex items-center">
|
||||
<!-- prettier-ignore -->
|
||||
{% if user.activated %}<div class="flex items-center"><div class="h-2.5 w-2.5 rounded-full bg-green-400 mr-2"></div> Active</div></div>{% else %}<div class="flex items-center"><div class="h-2.5 w-2.5 rounded-full bg-red-500 mr-2"></div> Offline</div>{% endif %}
|
||||
</td>
|
||||
<td class="p-4 space-x-2 whitespace-nowrap">
|
||||
<button type="button" data-target="{{user.json}}" class="user-edit-button inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg>
|
||||
Edit user
|
||||
</button>
|
||||
<button data-user-id={{ user.id }} type="button" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-red-600 rounded-lg hover:bg-red-800 focus:ring-4 focus:ring-red-300 dark:focus:ring-red-900 delete-user-btn">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
||||
Delete user
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if page.pages > 1 %}
|
||||
<div class="container content-center mt-3 flex bg-white dark:bg-gray-800">
|
||||
<nav aria-label="Page navigation example" class="mx-auto">
|
||||
<ul class="inline-flex items-center -space-x-px">
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page=1&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
<span class="sr-only">First</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page={{page.page-1 if page.page > 1 else 1}}&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
{% for p in page.pages_for_links %}
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
{% if p == page.page %}
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page={{p}}&q={{page.query}}" aria-current="page" class="z-10 px-3 py-2 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">{{p}}</a>
|
||||
{% else %}
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page={{p}}&q={{page.query}}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">{{p}}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page={{page.page+1 if page.page < page.pages else page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
<!-- prettier-ignore -->
|
||||
<span class="sr-only">Next</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<a href="{{ url_for('user.get_all') }}?page={{page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
|
||||
<!-- prettier-ignore -->
|
||||
<span class="sr-only">Last</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01.02-1.06L14.168 10 10.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'user/edit.html' %}
|
||||
{% include 'user/add.html' %}
|
||||
<!-- prettier-ignore -->
|
||||
{% endblock %}
|
||||
<!-- prettier-ignore -->
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# flake8: noqa F401
|
||||
from .auth import auth_blueprint
|
||||
from .main import main_blueprint
|
||||
from .user import bp as user_blueprint
|
|
@ -0,0 +1,36 @@
|
|||
from flask import Blueprint, render_template, url_for, redirect, flash, request, session
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
|
||||
from app import models as m
|
||||
from app import forms as f
|
||||
from app.logger import log
|
||||
|
||||
|
||||
auth_blueprint = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
@auth_blueprint.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
form = f.LoginForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
user = m.User.authenticate(form.user_id.data, form.password.data)
|
||||
log(log.INFO, "Form submitted. User: [%s]", user)
|
||||
if user:
|
||||
login_user(user)
|
||||
log(log.INFO, "Login successful.")
|
||||
flash("Login successful.", "success")
|
||||
return redirect(url_for("main.index"))
|
||||
flash("Wrong user ID or password.", "danger")
|
||||
|
||||
elif form.is_submitted():
|
||||
log(log.WARNING, "Form submitted error: [%s]", form.errors)
|
||||
return render_template("auth/login.html", form=form)
|
||||
|
||||
|
||||
@auth_blueprint.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
log(log.INFO, "You were logged out.")
|
||||
session.clear()
|
||||
return redirect(url_for("auth.login"))
|
|
@ -0,0 +1,11 @@
|
|||
from flask import render_template, Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
main_blueprint = Blueprint("main", __name__)
|
||||
|
||||
|
||||
@main_blueprint.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
return render_template("index.html")
|
|
@ -0,0 +1,93 @@
|
|||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
request,
|
||||
flash,
|
||||
redirect,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import login_required
|
||||
from app.controllers import create_pagination
|
||||
|
||||
from app import models as m, db
|
||||
from app import forms as f
|
||||
from app.logger import log
|
||||
|
||||
|
||||
bp = Blueprint("user", __name__, url_prefix="/user")
|
||||
|
||||
|
||||
@bp.route("/", methods=["GET"])
|
||||
@login_required
|
||||
def get_all():
|
||||
q = request.args.get("q", type=str, default=None)
|
||||
users = m.User.query.order_by(m.User.id)
|
||||
if q:
|
||||
users = users.filter(m.User.username.like(f"{q}%") | m.User.email.like(f"{q}%"))
|
||||
|
||||
pagination = create_pagination(total=users.count())
|
||||
|
||||
return render_template(
|
||||
"user/users.html",
|
||||
users=users.paginate(page=pagination.page, per_page=pagination.per_page),
|
||||
page=pagination,
|
||||
search_query=q,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/save", methods=["POST"])
|
||||
@login_required
|
||||
def save():
|
||||
form = f.UserForm()
|
||||
if form.validate_on_submit():
|
||||
u: m.User = m.User.query.get(int(form.user_id.data))
|
||||
if not u:
|
||||
log(log.ERROR, "Not found user by id : [%s]", form.user_id.data)
|
||||
flash("Cannot save user data", "danger")
|
||||
u.username = form.username.data
|
||||
u.email = form.email.data
|
||||
u.activated = form.activated.data
|
||||
if form.password.data.strip("*\n "):
|
||||
u.password = form.password.data
|
||||
u.save()
|
||||
if form.next_url.data:
|
||||
return redirect(form.next_url.data)
|
||||
return redirect(url_for("user.get_all"))
|
||||
|
||||
else:
|
||||
log(log.ERROR, "User save errors: [%s]", form.errors)
|
||||
flash(f"{form.errors}", "danger")
|
||||
return redirect(url_for("user.get_all"))
|
||||
|
||||
|
||||
@bp.route("/create", methods=["POST"])
|
||||
@login_required
|
||||
def create():
|
||||
form = f.NewUserForm()
|
||||
if form.validate_on_submit():
|
||||
user = m.User(
|
||||
username=form.username.data,
|
||||
email=form.email.data,
|
||||
password=form.password.data,
|
||||
activated=form.activated.data,
|
||||
)
|
||||
log(log.INFO, "Form submitted. User: [%s]", user)
|
||||
flash("User added!", "success")
|
||||
user.save()
|
||||
return redirect(url_for("user.get_all"))
|
||||
|
||||
|
||||
@bp.route("/delete/<id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def delete(id):
|
||||
u = m.User.query.filter_by(id=id).first()
|
||||
if not u:
|
||||
log(log.INFO, "There is no user with id: [%s]", id)
|
||||
flash("There is no such user", "danger")
|
||||
return "no user", 404
|
||||
|
||||
db.session.delete(u)
|
||||
db.session.commit()
|
||||
log(log.INFO, "User deleted. User: [%s]", u)
|
||||
flash("User deleted!", "success")
|
||||
return "ok", 200
|
|
@ -0,0 +1,105 @@
|
|||
import os
|
||||
from functools import lru_cache
|
||||
from pydantic import BaseSettings
|
||||
from flask import Flask
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
APP_ENV = os.environ.get("APP_ENV", "development")
|
||||
|
||||
|
||||
class BaseConfig(BaseSettings):
|
||||
"""Base configuration."""
|
||||
|
||||
ENV: str = "base"
|
||||
APP_NAME: str = "Simple Flask App"
|
||||
SECRET_KEY: str
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
|
||||
WTF_CSRF_ENABLED: bool = False
|
||||
|
||||
# Mail config
|
||||
MAIL_SERVER: str
|
||||
MAIL_PORT: int
|
||||
MAIL_USE_TLS: bool
|
||||
MAIL_USE_SSL: bool
|
||||
MAIL_USERNAME: str
|
||||
MAIL_PASSWORD: str
|
||||
MAIL_DEFAULT_SENDER: str
|
||||
|
||||
# Super admin
|
||||
ADMIN_USERNAME: str
|
||||
ADMIN_EMAIL: str
|
||||
ADMIN_PASSWORD: str
|
||||
|
||||
# Pagination
|
||||
DEFAULT_PAGE_SIZE: int
|
||||
PAGE_LINKS_NUMBER: int
|
||||
|
||||
@staticmethod
|
||||
def configure(app: Flask):
|
||||
# Implement this method to do further configuration on your app.
|
||||
pass
|
||||
|
||||
class Config:
|
||||
# `.env` takes priority over `project.env`
|
||||
env_file = "project.env", ".env"
|
||||
|
||||
|
||||
class DevelopmentConfig(BaseConfig):
|
||||
"""Development configuration."""
|
||||
|
||||
DEBUG: bool = True
|
||||
SQLALCHEMY_DATABASE_URI: str = "sqlite:///" + os.path.join(
|
||||
BASE_DIR, "database-dev.sqlite3"
|
||||
)
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"SQLALCHEMY_DATABASE_URI": {
|
||||
"env": "DEVEL_DATABASE_URL",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestingConfig(BaseConfig):
|
||||
"""Testing configuration."""
|
||||
|
||||
TESTING: bool = True
|
||||
PRESERVE_CONTEXT_ON_EXCEPTION: bool = False
|
||||
SQLALCHEMY_DATABASE_URI: str = "sqlite:///" + os.path.join(
|
||||
BASE_DIR, "database-test.sqlite3"
|
||||
)
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"SQLALCHEMY_DATABASE_URI": {
|
||||
"env": "TEST_DATABASE_URL",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProductionConfig(BaseConfig):
|
||||
"""Production configuration."""
|
||||
|
||||
SQLALCHEMY_DATABASE_URI: str = os.environ.get(
|
||||
"DATABASE_URL", "sqlite:///" + os.path.join(BASE_DIR, "database.sqlite3")
|
||||
)
|
||||
WTF_CSRF_ENABLED = True
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"SQLALCHEMY_DATABASE_URI": {
|
||||
"env": "DATABASE_URL",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@lru_cache
|
||||
def config(name=APP_ENV) -> DevelopmentConfig | TestingConfig | ProductionConfig:
|
||||
CONF_MAP = dict(
|
||||
development=DevelopmentConfig(),
|
||||
testing=TestingConfig(),
|
||||
production=ProductionConfig(),
|
||||
)
|
||||
configuration = CONF_MAP[name]
|
||||
configuration.ENV = name
|
||||
return configuration
|
|
@ -0,0 +1,31 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:14
|
||||
restart: always
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-passwd}
|
||||
POSTGRES_DB: db
|
||||
PGDATABASE: db
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD:-passwd}
|
||||
PGUSER: ${POSTGRES_USER:-postgres}
|
||||
ports:
|
||||
- 127.0.0.1:${LOCAL_DB_PORT:-15432}:5432
|
||||
|
||||
app:
|
||||
build: .
|
||||
# restart: always
|
||||
command: sh ./start_server.sh
|
||||
environment:
|
||||
APP_ENV: production
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- 127.0.0.1:${LOCAL_WEB_PORT:-8000}:8000
|
||||
|
||||
volumes:
|
||||
db_data:
|
|
@ -0,0 +1 @@
|
|||
Single-database configuration for Flask.
|
|
@ -0,0 +1,50 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,91 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions['migrate'].db.get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,40 @@
|
|||
"""init
|
||||
|
||||
Revision ID: 766427519c34
|
||||
Revises:
|
||||
Create Date: 2023-04-07 19:36:58.671750
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '766427519c34'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=60), nullable=False),
|
||||
sa.Column('email', sa.String(length=255), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=255), nullable=True),
|
||||
sa.Column('activated', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('unique_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('reset_password_uid', sa.String(length=64), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('users')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "static",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"watch": "webpack --watch",
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"flowbite": "^1.6.4",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^6.7.3",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-loader": "^7.2.4",
|
||||
"postcss-preset-env": "^8.3.1",
|
||||
"style-loader": "^3.3.2",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.79.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,900 @@
|
|||
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.10.3"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"},
|
||||
{file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
typing-extensions = ">=4"
|
||||
|
||||
[package.extras]
|
||||
tz = ["python-dateutil"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.12.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.6.2"
|
||||
description = "Fast, simple object-to-object and broadcast signaling"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
|
||||
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.3.0"
|
||||
description = "DNS toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"},
|
||||
{file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
|
||||
dnssec = ["cryptography (>=2.6,<40.0)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"]
|
||||
doq = ["aioquic (>=0.9.20)"]
|
||||
idna = ["idna (>=2.1,<4.0)"]
|
||||
trio = ["trio (>=0.14,<0.23)"]
|
||||
wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "1.3.1"
|
||||
description = "A robust email address syntax and deliverability validation library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"},
|
||||
{file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
dnspython = ">=1.15.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "18.4.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Faker-18.4.0-py3-none-any.whl", hash = "sha256:170ead9d0d140916168b142df69c44722b8f622ced2070802d0af9c476f0cb84"},
|
||||
{file = "Faker-18.4.0.tar.gz", hash = "sha256:977ad0b7aa7a61ed57287d6a0723a827e9d3dd1f8cc82aaf08707f281b33bacc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "4.0.1"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
|
||||
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.6.0,<0.7.0"
|
||||
pycodestyle = ">=2.8.0,<2.9.0"
|
||||
pyflakes = ">=2.4.0,<2.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "2.2.3"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-2.2.3-py3-none-any.whl", hash = "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"},
|
||||
{file = "Flask-2.2.3.tar.gz", hash = "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0"
|
||||
itsdangerous = ">=2.0"
|
||||
Jinja2 = ">=3.0"
|
||||
Werkzeug = ">=2.2.2"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-login"
|
||||
version = "0.6.2"
|
||||
description = "User authentication and session management for Flask."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-Login-0.6.2.tar.gz", hash = "sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3"},
|
||||
{file = "Flask_Login-0.6.2-py3-none-any.whl", hash = "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=1.0.4"
|
||||
Werkzeug = ">=1.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "flask-mail"
|
||||
version = "0.9.1"
|
||||
description = "Flask extension for sending email"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blinker = "*"
|
||||
Flask = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask-migrate"
|
||||
version = "4.0.4"
|
||||
description = "SQLAlchemy database migrations for Flask applications using Alembic."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"},
|
||||
{file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alembic = ">=1.9.0"
|
||||
Flask = ">=0.9"
|
||||
Flask-SQLAlchemy = ">=1.0"
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
version = "3.0.3"
|
||||
description = "Add SQLAlchemy support to your Flask application."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"},
|
||||
{file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=2.2"
|
||||
SQLAlchemy = ">=1.4.18"
|
||||
|
||||
[[package]]
|
||||
name = "flask-wtf"
|
||||
version = "1.1.1"
|
||||
description = "Form rendering, validation, and CSRF protection for Flask with WTForms."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-WTF-1.1.1.tar.gz", hash = "sha256:41c4244e9ae626d63bed42ae4785b90667b885b1535d5a4095e1f63060d12aa9"},
|
||||
{file = "Flask_WTF-1.1.1-py3-none-any.whl", hash = "sha256:7887d6f1ebb3e17bf648647422f0944c9a469d0fcf63e3b66fb9a83037e38b2c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = "*"
|
||||
itsdangerous = "*"
|
||||
WTForms = "*"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator"]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "2.0.2"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
files = [
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"},
|
||||
{file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "docutils (<0.18)"]
|
||||
test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.2.4"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
|
||||
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["Babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.2"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
|
||||
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.6.1"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.11.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
|
||||
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.2.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
|
||||
{file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.6"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "psycopg2-binary-2.9.6.tar.gz", hash = "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-win32.whl", hash = "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-win32.whl", hash = "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6"},
|
||||
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win32.whl", hash = "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win32.whl", hash = "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-win32.whl", hash = "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-win32.whl", hash = "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.8.0"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
|
||||
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.7"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"},
|
||||
{file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"},
|
||||
{file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"},
|
||||
{file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"},
|
||||
{file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"},
|
||||
{file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"},
|
||||
{file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"},
|
||||
{file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "2.4.0"
|
||||
description = "passive checker of Python programs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
|
||||
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.3.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"},
|
||||
{file = "pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.0"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
|
||||
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.9"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"},
|
||||
{file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"},
|
||||
{file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"},
|
||||
{file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"},
|
||||
{file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"},
|
||||
{file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"},
|
||||
{file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"},
|
||||
{file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx-oracle (>=7)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql"]
|
||||
sqlcipher = ["sqlcipher3-binary"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.5.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
|
||||
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.2.3"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"},
|
||||
{file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.1.1"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[[package]]
|
||||
name = "wtforms"
|
||||
version = "3.0.1"
|
||||
description = "Form validation and rendering for Python web development."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "WTForms-3.0.1-py3-none-any.whl", hash = "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"},
|
||||
{file = "WTForms-3.0.1.tar.gz", hash = "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = "*"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "f3aa267d5abb84abcb10c677d6c49bd9d274cdb680ff779f71e96d064615c6bf"
|
|
@ -0,0 +1,3 @@
|
|||
[virtualenvs]
|
||||
create = true
|
||||
in-project = true
|
|
@ -0,0 +1,7 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'postcss-preset-env',
|
||||
tailwindcss
|
||||
],
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
# Put environment variables that hold sensitive data such as passwords here.
|
||||
# NOTE: This file will not be placed under version control.
|
||||
|
||||
APP_NAME=Simple Flask App
|
||||
SECRET_KEY=set_here_secret
|
||||
|
||||
# Database
|
||||
# Database details for postgres container
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=passwd
|
||||
POSTGRES_DB=db
|
||||
LOCAL_DB_PORT=15432
|
||||
|
||||
# Database URL to use
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
DEVEL_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${LOCAL_DB_PORT}/${POSTGRES_DB}
|
||||
|
||||
# WEB app
|
||||
LOCAL_WEB_PORT=8000
|
||||
|
||||
# e-mail configuration
|
||||
MAIL_USERNAME=admin@simple2b.com
|
||||
MAIL_DEFAULT_SENDER=admin@simple2b.com
|
||||
MAIL_PASSWORD="super secret"
|
||||
|
||||
MAIL_SERVER=mail.simple2b.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USE_TLS=false
|
||||
MAIL_USE_SSL=true
|
||||
MAIL_USERNAME=user_name
|
||||
MAIL_PASSWORD=set_password
|
||||
MAIL_DEFAULT_SENDER=user@simple2b.com
|
||||
|
||||
# Super admin
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=simple2b.info@gmail.com
|
||||
ADMIN_PASSWORD=admin
|
||||
|
||||
# Pagination
|
||||
DEFAULT_PAGE_SIZE=8
|
||||
PAGE_LINKS_NUMBER=8
|
|
@ -0,0 +1,30 @@
|
|||
[tool.poetry]
|
||||
name = "flask.app"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["denys <denysburimov@gmail.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
flask = "^2.2.3"
|
||||
flask-migrate = "^4.0.4"
|
||||
flask-wtf = "^1.1.1"
|
||||
flask-mail = "^0.9.1"
|
||||
flask-login = "^0.6.2"
|
||||
python-dotenv = "^1.0.0"
|
||||
click = "^8.1.3"
|
||||
email-validator = "^1.3.1"
|
||||
psycopg2-binary = "^2.9.5"
|
||||
pydantic = "^1.10.7"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.1"
|
||||
flake8 = "^4.0.1"
|
||||
black = "^22.3.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
faker = "^18.3.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
|
@ -0,0 +1,4 @@
|
|||
import './styles.css';
|
||||
import {initUsers} from './user';
|
||||
|
||||
initUsers();
|
|
@ -0,0 +1,8 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
.text-danger {
|
||||
color: red;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import {Modal} from 'flowbite';
|
||||
import type {ModalOptions, ModalInterface} from 'flowbite';
|
||||
|
||||
// /*
|
||||
// * $editUserModal: required
|
||||
// * options: optional
|
||||
// */
|
||||
|
||||
// // For your js code
|
||||
|
||||
interface IUser {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
activated: boolean;
|
||||
}
|
||||
|
||||
const $modalElement: HTMLElement = document.querySelector('#editUserModal');
|
||||
const $addUserModalElement: HTMLElement =
|
||||
document.querySelector('#add-user-modal');
|
||||
|
||||
const modalOptions: ModalOptions = {
|
||||
placement: 'bottom-right',
|
||||
backdrop: 'dynamic',
|
||||
backdropClasses:
|
||||
'bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40',
|
||||
closable: true,
|
||||
onHide: () => {
|
||||
console.log('modal is hidden');
|
||||
},
|
||||
onShow: () => {
|
||||
console.log('user id: ');
|
||||
},
|
||||
onToggle: () => {
|
||||
console.log('modal has been toggled');
|
||||
},
|
||||
};
|
||||
|
||||
const modal: ModalInterface = new Modal($modalElement, modalOptions);
|
||||
const addModal: ModalInterface = new Modal($addUserModalElement, modalOptions);
|
||||
|
||||
export function initUsers() {
|
||||
const $buttonElements = document.querySelectorAll('.user-edit-button');
|
||||
$buttonElements.forEach(e =>
|
||||
e.addEventListener('click', () => {
|
||||
editUser(JSON.parse(e.getAttribute('data-target')));
|
||||
}),
|
||||
);
|
||||
|
||||
// closing add edit modal
|
||||
const $buttonClose = document.querySelector('#modalCloseButton');
|
||||
if ($buttonClose) {
|
||||
$buttonClose.addEventListener('click', () => {
|
||||
modal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
// closing add user modal
|
||||
const addModalCloseBtn = document.querySelector('#modalAddCloseButton');
|
||||
if (addModalCloseBtn) {
|
||||
addModalCloseBtn.addEventListener('click', () => {
|
||||
addModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
// search flow
|
||||
const searchInput: HTMLInputElement = document.querySelector(
|
||||
'#table-search-users',
|
||||
);
|
||||
const searchInputButton = document.querySelector('#table-search-user-button');
|
||||
if (searchInputButton && searchInput) {
|
||||
searchInputButton.addEventListener('click', () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('q', searchInput.value);
|
||||
window.location.href = `${url.href}`;
|
||||
});
|
||||
}
|
||||
const deleteButtons = document.querySelectorAll('.delete-user-btn');
|
||||
|
||||
deleteButtons.forEach(e => {
|
||||
e.addEventListener('click', async () => {
|
||||
if (confirm('Are sure?')) {
|
||||
let id = e.getAttribute('data-user-id');
|
||||
const response = await fetch(`/user/delete/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.status == 200) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editUser(user: IUser) {
|
||||
let input: HTMLInputElement = document.querySelector('#user-edit-username');
|
||||
input.value = user.username;
|
||||
input = document.querySelector('#user-edit-id');
|
||||
input.value = user.id.toString();
|
||||
input = document.querySelector('#user-edit-email');
|
||||
input.value = user.email;
|
||||
input = document.querySelector('#user-edit-password');
|
||||
input.value = '*******';
|
||||
input = document.querySelector('#user-edit-password_confirmation');
|
||||
input.value = '*******';
|
||||
input = document.querySelector('#user-edit-activated');
|
||||
input.checked = user.activated;
|
||||
input = document.querySelector('#user-edit-next_url');
|
||||
input.value = window.location.href;
|
||||
modal.show();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
sleep 2
|
||||
echo Run db upgrade
|
||||
poetry run flask db upgrade
|
||||
# echo Run app
|
||||
# flask run -h 0.0.0.0
|
||||
echo Run app server
|
||||
poetry run gunicorn -w 4 -b 0.0.0.0 'wsgi:app'
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./app/templates/**/*.html",
|
||||
"./src/js/**/*.js",
|
||||
"./node_modules/flowbite/**/*.js",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("flowbite/plugin")],
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
import pytest
|
||||
from flask import Flask
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from app import create_app, db
|
||||
from app import models as m
|
||||
from tests.utils import register
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
app = create_app("testing")
|
||||
app.config.update(
|
||||
{
|
||||
"TESTING": True,
|
||||
}
|
||||
)
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app: Flask):
|
||||
with app.test_client() as client:
|
||||
app_ctx = app.app_context()
|
||||
app_ctx.push()
|
||||
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
register()
|
||||
|
||||
yield client
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
app_ctx.pop()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def runner(app, client):
|
||||
from app import commands
|
||||
|
||||
commands.init(app)
|
||||
|
||||
yield app.test_cli_runner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def populate(client: FlaskClient):
|
||||
NUM_TEST_USERS = 100
|
||||
for i in range(NUM_TEST_USERS):
|
||||
m.User(
|
||||
username=f"user{i+1}",
|
||||
email=f"user{i+1}@mail.com",
|
||||
password="password",
|
||||
).save(False)
|
||||
db.session.commit()
|
||||
yield client
|
|
@ -0,0 +1,46 @@
|
|||
from typing import Generator
|
||||
from faker import Faker
|
||||
from sqlalchemy import func
|
||||
from app import db
|
||||
from app import models as m
|
||||
|
||||
|
||||
faker = Faker()
|
||||
|
||||
NUM_TEST_USERS = 100
|
||||
|
||||
|
||||
def gen_test_items(num_objects: int) -> Generator[str, None, None]:
|
||||
from faker import Faker
|
||||
|
||||
fake = Faker()
|
||||
|
||||
DOMAINS = ("com", "com.br", "net", "net.br", "org", "org.br", "gov", "gov.br")
|
||||
|
||||
i = db.session.query(func.max(m.User.id)).scalar()
|
||||
|
||||
for _ in range(num_objects):
|
||||
i += 1
|
||||
# Primary name
|
||||
first_name = fake.first_name()
|
||||
|
||||
# Secondary name
|
||||
last_name = fake.last_name()
|
||||
|
||||
company = fake.company().split()[0].strip(",")
|
||||
|
||||
# Company DNS
|
||||
dns_org = fake.random_choices(elements=DOMAINS, length=1)[0]
|
||||
|
||||
# email formatting
|
||||
yield f"{first_name}{i}".lower(), f"{first_name}.{last_name}{i}@{company}.{dns_org}".lower()
|
||||
|
||||
|
||||
def populate(count: int = NUM_TEST_USERS):
|
||||
for username, email in gen_test_items(count):
|
||||
m.User(
|
||||
username=username,
|
||||
email=email,
|
||||
).save(False)
|
||||
|
||||
db.session.commit()
|
|
@ -0,0 +1,22 @@
|
|||
from tests.utils import login
|
||||
|
||||
TEST_EMAIL = "test@gmail.com"
|
||||
|
||||
|
||||
def test_auth_pages(client):
|
||||
response = client.get("/login")
|
||||
assert response.status_code == 200
|
||||
response = client.get("/logout")
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def test_login_and_logout(client):
|
||||
# Access to logout view before login should fail.
|
||||
response = login(client, "sam")
|
||||
assert b"Login successful." in response.data
|
||||
# Incorrect login credentials should fail.
|
||||
response = login(client, "sam", "wrongpassword")
|
||||
assert b"Wrong user ID or password." in response.data
|
||||
# Correct credentials should login
|
||||
response = login(client, "sam")
|
||||
assert b"Login successful." in response.data
|
|
@ -0,0 +1,53 @@
|
|||
from flask import current_app as app
|
||||
from flask.testing import FlaskClient, FlaskCliRunner
|
||||
from click.testing import Result
|
||||
from app import models as m
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_list(populate: FlaskClient):
|
||||
login(populate)
|
||||
DEFAULT_PAGE_SIZE = app.config["DEFAULT_PAGE_SIZE"]
|
||||
response = populate.get("/user/")
|
||||
assert response
|
||||
assert response.status_code == 200
|
||||
html = response.data.decode()
|
||||
users = m.User.query.order_by(m.User.id).limit(11).all()
|
||||
assert len(users) == 11
|
||||
for user in users[:DEFAULT_PAGE_SIZE]:
|
||||
assert user.username in html
|
||||
assert users[10].username not in html
|
||||
|
||||
populate.application.config["PAGE_LINKS_NUMBER"] = 6
|
||||
response = populate.get("/user/?page=6")
|
||||
assert response
|
||||
assert response.status_code == 200
|
||||
html = response.data.decode()
|
||||
assert "/user/?page=6" in html
|
||||
assert "/user/?page=3" in html
|
||||
assert "/user/?page=8" in html
|
||||
assert "/user/?page=10" not in html
|
||||
assert "/user/?page=2" not in html
|
||||
|
||||
|
||||
def test_create_admin(runner: FlaskCliRunner):
|
||||
res: Result = runner.invoke(args=["create-admin"])
|
||||
assert "admin created" in res.output
|
||||
assert m.User.query.filter_by(username=app.config["ADMIN_USERNAME"]).first()
|
||||
|
||||
|
||||
def test_populate_db(runner: FlaskCliRunner):
|
||||
TEST_COUNT = 56
|
||||
count_before = m.User.query.count()
|
||||
res: Result = runner.invoke(args=["db-populate", "--count", f"{TEST_COUNT}"])
|
||||
assert f"populated by {TEST_COUNT}" in res.stdout
|
||||
assert (m.User.query.count() - count_before) == TEST_COUNT
|
||||
|
||||
|
||||
def test_delete_user(populate: FlaskClient):
|
||||
login(populate)
|
||||
users = m.User.query.all()
|
||||
uc = len(users)
|
||||
response = populate.delete("/user/delete/1")
|
||||
assert m.User.query.count() < uc
|
||||
assert response.status_code == 200
|
|
@ -0,0 +1,24 @@
|
|||
from app.models import User
|
||||
|
||||
TEST_ADMIN_NAME = "bob"
|
||||
TEST_ADMIN_EMAIL = "bob@test.com"
|
||||
TEST_ADMIN_PASSWORD = "password"
|
||||
|
||||
|
||||
def register(
|
||||
username=TEST_ADMIN_NAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD
|
||||
):
|
||||
user = User(username=username, email=email)
|
||||
user.password = password
|
||||
user.save()
|
||||
return user.id
|
||||
|
||||
|
||||
def login(client, username=TEST_ADMIN_NAME, password=TEST_ADMIN_PASSWORD):
|
||||
return client.post(
|
||||
"/login", data=dict(user_id=username, password=password), follow_redirects=True
|
||||
)
|
||||
|
||||
|
||||
def logout(client):
|
||||
return client.get("/logout", follow_redirects=True)
|
|
@ -0,0 +1,6 @@
|
|||
[flake8]
|
||||
;ignore = E226,E302,E41,E202,E203
|
||||
max-line-length = 120
|
||||
;exclude = tests/*
|
||||
;max-complexity = 10
|
||||
exclude = .git,__pycache__,.venv/,migrations/
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es2015"],
|
||||
"outDir": "./app/static/js/",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*.ts*"],
|
||||
"exclude": ["node_modules", "dist", "lib"]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//webpack.config.js
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
devtool: "inline-source-map",
|
||||
entry: {
|
||||
main: "./src/main.ts",
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "./app/static/js"),
|
||||
filename: "main.js", // <--- Will be compiled to this single file
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
include: path.resolve(__dirname, 'src'),
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
#!/user/bin/env python
|
||||
from app import create_app
|
||||
from app import commands
|
||||
|
||||
app = create_app()
|
||||
commands.init(app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
Loading…
Reference in New Issue