TG-103 Initial Flask app stucture
This commit is contained in:
parent
ee571061e2
commit
cdd13c4974
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# VirtualEnv
|
||||||
|
.Python
|
||||||
|
[Bb]in
|
||||||
|
[Ii]nclude
|
||||||
|
[Ll]ib
|
||||||
|
[Ll]ib64
|
||||||
|
[Ll]ocal
|
||||||
|
[Ss]cripts
|
||||||
|
pyvenv.cfg
|
||||||
|
.venv
|
||||||
|
*.egg
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
# sqLite
|
||||||
|
*.db
|
||||||
|
*.sqlite*
|
||||||
|
|
||||||
|
# Pycharm
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Tox
|
||||||
|
.tox
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
coverage.xml
|
18
backend/app/api/exampleapi/controller.py
Normal file
18
backend/app/api/exampleapi/controller.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
|
||||||
|
|
||||||
|
class SomeApi(Resource):
|
||||||
|
"""
|
||||||
|
Some Api Resource
|
||||||
|
"""
|
||||||
|
def post(self):
|
||||||
|
return {'somepost': 'somepostdata'}, 201
|
||||||
|
|
||||||
|
def get(self, id=None):
|
||||||
|
return {'someget': 'somegetdata'}, 200
|
||||||
|
|
||||||
|
def delete(self, id=None):
|
||||||
|
return {'somedelete': 'somedeletedata'}, 204
|
||||||
|
|
||||||
|
def put(self, id=None):
|
||||||
|
return {'someput': 'someputdata'}, 204
|
33
backend/app/api/exampleapi/model.py
Normal file
33
backend/app/api/exampleapi/model.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from passlib.apps import custom_app_context as pwd_context
|
||||||
|
|
||||||
|
from app.core import db
|
||||||
|
|
||||||
|
users_roles = db.Table('users_roles',
|
||||||
|
db.Column('user_id',
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('user.id')
|
||||||
|
),
|
||||||
|
db.Column('role_id',
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('role.id')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Role(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), index=True, unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(64), nullable=False)
|
||||||
|
roles = db.relationship('Role', secondary=users_roles)
|
||||||
|
|
||||||
|
def hash_password(self, password):
|
||||||
|
self.password_hash = pwd_context.encrypt(password)
|
||||||
|
|
||||||
|
def verify_password(self, password):
|
||||||
|
return pwd_context.verify(password, self.password_hash)
|
31
backend/app/config.py
Normal file
31
backend/app/config.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
DEBUG = False
|
||||||
|
TESTING = False
|
||||||
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
SQLALCHEMY_DATABASE_URI = \
|
||||||
|
'sqlite:///' + os.path.join(BASE_DIR, '../app.db')
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
THREADS_PER_PAGE = 2
|
||||||
|
SECRET_KEY = "secret"
|
||||||
|
BUNDLE_ERRORS = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
SESSION_VALIDITY_DURATION_WITHOUT_ACTIVITY_MIN = 20
|
||||||
|
|
||||||
|
|
||||||
|
class Prod(Config):
|
||||||
|
SQLALCHEMY_DATABASE_URI = 'mysql://user@localhost/foo'
|
||||||
|
|
||||||
|
|
||||||
|
class Debug(Config):
|
||||||
|
DEBUG = True
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
|
||||||
|
class Test(Config):
|
||||||
|
TESTING = True
|
||||||
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
SQLALCHEMY_DATABASE_URI = \
|
||||||
|
'sqlite:///' + os.path.join(BASE_DIR, '../test.db')
|
40
backend/app/core.py
Normal file
40
backend/app/core.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from flask import Flask, session
|
||||||
|
from flask_restful import Api
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def configure_app(config="prod"):
|
||||||
|
if config.lower() == "debug":
|
||||||
|
app.config.from_object('app.config.Debug')
|
||||||
|
elif config.lower() == "test":
|
||||||
|
app.config.from_object('app.config.Test')
|
||||||
|
else:
|
||||||
|
app.config.from_object('app.config.Prod')
|
||||||
|
|
||||||
|
app.permanent_session_lifetime = \
|
||||||
|
timedelta(
|
||||||
|
minutes=app.config
|
||||||
|
['SESSION_VALIDITY_DURATION_WITHOUT_ACTIVITY_MIN']
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
session.modified = True
|
||||||
|
|
||||||
|
|
||||||
|
# initialization Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
configure_app()
|
||||||
|
|
||||||
|
# SQLAlchemy
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
# RestFul Flask
|
||||||
|
api = Api(app)
|
||||||
|
|
||||||
|
# import api resources
|
||||||
|
importlib.import_module("app.urls")
|
4
backend/app/urls.py
Normal file
4
backend/app/urls.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from app.core import api
|
||||||
|
|
||||||
|
# Some Api resource
|
||||||
|
api.add_resource(api, '/api/someapi', '/api/someapi/<int:id>')
|
74
backend/manage.py
Normal file
74
backend/manage.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
|
from flask_script import Manager, Command
|
||||||
|
from flask_script import prompt_bool
|
||||||
|
|
||||||
|
from backend.app.core import app, db, configure_app
|
||||||
|
|
||||||
|
|
||||||
|
warnings.simplefilter('ignore')
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument("-d", "--debug", action="store_true")
|
||||||
|
group.add_argument("-t", "--test", action="store_true")
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
configure_app(config="debug")
|
||||||
|
if args.test:
|
||||||
|
configure_app(config="test")
|
||||||
|
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_option("-d", "--debug",
|
||||||
|
action="store_true", dest="debug", required=False)
|
||||||
|
manager.add_option("-t", "--test",
|
||||||
|
action="store_true", dest="test", required=False)
|
||||||
|
|
||||||
|
# migrations : python manage.py db to show usage
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
|
||||||
|
|
||||||
|
class SeedDB(Command):
|
||||||
|
"""Seed the db """
|
||||||
|
def run(self):
|
||||||
|
if args.test:
|
||||||
|
raise Exception("Test Database is seed in test case tear up !")
|
||||||
|
#some seed method
|
||||||
|
|
||||||
|
|
||||||
|
manager.add_command('seeddb', SeedDB())
|
||||||
|
|
||||||
|
|
||||||
|
class DropDB(Command):
|
||||||
|
"""drop db """
|
||||||
|
def run(self):
|
||||||
|
if prompt_bool("Are you sure you want to lose all your data"):
|
||||||
|
os.system("python manage.py -t db downgrade base")
|
||||||
|
|
||||||
|
|
||||||
|
manager.add_command('dropdb', DropDB())
|
||||||
|
|
||||||
|
|
||||||
|
class RunTests(Command):
|
||||||
|
"""Seed the db """
|
||||||
|
def run(self):
|
||||||
|
configure_app(config="test")
|
||||||
|
os.system("python manage.py -t db downgrade base")
|
||||||
|
os.system("python manage.py -t db upgrade")
|
||||||
|
test_loader = unittest.defaultTestLoader
|
||||||
|
test_runner = unittest.TextTestRunner()
|
||||||
|
test_suite = test_loader.discover('tests')
|
||||||
|
test_runner.run(test_suite)
|
||||||
|
|
||||||
|
|
||||||
|
manager.add_command('runtests', RunTests())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
7
backend/requirements/common.txt
Normal file
7
backend/requirements/common.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
flask < 0.13
|
||||||
|
flask-script < 2.1
|
||||||
|
flask-sqlalchemy < 2.2
|
||||||
|
flask-migrate < 2.1
|
||||||
|
flask-bootstrap < 3.4
|
||||||
|
flask-restful < 0.4
|
||||||
|
passlib < 1.8
|
4
backend/requirements/test.txt
Normal file
4
backend/requirements/test.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-r common.txt
|
||||||
|
tox
|
||||||
|
discover
|
||||||
|
coverage
|
170
backend/tests/api/example_test_someapi.py
Normal file
170
backend/tests/api/example_test_someapi.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
from flask import json
|
||||||
|
|
||||||
|
from backend.app.core import app
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTestCase(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = app.test_client()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def login(self, email, password):
|
||||||
|
return self.app.post('/api/auth',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=email,
|
||||||
|
password=password
|
||||||
|
)
|
||||||
|
), content_type='application/json')
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
return self.app.get('/api/auth')
|
||||||
|
|
||||||
|
def create_user(self, email, password, confirm):
|
||||||
|
return self.app.post('/api/user',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
confirm=confirm
|
||||||
|
)
|
||||||
|
), content_type='application/json')
|
||||||
|
|
||||||
|
def get_user(self, user_id=None):
|
||||||
|
if user_id:
|
||||||
|
return self.app.get('api/user/{}'.format(user_id))
|
||||||
|
return self.app.get('api/user')
|
||||||
|
|
||||||
|
def delete_user(self, user_id):
|
||||||
|
return self.app.delete('api/user/{}'.format(user_id))
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
return self.app.get('api/auth/status')
|
||||||
|
|
||||||
|
def test_login_logout(self):
|
||||||
|
rv = self.login('admin@admin.com', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 204, 'Login as admin Failed')
|
||||||
|
|
||||||
|
rv = self.get_status()
|
||||||
|
self.assertEqual(rv.status_code, 200,
|
||||||
|
'Status problem : should be auth')
|
||||||
|
|
||||||
|
rv = self.logout()
|
||||||
|
self.assertEqual(rv.status_code, 204, 'Logout Failed')
|
||||||
|
|
||||||
|
rv = self.login('adminx', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('Invalid email format', json.loads(rv.data)['message']['email'],
|
||||||
|
'Should return : invalid format email')
|
||||||
|
|
||||||
|
rv = self.login('admin@admin.com', 'default')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('invalid email/password', json.loads(rv.data)['message'],
|
||||||
|
'Login invalid password unexpected return')
|
||||||
|
|
||||||
|
rv = self.login('admin@admin.comx', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('invalid email/password', json.loads(rv.data)['message'],
|
||||||
|
'Login with invalid mail unexpected retutn')
|
||||||
|
|
||||||
|
def test_add_user(self):
|
||||||
|
rv = self.create_user('paul@paul.fr', 'superpassword', 'superpassword')
|
||||||
|
self.assertEqual(rv.status_code, 401,
|
||||||
|
'Not connected user shouldn\'t be allow to add user')
|
||||||
|
|
||||||
|
rv = self.login('admin@admin.com', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 204, 'Login as admin Failed')
|
||||||
|
|
||||||
|
rv = self.create_user('paulatpaul.fr', 'superpassword', 'superpassword')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('Invalid email format', json.loads(rv.data)['message']['email'],
|
||||||
|
'Should return : invalid format email')
|
||||||
|
|
||||||
|
rv = self.create_user('paul@paul.fr', 'super', 'super')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('Password minimum length 6', json.loads(rv.data)['message']['password'],
|
||||||
|
'Should return : Password minimum length 6')
|
||||||
|
|
||||||
|
rv = self.create_user('paul@paul.fr', 'superpassword', 'superpass')
|
||||||
|
self.assertEqual(rv.status_code, 401)
|
||||||
|
self.assertIn('Password and confirmation are not the same', json.loads(rv.data)['message']['password'],
|
||||||
|
'Should return : Password and confirmation are not the same')
|
||||||
|
|
||||||
|
rv = self.create_user('paul@paul.fr', 'superpassword', 'superpassword')
|
||||||
|
self.assertEqual(rv.status_code, 201,
|
||||||
|
'Add user failed')
|
||||||
|
|
||||||
|
rv = self.create_user('paul@paul.fr', 'superpassword', 'superpassword')
|
||||||
|
self.assertEqual(rv.status_code, 401,
|
||||||
|
'Should not can add a user with a email already in user')
|
||||||
|
self.assertIn('email already in use', json.loads(rv.data)['message'],
|
||||||
|
'Bad error message')
|
||||||
|
|
||||||
|
rv = self.login('paul@paul.fr', 'superpassword')
|
||||||
|
self.assertEqual(rv.status_code, 204,
|
||||||
|
'Can\' login with new user !')
|
||||||
|
|
||||||
|
rv = self.get_status()
|
||||||
|
self.assertEqual(rv.status_code, 200,
|
||||||
|
'Status problem : should be auth')
|
||||||
|
new_user_id = json.loads(rv.data)['id']
|
||||||
|
|
||||||
|
rv = self.get_user(new_user_id)
|
||||||
|
self.assertEqual(rv.status_code, 200,
|
||||||
|
'Can\'t get the new user')
|
||||||
|
self.assertEqual('paul@paul.fr', json.loads(rv.data)['email'],
|
||||||
|
'The new user email is invalid')
|
||||||
|
|
||||||
|
def test_get_user(self):
|
||||||
|
rv = self.get_user(user_id=1)
|
||||||
|
self.assertEqual(rv.status_code, 401,
|
||||||
|
'Not connected user shouldn\'t be allow to get user')
|
||||||
|
|
||||||
|
rv = self.login('admin@admin.com', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 204, 'Login as admin Failed')
|
||||||
|
|
||||||
|
rv = self.get_user()
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
users = json.loads(rv.data)['users']
|
||||||
|
results = User.query.all()
|
||||||
|
self.assertEqual(len(users), len(results))
|
||||||
|
|
||||||
|
for user, result in zip(users, results):
|
||||||
|
self.assertEqual(user['email'], result.email)
|
||||||
|
|
||||||
|
rv = self.get_user(user_id=1111111111)
|
||||||
|
self.assertEqual(rv.status_code, 404)
|
||||||
|
|
||||||
|
rv = self.get_user(user_id=1)
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
self.assertEqual(User.query.get(1).email, json.loads(rv.data)['email'])
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
rv = self.delete_user(user_id=2)
|
||||||
|
self.assertEqual(rv.status_code, 401,
|
||||||
|
'Not connected user shouldn\'t be allow to delte user')
|
||||||
|
|
||||||
|
rv = self.login('admin@admin.com', 'admin')
|
||||||
|
self.assertEqual(rv.status_code, 204, 'Login as admin Failed')
|
||||||
|
|
||||||
|
rv = self.delete_user(user_id=1111111111)
|
||||||
|
self.assertEqual(rv.status_code, 404)
|
||||||
|
|
||||||
|
rv = self.delete_user(user_id=2)
|
||||||
|
self.assertEqual(rv.status_code, 204)
|
||||||
|
self.assertIsNone(User.query.get(2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
14
backend/tox.ini
Normal file
14
backend/tox.ini
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[flake8]
|
||||||
|
exclude = .git, app/static, app/templates, tox.ini
|
||||||
|
|
||||||
|
[tox]
|
||||||
|
#envlist = py25,py26,py27
|
||||||
|
skipsdist = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
commands=
|
||||||
|
coverage run --source app/api --omit app/api/*/model.py manage.py runtests
|
||||||
|
coverage report -m
|
||||||
|
coverage xml
|
||||||
|
coverage html
|
||||||
|
deps=-r{toxinidir}/requirements/test.txt
|
Reference in New Issue
Block a user