Initial Harambee commit

master
Yuri van Midden 4 years ago
parent 510934cd58
commit 85b4230e71
Signed by: yuri
GPG Key ID: B1E365DD233EF90A
  1. 9
      harambee_led_bord/.env.example
  2. 21
      harambee_led_bord/.eslintrc
  3. 58
      harambee_led_bord/.gitignore
  4. 16
      harambee_led_bord/.travis.yml
  5. 47
      harambee_led_bord/Dockerfile
  6. 19
      harambee_led_bord/LICENSE
  7. 60
      harambee_led_bord/Pipfile
  8. 171
      harambee_led_bord/README.md
  9. 86
      harambee_led_bord/assets/css/style.css
  10. 0
      harambee_led_bord/assets/img/.gitkeep
  11. BIN
      harambee_led_bord/assets/img/favicon.ico
  12. 21
      harambee_led_bord/assets/js/main.js
  13. 1
      harambee_led_bord/assets/js/plugins.js
  14. 1
      harambee_led_bord/assets/js/script.js
  15. 5
      harambee_led_bord/autoapp.py
  16. 57
      harambee_led_bord/docker-compose.yml
  17. 1
      harambee_led_bord/harambee_led_bord/__init__.py
  18. 91
      harambee_led_bord/harambee_led_bord/app.py
  19. 65
      harambee_led_bord/harambee_led_bord/commands.py
  20. 18
      harambee_led_bord/harambee_led_bord/compat.py
  21. 84
      harambee_led_bord/harambee_led_bord/database.py
  22. 19
      harambee_led_bord/harambee_led_bord/extensions.py
  23. 3
      harambee_led_bord/harambee_led_bord/public/__init__.py
  24. 39
      harambee_led_bord/harambee_led_bord/public/forms.py
  25. 77
      harambee_led_bord/harambee_led_bord/public/views.py
  26. 23
      harambee_led_bord/harambee_led_bord/settings.py
  27. 16
      harambee_led_bord/harambee_led_bord/templates/401.html
  28. 15
      harambee_led_bord/harambee_led_bord/templates/404.html
  29. 14
      harambee_led_bord/harambee_led_bord/templates/500.html
  30. 14
      harambee_led_bord/harambee_led_bord/templates/footer.html
  31. 71
      harambee_led_bord/harambee_led_bord/templates/layout.html
  32. 49
      harambee_led_bord/harambee_led_bord/templates/nav.html
  33. 12
      harambee_led_bord/harambee_led_bord/templates/public/about.html
  34. 35
      harambee_led_bord/harambee_led_bord/templates/public/home.html
  35. 31
      harambee_led_bord/harambee_led_bord/templates/public/register.html
  36. 9
      harambee_led_bord/harambee_led_bord/templates/users/members.html
  37. 3
      harambee_led_bord/harambee_led_bord/user/__init__.py
  38. 45
      harambee_led_bord/harambee_led_bord/user/forms.py
  39. 72
      harambee_led_bord/harambee_led_bord/user/models.py
  40. 13
      harambee_led_bord/harambee_led_bord/user/views.py
  41. 10
      harambee_led_bord/harambee_led_bord/utils.py
  42. 0
      harambee_led_bord/harambee_led_bord/webpack/.gitkeep
  43. 1
      harambee_led_bord/migrations/README
  44. 45
      harambee_led_bord/migrations/alembic.ini
  45. 96
      harambee_led_bord/migrations/env.py
  46. 24
      harambee_led_bord/migrations/script.py.mako
  47. 50
      harambee_led_bord/migrations/versions/6741bbbc111a_.py
  48. 52
      harambee_led_bord/package.json
  49. 11
      harambee_led_bord/setup.cfg
  50. 7
      harambee_led_bord/shell_scripts/auto_pipenv.sh
  51. 12
      harambee_led_bord/shell_scripts/supervisord_entrypoint.sh
  52. 21
      harambee_led_bord/supervisord.conf
  53. 18
      harambee_led_bord/supervisord_programs/gunicorn.conf
  54. 1
      harambee_led_bord/tests/__init__.py
  55. 53
      harambee_led_bord/tests/conftest.py
  56. 31
      harambee_led_bord/tests/factories.py
  57. 12
      harambee_led_bord/tests/settings.py
  58. 77
      harambee_led_bord/tests/test_forms.py
  59. 120
      harambee_led_bord/tests/test_functional.py
  60. 66
      harambee_led_bord/tests/test_models.py
  61. 82
      harambee_led_bord/webpack.config.js

@ -0,0 +1,9 @@
# Environment variable overrides for local development
FLASK_APP=autoapp.py
FLASK_DEBUG=1
FLASK_ENV=development
DATABASE_URL=sqlite:////tmp/dev.db
GUNICORN_WORKERS=1
LOG_LEVEL=debug
SECRET_KEY=not-so-secret
SEND_FILE_MAX_AGE_DEFAULT=0 # In production, set to a higher number, like 31556926

@ -0,0 +1,21 @@
{
"extends": "airbnb-base",
"parser": "babel-eslint",
"rules": {
"no-param-reassign": 0,
"import/no-extraneous-dependencies": 0,
"import/prefer-default-export": 0,
"consistent-return": 0,
"no-confusing-arrow": 0,
"no-underscore-dangle": 0
},
"env": {
"browser": true,
"node": true
},
"globals": {
"__dirname": true,
"jQuery": true,
"$": true
}
}

@ -0,0 +1,58 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Complexity
output/*.html
output/*/index.html
# Sphinx
docs/_build
.webassets-cache
# Virtualenvs
env/
# npm
/node_modules/
# webpack-built files
/harambee_led_bord/static/build/
# Configuration
.env
# Development database
*.db

@ -0,0 +1,16 @@
# Config file for automatic testing at travis-ci.org
dist: xenial
language: python
python:
- 3.6
- 3.7
install:
- pip install -r requirements/dev.txt
- nvm install 12
- nvm use 12
- npm install
before_script:
- cp .env.example .env
- npm run lint
- flask lint --check
script: flask test

@ -0,0 +1,47 @@
# ==================================== BASE ====================================
ARG INSTALL_PYTHON_VERSION=${INSTALL_PYTHON_VERSION:-3.7}
FROM python:${INSTALL_PYTHON_VERSION}-slim-buster AS base
RUN apt-get update
RUN apt-get install -y \
curl \
gcc
ARG INSTALL_NODE_VERSION=${INSTALL_NODE_VERSION:-12}
RUN curl -sL https://deb.nodesource.com/setup_${INSTALL_NODE_VERSION}.x | bash -
RUN apt-get install -y \
nodejs \
&& apt-get -y autoclean
WORKDIR /app
COPY ["Pipfile", "shell_scripts/auto_pipenv.sh", "./"]
RUN pip install pipenv
COPY . .
RUN useradd -m sid
RUN chown -R sid:sid /app
USER sid
ENV PATH="/home/sid/.local/bin:${PATH}"
RUN npm install
# ================================= DEVELOPMENT ================================
FROM base AS development
RUN pipenv install --dev
EXPOSE 2992
EXPOSE 5000
CMD [ "pipenv", "run", "npm", "start" ]
# ================================= PRODUCTION =================================
FROM base AS production
RUN pipenv install
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY supervisord_programs /etc/supervisor/conf.d
EXPOSE 5000
ENTRYPOINT ["/bin/bash", "shell_scripts/supervisord_entrypoint.sh"]
CMD ["-c", "/etc/supervisor/supervisord.conf"]
# =================================== MANAGE ===================================
FROM base AS manage
COPY --from=development /home/sid/.local/share/virtualenvs/ /home/sid/.local/share/virtualenvs/
ENTRYPOINT [ "pipenv", "run", "flask" ]

@ -0,0 +1,19 @@
Copyright 2019 Yuri van Midden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,60 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
# Flask
Flask = "==1.1.1"
Werkzeug = "==0.16.0"
click = ">=5.0"
# Database
Flask-SQLAlchemy = "==2.4.1"
SQLAlchemy = "==1.3.11"
psycopg2-binary = "==2.8.4"
# Migrations
Flask-Migrate = "==2.5.2"
# Forms
Flask-WTF = "==0.14.2"
WTForms = "==2.2.1"
# Deployment
gevent = "==1.4.0"
gunicorn = ">=19.1.1"
supervisor = "==4.1.0"
# Flask Static Digest
Flask-Static-Digest = "==0.1.2"
# Auth
Flask-Login = "==0.4.1"
Flask-Bcrypt = "==0.7.1"
# Caching
Flask-Caching = ">=1.0.0"
# Debug toolbar
Flask-DebugToolbar = "==0.10.1"
# Environment variable parsing
environs = "==7.1.0"
[dev-packages]
# Testing
pytest = "==5.3.1"
WebTest = "==2.0.33"
factory-boy = "==2.12.*"
pdbpp = "==0.10.2"
# Lint and code style
black = "==19.10b0"
flake8 = "==3.7.9"
flake8-blind-except = "==0.1.1"
flake8-debugger = "==3.2.1"
flake8-docstrings = "==1.5.0"
flake8-isort = "==2.8.0"
isort = "==4.3.21"
pep8-naming = "==0.9.1"

@ -0,0 +1,171 @@
# Harambee LED bord
Backend voor het Harambee led bord
## Docker Quickstart
This app can be run completely using `Docker` and `docker-compose`. **Using Docker is recommended, as it guarantees the application is run using compatible versions of Python and Node**.
There are three main services:
To run the development version of the app
```bash
docker-compose up flask-dev
```
To run the production version of the app
```bash
docker-compose up flask-prod
```
The list of `environment:` variables in the `docker-compose.yml` file takes precedence over any variables specified in `.env`.
To run any commands using the `Flask CLI`
```bash
docker-compose run --rm manage <<COMMAND>>
```
Therefore, to initialize a database you would run
```bash
docker-compose run --rm manage db init
docker-compose run --rm manage db migrate
docker-compose run --rm manage db upgrade
```
A docker volume `node-modules` is created to store NPM packages and is reused across the dev and prod versions of the application. For the purposes of DB testing with `sqlite`, the file `dev.db` is mounted to all containers. This volume mount should be removed from `docker-compose.yml` if a production DB server is used.
### Running locally
Run the following commands to bootstrap your environment if you are unable to run the application using Docker
```bash
cd harambee_led_bord
pipenv install --dev
pipenv shell
npm install
npm start # run the webpack dev server and flask server using concurrently
```
You will see a pretty welcome screen.
#### Database Initialization (locally)
Once you have installed your DBMS, run the following to create your app's
database tables and perform the initial migration
```bash
flask db init
flask db migrate
flask db upgrade
```
## Deployment
When using Docker, reasonable production defaults are set in `docker-compose.yml`
```text
FLASK_ENV=production
FLASK_DEBUG=0
```
Therefore, starting the app in "production" mode is as simple as
```bash
docker-compose up flask-prod
```
If running without Docker
```bash
export FLASK_ENV=production
export FLASK_DEBUG=0
export DATABASE_URL="<YOUR DATABASE URL>"
npm run build # build assets with webpack
flask run # start the flask server
```
## Shell
To open the interactive shell, run
```bash
docker-compose run --rm manage db shell
flask shell # If running locally without Docker
```
By default, you will have access to the flask `app`.
## Running Tests/Linter
To run all tests, run
```bash
docker-compose run --rm manage test
flask test # If running locally without Docker
```
To run the linter, run
```bash
docker-compose run --rm manage lint
flask lint # If running locally without Docker
```
The `lint` command will attempt to fix any linting/style errors in the code. If you only want to know if the code will pass CI and do not wish for the linter to make changes, add the `--check` argument.
## Migrations
Whenever a database migration needs to be made. Run the following commands
```bash
docker-compose run --rm manage db migrate
flask db migrate # If running locally without Docker
```
This will generate a new migration script. Then run
```bash
docker-compose run --rm manage db upgrade
flask db upgrade # If running locally without Docker
```
To apply the migration.
For a full migration command reference, run `docker-compose run --rm manage db --help`.
If you will deploy your application remotely (e.g on Heroku) you should add the `migrations` folder to version control.
You can do this after `flask db migrate` by running the following commands
```bash
git add migrations/*
git commit -m "Add migrations"
```
Make sure folder `migrations/versions` is not empty.
## Asset Management
Files placed inside the `assets` directory and its subdirectories
(excluding `js` and `css`) will be copied by webpack's
`file-loader` into the `static/build` directory. In production, the plugin
`Flask-Static-Digest` zips the webpack content and tags them with a MD5 hash.
As a result, you must use the `static_url_for` function when including static content,
as it resolves the correct file name, including the MD5 hash.
For example
```html
<link rel="shortcut icon" href="{{static_url_for('static', filename='build/img/favicon.ico') }}">
```
If all of your static files are managed this way, then their filenames will change whenever their
contents do, and you can ask Flask to tell web browsers that they
should cache all your assets forever by including the following line
in ``.env``:
```text
SEND_FILE_MAX_AGE_DEFAULT=31556926 # one year
```

@ -0,0 +1,86 @@
/* =============================================================================
App specific CSS file.
========================================================================== */
/* universal */
html {
overflow-y: scroll;
}
body {
padding-top: 60px;
}
section {
overflow: auto;
}
textarea {
resize: vertical;
}
.container-narrow{
margin: 0 auto;
max-width: 700px;
}
/* Forms */
.navbar-form input[type="text"],
.navbar-form input[type="password"] {
width: 180px;
}
.form-register {
width: 50%;
}
.form-register .form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/* footer */
footer {
margin-top: 45px;
padding-top: 5px;
border-top: 1px solid #eaeaea;
color: #999999;
}
footer a {
color: #999999;
}
footer p {
float: right;
margin-right: 25px;
}
footer ul {
list-style: none;
}
footer ul li {
float: left;
margin-left: 10px;
}
footer .company {
float: left;
margin-left: 25px;
}
footer .footer-nav {
float: right;
margin-right: 25px;
list-style: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@ -0,0 +1,21 @@
/*
* Main Javascript file for harambee_led_bord.
*
* This file bundles all of your javascript together using webpack.
*/
// JavaScript modules
require('@fortawesome/fontawesome-free');
require('jquery');
require('popper.js');
require('bootstrap');
require.context(
'../img', // context folder
true, // include subdirectories
/.*/, // RegExp
);
// Your own code
require('./plugins.js');
require('./script.js');

@ -0,0 +1 @@
// place any jQuery/helper plugins in here, instead of separate, slower script files.

@ -0,0 +1 @@
// App initialization code goes here

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Create an application instance."""
from harambee_led_bord.app import create_app
app = create_app()

@ -0,0 +1,57 @@
version: '3.6'
x-build-args: &build_args
INSTALL_PYTHON_VERSION: 3.8
INSTALL_NODE_VERSION: 12
x-default-volumes: &default_volumes
volumes:
- ./:/app
- node-modules:/app/node_modules
- ./dev.db:/tmp/dev.db
services:
flask-dev:
build:
context: .
target: development
args:
<<: *build_args
image: "harambee_led_bord-development"
ports:
- "5000:5000"
- "2992:2992"
<<: *default_volumes
flask-prod:
build:
context: .
target: production
args:
<<: *build_args
image: "harambee_led_bord-production"
ports:
- "5000:5000"
environment:
FLASK_ENV: production
FLASK_DEBUG: 0
LOG_LEVEL: info
GUNICORN_WORKERS: 4
<<: *default_volumes
manage:
build:
context: .
target: manage
environment:
FLASK_ENV: production
FLASK_DEBUG: 0
image: "harambee_led_bord-manage"
stdin_open: true
tty: true
<<: *default_volumes
volumes:
node-modules:
static-build:
dev-db:

@ -0,0 +1 @@
"""Main application package."""

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
"""The app module, containing the app factory function."""
import logging
import sys
from flask import Flask, render_template
from harambee_led_bord import commands, public, user
from harambee_led_bord.extensions import (
bcrypt,
cache,
csrf_protect,
db,
debug_toolbar,
flask_static_digest,
login_manager,
migrate,
)
def create_app(config_object="harambee_led_bord.settings"):
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
:param config_object: The configuration object to use.
"""
app = Flask(__name__.split(".")[0])
app.config.from_object(config_object)
register_extensions(app)
register_blueprints(app)
register_errorhandlers(app)
register_shellcontext(app)
register_commands(app)
configure_logger(app)
return app
def register_extensions(app):
"""Register Flask extensions."""
bcrypt.init_app(app)
cache.init_app(app)
db.init_app(app)
csrf_protect.init_app(app)
login_manager.init_app(app)
debug_toolbar.init_app(app)
migrate.init_app(app, db)
flask_static_digest.init_app(app)
return None
def register_blueprints(app):
"""Register Flask blueprints."""
app.register_blueprint(public.views.blueprint)
app.register_blueprint(user.views.blueprint)
return None
def register_errorhandlers(app):
"""Register error handlers."""
def render_error(error):
"""Render error template."""
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, "code", 500)
return render_template(f"{error_code}.html"), error_code
for errcode in [401, 404, 500]:
app.errorhandler(errcode)(render_error)
return None
def register_shellcontext(app):
"""Register shell context objects."""
def shell_context():
"""Shell context objects."""
return {"db": db, "User": user.models.User}
app.shell_context_processor(shell_context)
def register_commands(app):
"""Register Click commands."""
app.cli.add_command(commands.test)
app.cli.add_command(commands.lint)
def configure_logger(app):
"""Configure loggers."""
handler = logging.StreamHandler(sys.stdout)
if not app.logger.handlers:
app.logger.addHandler(handler)

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""Click commands."""
import os
from glob import glob
from subprocess import call
import click
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
TEST_PATH = os.path.join(PROJECT_ROOT, "tests")
@click.command()
def test():
"""Run the tests."""
import pytest
rv = pytest.main([TEST_PATH, "--verbose"])
exit(rv)
@click.command()
@click.option(
"-f",
"--fix-imports",
default=True,
is_flag=True,
help="Fix imports using isort, before linting",
)
@click.option(
"-c",
"--check",
default=False,
is_flag=True,
help="Don't make any changes to files, just confirm they are formatted correctly",
)
def lint(fix_imports, check):
"""Lint and check code style with black, flake8 and isort."""
skip = ["node_modules", "requirements", "migrations"]
root_files = glob("*.py")
root_directories = [
name for name in next(os.walk("."))[1] if not name.startswith(".")
]
files_and_directories = [
arg for arg in root_files + root_directories if arg not in skip
]
def execute_tool(description, *args):
"""Execute a checking tool with its arguments."""
command_line = list(args) + files_and_directories
click.echo(f"{description}: {' '.join(command_line)}")
rv = call(command_line)
if rv != 0:
exit(rv)
isort_args = ["-rc"]
black_args = []
if check:
isort_args.append("-c")
black_args.append("--check")
if fix_imports:
execute_tool("Fixing import order", "isort", *isort_args)
execute_tool("Formatting style", "black", *black_args)
execute_tool("Checking code style", "flake8")

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""Python 2/3 compatibility module."""
import sys
PY2 = int(sys.version[0]) == 2
if PY2:
text_type = unicode # noqa
binary_type = str
string_types = (str, unicode) # noqa
unicode = unicode # noqa
basestring = basestring # noqa
else:
text_type = str
binary_type = bytes
string_types = (str,)
unicode = str
basestring = (str, bytes)

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""Database module, including the SQLAlchemy database object and DB-related utilities."""
from .compat import basestring
from .extensions import db
# Alias common SQLAlchemy names
Column = db.Column
relationship = db.relationship
class CRUDMixin(object):
"""Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
@classmethod
def create(cls, **kwargs):
"""Create a new record and save it the database."""
instance = cls(**kwargs)
return instance.save()
def update(self, commit=True, **kwargs):
"""Update specific fields of a record."""
for attr, value in kwargs.items():
setattr(self, attr, value)
return commit and self.save() or self
def save(self, commit=True):
"""Save the record."""
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
"""Remove the record from the database."""
db.session.delete(self)
return commit and db.session.commit()
class Model(CRUDMixin, db.Model):
"""Base model class that includes CRUD convenience methods."""
__abstract__ = True
# From Mike Bayer's "Building the app" talk
# https://speakerdeck.com/zzzeek/building-the-app
class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column named ``id`` to any declarative-mapped class."""
__table_args__ = {"extend_existing": True}
id = Column(db.Integer, primary_key=True)
@classmethod
def get_by_id(cls, record_id):
"""Get record by ID."""
if any(
(
isinstance(record_id, basestring) and record_id.isdigit(),
isinstance(record_id, (int, float)),
)
):
return cls.query.get(int(record_id))
return None
def reference_col(
tablename, nullable=False, pk_name="id", foreign_key_kwargs=None, column_kwargs=None
):
"""Column that adds primary key foreign key reference.
Usage: ::
category_id = reference_col('category')
category = relationship('Category', backref='categories')
"""
foreign_key_kwargs = foreign_key_kwargs or {}
column_kwargs = column_kwargs or {}
return Column(
db.ForeignKey(f"{tablename}.{pk_name}", **foreign_key_kwargs),
nullable=nullable,
**column_kwargs,
)

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""Extensions module. Each extension is initialized in the app factory located in app.py."""
from flask_bcrypt import Bcrypt
from flask_caching import Cache
from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_static_digest import FlaskStaticDigest
from flask_wtf.csrf import CSRFProtect
bcrypt = Bcrypt()
csrf_protect = CSRFProtect()
login_manager = LoginManager()
db = SQLAlchemy()
migrate = Migrate()
cache = Cache()
debug_toolbar = DebugToolbarExtension()
flask_static_digest = FlaskStaticDigest()

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""The public module, including the homepage and user auth."""
from . import views # noqa

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""Public forms."""
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField
from wtforms.validators import DataRequired
from harambee_led_bord.user.models import User
class LoginForm(FlaskForm):
"""Login form."""
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
def __init__(self, *args, **kwargs):
"""Create instance."""
super(LoginForm, self).__init__(*args, **kwargs)
self.user = None
def validate(self):
"""Validate the form."""
initial_validation = super(LoginForm, self).validate()
if not initial_validation:
return False
self.user = User.query.filter_by(username=self.username.data).first()
if not self.user:
self.username.errors.append("Unknown username")
return False
if not self.user.check_password(self.password.data):
self.password.errors.append("Invalid password")
return False
if not self.user.active:
self.username.errors.append("User not activated")
return False
return True

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""Public section, including homepage and signup."""
from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
url_for,
)
from flask_login import login_required, login_user, logout_user
from harambee_led_bord.extensions import login_manager
from harambee_led_bord.public.forms import LoginForm
from harambee_led_bord.user.forms import RegisterForm
from harambee_led_bord.user.models import User
from harambee_led_bord.utils import flash_errors
blueprint = Blueprint("public", __name__, static_folder="../static")
@login_manager.user_loader
def load_user(user_id):
"""Load user by ID."""
return User.get_by_id(int(user_id))
@blueprint.route("/", methods=["GET", "POST"])
def home():
"""Home page."""
form = LoginForm(request.form)
current_app.logger.info("Hello from the home page!")
# Handle logging in
if request.method == "POST":
if form.validate_on_submit():
login_user(form.user)
flash("You are logged in.", "success")
redirect_url = request.args.get("next") or url_for("user.members")
return redirect(redirect_url)
else:
flash_errors(form)
return render_template("public/home.html", form=form)
@blueprint.route("/logout/")
@login_required
def logout():
"""Logout."""
logout_user()
flash("You are logged out.", "info")
return redirect(url_for("public.home"))
@blueprint.route("/register/", methods=["GET", "POST"])
def register():
"""Register new user."""
form = RegisterForm(request.form)
if form.validate_on_submit():
User.create(
username=form.username.data,
email=form.email.data,
password=form.password.data,
active=True,
)
flash("Thank you for registering. You can now log in.", "success")
return redirect(url_for("public.home"))
else:
flash_errors(form)
return render_template("public/register.html", form=form)
@blueprint.route("/about/")
def about():
"""About page."""
form = LoginForm(request.form)
return render_template("public/about.html", form=form)

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""Application configuration.
Most configuration is set via environment variables.
For local development, use a .env file to set
environment variables.
"""
from environs import Env
env = Env()
env.read_env()
ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == "development"
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
SECRET_KEY = env.str("SECRET_KEY")
SEND_FILE_MAX_AGE_DEFAULT = env.int("SEND_FILE_MAX_AGE_DEFAULT")
BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
DEBUG_TB_ENABLED = DEBUG
DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block page_title %}Unauthorized{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>401</h1>
<p>You are not authorized to see this page. Please <a href="{{ url_for('public.home')}}">log in</a> or
<a href="{{ url_for('public.register') }}">create a new account</a>.
</p>
</div>
</div>
{% endblock %}

@ -0,0 +1,15 @@
{% extends "layout.html" %}
{% block page_title %}Page Not Found{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>404</h1>
<p>Sorry, that page doesn't exist.</p>
<p>Want to <a href="{{ url_for('public.home') }}">go home</a> instead?</p>
</div>
</div>
{% endblock %}

@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block page_title %}Server error{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>500</h1>
<p>Sorry, something went wrong on our system. Don't panic, we are fixing it! Please try again later.</p>
</div>
</div>
{% endblock %}

@ -0,0 +1,14 @@
<footer>
<small>
<ul class="company">
<li>© Yuri van Midden</li>
</ul>
<ul class="footer-nav">
<li><a href="{{ url_for('public.about') }}">About</a></li>
<li><a href="mailto:me@yurivanmidden.nl">Contact</a></li>
</ul>
</small>
</footer>

@ -0,0 +1,71 @@
<!DOCTYPE html>
<!-- paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ -->
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" lang="en">
<!--<![endif]-->
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="{{static_url_for('static', filename='build/img/favicon.ico') }}">
<title>
{% block page_title %}
Harambee LED bord
{% endblock %}
</title>
<meta
name="description"
content="{% block meta_description %}{% endblock %}"
/>
<meta name="author" content="{% block meta_author %}{% endblock %}" />
<!-- Mobile viewport optimized: h5bp.com/viewport -->
<meta name="viewport" content="width=device-width" />
<link
rel="stylesheet"
type="text/css"
href="{{ static_url_for('static', filename='build/main_css.bundle.css') }}"
/>
{% block css %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{% block body %} {% with form=form %} {% include "nav.html" %} {% endwith %}
<header>{% block header %}{% endblock %}</header>
<main role="main">
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %}
<div class="row">
<div class="col-md-12">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
<a class="close" title="Close" href="#" data-dismiss="alert"
>&times;</a
>
{{ message }}
</div>
<!-- end .alert -->
{% endfor %}
</div>
<!-- end col-md -->
</div>
<!-- end row -->
{% endif %} {% endwith %} {% block content %}{% endblock %}
</main>
{% include "footer.html" %}
<!-- JavaScript at the bottom for fast page loading -->
<script src="{{ static_url_for('static', filename='build/main_js.bundle.js') }}"></script>
{% block js %}{% endblock %}
<!-- end scripts -->
{% endblock %}
</body>
</html>

@ -0,0 +1,49 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="{{ url_for('public.home') }}">
Harambee LED bord
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="{{ url_for('public.home') }}">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('public.about') }}">About</a>
</li>
</ul>
{% if current_user and current_user.is_authenticated %}
<ul class="navbar-nav my-auto">
<li class="nav-item active">
<a class="nav-link" href="{{ url_for('user.members') }}">Logged in as {{ current_user.username }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('public.logout') }}">
<i class="fa fa-sign-out"></i>
</a>
</li>
</ul>
{% elif form %}
<form class="form-inline" id="loginForm" method="POST" action="/" role="login">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="input-group mb-2 mr-sm-2">
{{ form.username(placeholder="Username", class_="form-control mr-sm-1 my-auto") }} {{ form.password(placeholder="Password",
class_="form-control mr-sm-1 my-auto") }}
<button class="btn btn-light btn-primary m-auto" type="submit">Login</button>
</div>
</form>
<ul class="navbar-nav my-auto">
<li class="nav-item">
<a class="nav-link navbar-text" href="{{ url_for('public.register') }}">Create account</a>
</li>
</ul>
{% endif %}
</div><!-- /.navbar-collapse -->
</nav>

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<h1 class="mt-5">About</h1>
<div class="row">
<p class="lead">This template was created by <a href="http://github.com/sloria/">Steven Loria</a> for use with the <a href="http://github.com/audreyr/cookiecutter/">cookiecutter</a> package by <a href="http://github.com/audreyr/">Audrey Roy</a>.</p>
</div>
</div>
{% endblock %}

@ -0,0 +1,35 @@
{% extends "layout.html" %}
{% block content %}
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Welcome to Harambee LED bord</h1>
<p>This is a starter Flask template. It includes Bootstrap 4, jQuery 3, Flask-SQLAlchemy, WTForms, and various testing utilities out of the box.</p>
<p><a href="https://github.com/cookiecutter-flask/cookiecutter-flask" class="btn btn-primary btn-large">Learn more &raquo;</a></p>
</div>
</div><!-- /.jumbotron -->
<div class="container">
<div class="row">
<div class="col-lg-4">
<h2><i class="fa fa-code"></i> Bootstrap 4</h2>
<p>Sleek, intuitive, and powerful mobile-first front-end framework for faster and easier web development.</p>
<p><a class="btn btn-outline-secondary" href="http://getbootstrap.com/">Official website &raquo;</a></p>
</div>
<div class="col-lg-4">
<h2><i class="fa fa-flask"></i> SQLAlchemy</h2>
<p>SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.</p>
<p><a href="http://www.sqlalchemy.org/" class="btn btn-outline-secondary">Official website &raquo;</a></p>
</div>
<div class="col-lg-4">
<h2><i class="fa fa-edit"></i> WTForms</h2>
<p>WTForms is a flexible forms validation and rendering library for python web development.</p>
<p><a href="http://wtforms.simplecodes.com/" class="btn btn-outline-secondary">Official website &raquo;</a></p>
</div>
</div><!-- /.row -->
</div>
{% endblock %}

@ -0,0 +1,31 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-narrow">
<h1 class="mt-5">Register</h1>
<br/>
<form id="registerForm" class="form form-register" method="POST" action="" role="form">
{{ form.csrf_token }}
<div class="form-group">
{{form.username.label}}
{{form.username(placeholder="Username", class_="form-control")}}
</div>
<div class="form-group">
{{form.email.label}}
{{form.email(placeholder="Email", class_="form-control")}}
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
{{form.password.label}}
{{form.password(placeholder="Password", class_="form-control")}}
</div>
<div class="form-group">
{{form.confirm.label}}
{{form.confirm(placeholder="Password (again)", class_="form-control")}}
</div>
<p><input class="btn btn-primary" type="submit" value="Register"></p>
</form>
<p><em>Already registered?</em> Click <a href="/">here</a> to login.</p>
</div>
{% endblock %}

@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<h1>Welcome {{ current_user.username }}</h1>
<h3>This is the members-only page.</h3>
</div>
{% endblock %}

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""The user module."""
from . import views # noqa

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""User forms."""
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField
from wtforms.validators import DataRequired, Email, EqualTo, Length
from .models import User
class RegisterForm(FlaskForm):
"""Register form."""
username = StringField(
"Username", validators=[DataRequired(), Length(min=3, max=25)]
)
email = StringField(
"Email", validators=[DataRequired(), Email(), Length(min=6, max=40)]
)
password = PasswordField(
"Password", validators=[DataRequired(), Length(min=6, max=40)]
)
confirm = PasswordField(
"Verify password",
[DataRequired(), EqualTo("password", message="Passwords must match")],
)
def __init__(self, *args, **kwargs):
"""Create instance."""
super(RegisterForm, self).__init__(*args, **kwargs)
self.user = None
def validate(self):
"""Validate the form."""
initial_validation = super(RegisterForm, self).validate()
if not initial_validation:
return False
user = User.query.filter_by(username=self.username.data).first()
if user:
self.username.errors.append("Username already registered")
return False
user = User.query.filter_by(email=self.email.data).first()
if user:
self.email.errors.append("Email already registered")
return False
return True

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""User models."""
import datetime as dt
from flask_login import UserMixin
from harambee_led_bord.database import (
Column,
Model,
SurrogatePK,
db,
reference_col,
relationship,
)
from harambee_led_bord.extensions import bcrypt
class Role(SurrogatePK, Model):
"""A role for a user."""
__tablename__ = "roles"
name = Column(db.String(80), unique=True, nullable=False)
user_id = reference_col("users", nullable=True)
user = relationship("User", backref="roles")
def __init__(self, name, **kwargs):
"""Create instance."""
db.Model.__init__(self, name=name, **kwargs)
def __repr__(self):
"""Represent instance as a unique string."""
return f"<Role({self.name})>"
class User(UserMixin, SurrogatePK, Model):
"""A user of the app."""
__tablename__ = "users"
username = Column(db.String(80), unique=True, nullable=False)
email = Column(db.String(80), unique=True, nullable=False)
#: The hashed password
password = Column(db.LargeBinary(128), nullable=True)
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
first_name = Column(db.String(30), nullable=True)
last_name = Column(db.String(30), nullable=True)
active = Column(db.Boolean(), default=False)
is_admin = Column(db.Boolean(), default=False)
def __init__(self, username, email, password=None, **kwargs):
"""Create instance."""
db.Model.__init__(self, username=username, email=email, **kwargs)
if password:
self.set_password(password)
else:
self.password = None
def set_password(self, password):
"""Set password."""
self.password = bcrypt.generate_password_hash(password)
def check_password(self, value):
"""Check password."""
return bcrypt.check_password_hash(self.password, value)
@property
def full_name(self):
"""Full user name."""
return f"{self.first_name} {self.last_name}"
def __repr__(self):
"""Represent instance as a unique string."""
return f"<User({self.username!r})>"

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
"""User views."""
from flask import Blueprint, render_template
from flask_login import login_required
blueprint = Blueprint("user", __name__, url_prefix="/users", static_folder="../static")
@blueprint.route("/")
@login_required
def members():
"""List members."""
return render_template("users/members.html")

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""Helper utilities and decorators."""
from flask import flash
def flash_errors(form, category="warning"):
"""Flash all errors for a form."""
for field, errors in form.errors.items():
for error in errors:
flash(f"{getattr(form, field).label.text} - {error}", category)

@ -0,0 +1 @@
Generic single-database configuration.

@ -0,0 +1,45 @@
# 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
[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
[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,96 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
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
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').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 = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
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,50 @@
"""empty message
Revision ID: 6741bbbc111a
Revises:
Create Date: 2019-12-13 15:00:37.768224
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6741bbbc111a'
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=80), nullable=False),
sa.Column('email', sa.String(length=80), nullable=False),
sa.Column('password', sa.LargeBinary(length=128), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('first_name', sa.String(length=30), nullable=True),
sa.Column('last_name', sa.String(length=30), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('roles')
op.drop_table('users')
# ### end Alembic commands ###

@ -0,0 +1,52 @@
{
"name": "harambee_led_bord",
"version": "1.0.0",
"description": "Backend voor het Harambee led bord",
"scripts": {
"build": "NODE_ENV=production webpack --progress --colors -p && npm run flask-static-digest",
"start": "concurrently -n \"WEBPACK,FLASK\" -c \"bgBlue.bold,bgMagenta.bold\" \"npm run webpack-watch\" \"npm run flask-server\"",
"webpack-watch": "NODE_ENV=debug webpack --mode development --watch",
"flask-server": "pipenv run flask run --host=0.0.0.0",
"flask-static-digest": "pipenv run flask digest compile",
"lint": "eslint \"assets/js/*.js\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/yurivanmidden/harambee_led_bord.git"
},
"author": "Yuri van Midden",
"license": "MIT",
"engines": {
"node": ">=12"
},
"bugs": {
"url": "https://github.com/yurivanmidden/harambee_led_bord/issues"
},
"homepage": "https://github.com/yurivanmidden/harambee_led_bord#readme",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.9.0",
"bootstrap": "^4.3.1",
"font-awesome": "^4.7.0",
"jquery": "^3.4.1",
"popper.js": "^1.15.0"
},
"devDependencies": {
"@babel/core": "^7.4.5",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6",
"babel-preset-env": "^1.7.0",
"concurrently": "^5.0.0",
"css-loader": "^3.0.0",
"eslint": "^6.2.2",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.17.3",
"file-loader": "^5.0.2",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.8.0",
"raw-loader": "^4.0.0",
"url-loader": "^3.0.0",
"webpack": "^4.33.0",
"webpack-cli": "^3.3.2"
}
}

@ -0,0 +1,11 @@
[flake8]
ignore = D401,D202,E226,E302,E41
max-line-length=120
exclude = migrations/*
max-complexity = 10
[isort]
line_length=88
multi_line_output=3
skip=migrations/*
include_trailing_comma=true

@ -0,0 +1,7 @@
#!/usr/bin/env sh
function auto_pipenv_shell {
if [ -f "Pipfile" ] ; then
source "$(pipenv --venv)/bin/activate"
fi
}

@ -0,0 +1,12 @@
#!/usr/bin/env sh
set -e
npm run build
source ./shell_scripts/auto_pipenv.sh
auto_pipenv_shell
if [ $# -eq 0 ] || [ "${1#-}" != "$1" ]; then
set -- supervisord "$@"
fi
exec "$@"

@ -0,0 +1,21 @@
[unix_http_server]
file=/tmp/supervisor.sock ; path to your socket file
[supervisord]
logfile=/tmp/supervisord.log ; supervisord log file
logfile_maxbytes=50MB ; maximum size of logfile before rotation
logfile_backups=10 ; number of backed up logfiles
loglevel=%(ENV_LOG_LEVEL)s ; info, debug, warn, trace
pidfile=/tmp/supervisord.pid ; pidfile location
nodaemon=true ; run supervisord as a daemon
minfds=1024 ; number of startup file descriptors
minprocs=200 ; number of process descriptors
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
[include]
files = /etc/supervisor/conf.d/*.conf

@ -0,0 +1,18 @@
[program:gunicorn]
directory=/app
command=gunicorn
harambee_led_bord.app:create_app()
-b :5000
-w %(ENV_GUNICORN_WORKERS)s
-k gevent
--max-requests=5000
--max-requests-jitter=500
--log-level=%(ENV_LOG_LEVEL)s
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

@ -0,0 +1 @@
"""Tests for the app."""

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""Defines fixtures available to all tests."""
import logging
import pytest
from webtest import TestApp
from harambee_led_bord.app import create_app
from harambee_led_bord.database import db as _db
from .factories import UserFactory
@pytest.fixture
def app():
"""Create application for the tests."""
_app = create_app("tests.settings")
_app.logger.setLevel(logging.CRITICAL)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
@pytest.fixture
def testapp(app):
"""Create Webtest app."""
return TestApp(app)
@pytest.fixture
def db(app):
"""Create database for the tests."""
_db.app = app
with app.app_context():
_db.create_all()
yield _db
# Explicitly close DB connection
_db.session.close()
_db.drop_all()
@pytest.fixture
def user(db):
"""Create user for the tests."""
user = UserFactory(password="myprecious")
db.session.commit()
return user

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""Factories to help in tests."""
from factory import PostGenerationMethodCall, Sequence
from factory.alchemy import SQLAlchemyModelFactory
from harambee_led_bord.database import db
from harambee_led_bord.user.models import User
class BaseFactory(SQLAlchemyModelFactory):
"""Base factory."""
class Meta:
"""Factory configuration."""
abstract = True
sqlalchemy_session = db.session
class UserFactory(BaseFactory):
"""User factory."""
username = Sequence(lambda n: f"user{n}")
email = Sequence(lambda n: f"user{n}@example.com")
password = PostGenerationMethodCall("set_password", "example")
active = True
class Meta:
"""Factory configuration."""
model = User

@ -0,0 +1,12 @@
"""Settings module for test app."""
ENV = "development"
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite://"
SECRET_KEY = "not-so-secret-in-tests"
BCRYPT_LOG_ROUNDS = (
4 # For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds"
)
DEBUG_TB_ENABLED = False
CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False
WTF_CSRF_ENABLED = False # Allows form testing

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""Test forms."""
from harambee_led_bord.public.forms import LoginForm
from harambee_led_bord.user.forms import RegisterForm
class TestRegisterForm:
"""Register form."""
def test_validate_user_already_registered(self, user):
"""Enter username that is already registered."""
form = RegisterForm(
username=user.username,
email="foo@bar.com",
password="example",
confirm="example",
)
assert form.validate() is False
assert "Username already registered" in form.username.errors
def test_validate_email_already_registered(self, user):
"""Enter email that is already registered."""
form = RegisterForm(
username="unique", email=user.email, password="example", confirm="example"
)
assert form.validate() is False
assert "Email already registered" in form.email.errors
def test_validate_success(self, db):
"""Register with success."""
form = RegisterForm(
username="newusername",
email="new@test.test",
password="example",
confirm="example",
)
assert form.validate() is True
class TestLoginForm:
"""Login form."""
def test_validate_success(self, user):
"""Login successful."""
user.set_password("example")
user.save()
form = LoginForm(username=user.username, password="example")
assert form.validate() is True
assert form.user == user
def test_validate_unknown_username(self, db):
"""Unknown username."""
form = LoginForm(username="unknown", password="example")
assert form.validate() is False
assert "Unknown username" in form.username.errors
assert form.user is None
def test_validate_invalid_password(self, user):
"""Invalid password."""
user.set_password("example")
user.save()
form = LoginForm(username=user.username, password="wrongpassword")
assert form.validate() is False
assert "Invalid password" in form.password.errors
def test_validate_inactive_user(self, user):
"""Inactive user."""
user.active = False
user.set_password("example")
user.save()
# Correct username and password, but user is not activated
form = LoginForm(username=user.username, password="example")
assert form.validate() is False
assert "User not activated" in form.username.errors

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""Functional tests using WebTest.
See: http://webtest.readthedocs.org/
"""
from flask import url_for
from harambee_led_bord.user.models import User
from .factories import UserFactory
class TestLoggingIn:
"""Login."""
def test_can_log_in_returns_200(self, user, testapp):
"""Login successful."""
# Goes to homepage
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "myprecious"
# Submits
res = form.submit().follow()
assert res.status_code == 200
def test_sees_alert_on_log_out(self, user, testapp):
"""Show alert on logout."""
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "myprecious"
# Submits
res = form.submit().follow()
res = testapp.get(url_for("public.logout")).follow()
# sees alert
assert "You are logged out." in res
def test_sees_error_message_if_password_is_incorrect(self, user, testapp):
"""Show error if password is incorrect."""
# Goes to homepage
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "wrong"
# Submits
res = form.submit()
# sees error
assert "Invalid password" in res
def test_sees_error_message_if_username_doesnt_exist(self, user, testapp):
"""Show error if username doesn't exist."""
# Goes to homepage
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms["loginForm"]
form["username"] = "unknown"
form["password"] = "myprecious"
# Submits
res = form.submit()
# sees error
assert "Unknown user" in res
class TestRegistering:
"""Register a user."""
def test_can_register(self, user, testapp):
"""Register a new user."""
old_count = len(User.query.all())
# Goes to homepage
res = testapp.get("/")
# Clicks Create Account button
res = res.click("Create account")
# Fills out the form
form = res.forms["registerForm"]
form["username"] = "foobar"
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secret"
# Submits
res = form.submit().follow()
assert res.status_code == 200
# A new user was created
assert len(User.query.all()) == old_count + 1
def test_sees_error_message_if_passwords_dont_match(self, user, testapp):
"""Show error if passwords don't match."""
# Goes to registration page
res = testapp.get(url_for("public.register"))
# Fills out form, but passwords don't match
form = res.forms["registerForm"]
form["username"] = "foobar"
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secrets"
# Submits
res = form.submit()
# sees error message
assert "Passwords must match" in res
def test_sees_error_message_if_user_already_registered(self, user, testapp):
"""Show error if user already registered."""
user = UserFactory(active=True) # A registered user
user.save()
# Goes to registration page
res = testapp.get(url_for("public.register"))
# Fills out form, but username is already registered
form = res.forms["registerForm"]
form["username"] = user.username
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secret"
# Submits
res = form.submit()
# sees error
assert "Username already registered" in res

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""Model unit tests."""
import datetime as dt
import pytest
from harambee_led_bord.user.models import Role, User
from .factories import UserFactory
@pytest.mark.usefixtures("db")
class TestUser:
"""User tests."""
def test_get_by_id(self):
"""Get user by ID."""
user = User("foo", "foo@bar.com")
user.save()
retrieved = User.get_by_id(user.id)
assert retrieved == user
def test_created_at_defaults_to_datetime(self):
"""Test creation date."""
user = User(username="foo", email="foo@bar.com")
user.save()
assert bool(user.created_at)
assert isinstance(user.created_at, dt.datetime)
def test_password_is_nullable(self):
"""Test null password."""
user = User(username="foo", email="foo@bar.com")
user.save()
assert user.password is None
def test_factory(self, db):
"""Test user factory."""
user = UserFactory(password="myprecious")
db.session.commit()
assert bool(user.username)
assert bool(user.email)
assert bool(user.created_at)
assert user.is_admin is False
assert user.active is True
assert user.check_password("myprecious")
def test_check_password(self):
"""Check password."""
user = User.create(username="foo", email="foo@bar.com", password="foobarbaz123")
assert user.check_password("foobarbaz123") is True
assert user.check_password("barfoobaz") is False
def test_full_name(self):
"""User full name."""
user = UserFactory(first_name="Foo", last_name="Bar")
assert user.full_name == "Foo Bar"
def test_roles(self):
"""Add a role to a user."""
role = Role(name="admin")
role.save()
user = UserFactory()
user.roles.append(role)
user.save()
assert role in user.roles

@ -0,0 +1,82 @@
const path = require('path');
const webpack = require('webpack');
/*
* Webpack Plugins
*/
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProductionPlugins = [
// production webpack plugins go here
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production")
}
})
]
const debug = (process.env.NODE_ENV !== 'production');
const rootAssetPath = path.join(__dirname, 'assets');
module.exports = {
// configuration
context: __dirname,
entry: {
main_js: './assets/js/main',
main_css: [
path.join(__dirname, 'node_modules', 'font-awesome', 'css', 'font-awesome.css'),
path.join(__dirname, 'node_modules', 'bootstrap', 'dist', 'css', 'bootstrap.css'),
path.join(__dirname, 'assets', 'css', 'style.css'),
],
},
mode: debug,
output: {
chunkFilename: "[id].js",
filename: "[name].bundle.js",
path: path.join(__dirname, "harambee_led_bord", "static", "build"),
publicPath: "/static/build/"
},
resolve: {
extensions: [".js", ".jsx", ".css"]
},
devtool: debug ? "eval-source-map" : false,
plugins: [
new MiniCssExtractPlugin({ filename: "[name].bundle.css" }),
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery" })
].concat(debug ? [] : ProductionPlugins),
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: debug,
},
},
'css-loader!less-loader',
],
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: debug,
},
},
'css-loader',
],
},
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff' } },
{
test: /\.(ttf|eot|svg|png|jpe?g|gif|ico)(\?.*)?$/i,
loader: `file-loader?context=${rootAssetPath}&name=[path][name].[ext]`
},
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query: { presets: ['env'], cacheDirectory: true } },
],
}
};
Loading…
Cancel
Save