Commit 6238dd47 authored by Romain Loth's avatar Romain Loth

cleaning up archives (essentials of the flask server are copied/translated in root dir)

parent 7859bb3e
*.sqlite
*.db
*.json
env/
logs/nginx/*
setup/regcomex_venv/*
archives_local/*
__pycache__/
web: gunicorn manage:app --log-file -
\ No newline at end of file
# app directory
This directory contains the Flask application code.
The code has been organized into the following directories:
# Code directories
models # Database Models and their Forms
startup # Application startup code and settings.py file
views # View functions
# Asset and Template directories
static # This subdirectory will be mapped to the "/static/" URL
templates # Jinja2 HTML template files
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__) # The WSGI compliant web application object
db = SQLAlchemy(app) # Setup Flask-SQLAlchemy
manager = Manager(app) # Setup Flask-Script
from reversal import ReverseProxied
app.wsgi_app = ReverseProxied(app.wsgi_app)
from app.startup.create_app import create_app
\ No newline at end of file
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.
from . import manager
from . import models
from . import views
# This file defines command line commands for manage.py
#
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
from app import manager
@manager.command
def init_db():
from app.startup.create_users import create_users
create_users()
\ No newline at end of file
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
from flask_user import UserMixin
from flask_user.forms import RegisterForm
from flask_wtf import Form
from wtforms import StringField, SubmitField, validators
from app import db
# Define the User data model. Make sure to add the flask_user.UserMixin !!
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
# User authentication information (required for Flask-User)
email = db.Column(db.Unicode(255), nullable=False, server_default=u'', unique=True)
confirmed_at = db.Column(db.DateTime())
password = db.Column(db.String(255), nullable=False, server_default='')
reset_password_token = db.Column(db.String(100), nullable=False, server_default='')
active = db.Column(db.Boolean(), nullable=False, server_default='0')
# User information
active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0')
first_name = db.Column(db.Unicode(50), nullable=False, server_default=u'')
last_name = db.Column(db.Unicode(50), nullable=False, server_default=u'')
# Relationships
roles = db.relationship('Role', secondary='users_roles',
backref=db.backref('users', lazy='dynamic'))
# Define the Role data model
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(50), nullable=False, server_default=u'', unique=True) # for @roles_accepted()
label = db.Column(db.Unicode(255), server_default=u'') # for display purposes
# Define the UserRoles association model
class UsersRoles(db.Model):
__tablename__ = 'users_roles'
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE'))
role_id = db.Column(db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE'))
# Define the User registration form
# It augments the Flask-User RegisterForm with additional fields
class MyRegisterForm(RegisterForm):
first_name = StringField('First name', validators=[
validators.DataRequired('First name is required')])
last_name = StringField('Last name', validators=[
validators.DataRequired('Last name is required')])
# Define the User profile form
class UserProfileForm(Form):
first_name = StringField('First name', validators=[
validators.DataRequired('First name is required')])
last_name = StringField('Last name', validators=[
validators.DataRequired('Last name is required')])
submit = SubmitField('Save')
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
from flask import redirect, render_template, render_template_string, Blueprint
from flask import request, url_for
from flask_user import current_user, login_required, roles_accepted
from app import app, db
from app.core.models import UserProfileForm
core_blueprint = Blueprint('core', __name__, url_prefix='/')
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper
def crossdomain(origin=None, methods=None, headers=None, max_age=21600, attach_to_all=True, automatic_options=True):
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, str):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, str):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()
def get_methods():
if methods is not None:
return methods
options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']
def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp
h = resp.headers
h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
if headers is not None:
h['Access-Control-Allow-Headers'] = headers
return resp
f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator
# The Home page is accessible to anyone
@core_blueprint.route('')
# @crossdomain(origin='*')
def home_page():
return render_template('core/home_page.html')
# The User page is accessible to authenticated users (users that have logged in)
@core_blueprint.route('user')
@login_required # Limits access to authenticated users
# @crossdomain(origin='*')
def user_page():
return render_template('core/user_page.html')
# The Admin page is accessible to users with the 'admin' role
@core_blueprint.route('admin')
@roles_accepted('admin') # Limits access to users with the 'admin' role
# @crossdomain(origin='*')
def admin_page():
return render_template('core/admin_page.html')
@core_blueprint.route('user/profile', methods=['GET', 'POST'])
@login_required
# @crossdomain(origin='*')
def user_profile_page():
# Initialize form
form = UserProfileForm(request.form, current_user)
# Process valid POST
if request.method == 'POST' and form.validate():
# Copy form fields to user_profile fields
form.populate_obj(current_user)
# Save user_profile
db.session.commit()
# Redirect to home page
return redirect(url_for('core.home_page'))
# Process GET or invalid POST
return render_template('core/user_profile_page.html',
form=form)
# Register blueprint
app.register_blueprint(core_blueprint)
import os
import json
# *****************************
# Environment specific settings
# *****************************
# The settings below can (and should) be over-ruled by OS environment variable settings
# Flask settings # Generated with: import os; os.urandom(24)
SECRET_KEY = '\xb9\x8d\xb5\xc2\xc4Q\xe7\x8ej\xe0\x05\xf3\xa3kp\x99l\xe7\xf2i\x00\xb1-\xcd'
# PLEASE USE A DIFFERENT KEY FOR PRODUCTION ENVIRONMENTS!
# SQLAlchemy settings
SQLALCHEMY_DATABASE_URI = 'sqlite:///../app.sqlite'
with open("credentials.json") as data_file:
C = json.load(data_file)
# Flask-Mail settings
MAIL_USERNAME = C["mail"]
MAIL_PASSWORD = C["pass"]
MAIL_DEFAULT_SENDER = '"CommunityExplorer" <noreply@communityexplorer.org>'
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 465
MAIL_USE_SSL = True
MAIL_USE_TLS = False
ADMINS = [
'"Admin One" <kaislean@gmail.com>',
]
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.
# Intentionally left empty
\ No newline at end of file
import os
# ***********************************
# Settings common to all environments
# ***********************************
# Application settings
APP_NAME = "AppName"
APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
# Flask settings
CSRF_ENABLED = True
# Flask-User settings
USER_APP_NAME = APP_NAME
USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password
USER_ENABLE_CHANGE_USERNAME = False # Allow users to change their username
USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email
USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords
USER_ENABLE_EMAIL = True # Register with Email
USER_ENABLE_REGISTRATION = True # Allow new users to register
USER_ENABLE_RETYPE_PASSWORD = True # Prompt for `retype password` in:
USER_ENABLE_USERNAME = False # Register and Login with username
USER_AFTER_LOGIN_ENDPOINT = 'core.user_page'
USER_AFTER_LOGOUT_ENDPOINT = 'core.home_page'
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
from flask_mail import Mail
from flask_migrate import Migrate, MigrateCommand
from flask_user import UserManager, SQLAlchemyAdapter
from flask_wtf.csrf import CsrfProtect
import os
from app import app, db, manager
@app.before_first_request
def initialize_app_on_first_request():
""" Create users and roles tables on first HTTP request """
from .create_users import create_users
create_users()
def create_app(extra_config_settings={}):
"""
Initialize Flask applicaton
"""
# ***** Initialize app config settings *****
# Read common settings from 'app/startup/common_settings.py' file
app.config.from_object('app.startup.common_settings')
# Read environment-specific settings from file defined by OS environment variable 'ENV_SETTINGS_FILE'
env_settings_file = os.environ.get('ENV_SETTINGS_FILE', 'env_settings_example.py')
app.config.from_pyfile(env_settings_file)
# Read extra config settings from function parameter 'extra_config_settings'
app.config.update(extra_config_settings) # Overwrite with 'extra_config_settings' parameter
if app.testing:
app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF checks while testing
# Setup Flask-Migrate
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
# Setup Flask-Mail
mail = Mail(app)
# Setup WTForms CsrfProtect
CsrfProtect(app)
# Define bootstrap_is_hidden_field for flask-bootstrap's bootstrap_wtf.html
from wtforms.fields import HiddenField
def is_hidden_field_filter(field):
return isinstance(field, HiddenField)
app.jinja_env.globals['bootstrap_is_hidden_field'] = is_hidden_field_filter
# Setup an error-logger to send emails to app.config.ADMINS
init_email_error_handler(app)
# Setup Flask-User to handle user account related forms
from app.core.models import User, MyRegisterForm
from app.core.views import user_profile_page
db_adapter = SQLAlchemyAdapter(db, User) # Setup the SQLAlchemy DB Adapter
user_manager = UserManager(db_adapter, app, # Init Flask-User and bind to app
register_form=MyRegisterForm, # using a custom register form with UserProfile fields
user_profile_view_function=user_profile_page,
)
# Load all blueprints with their manager commands, models and views
from app import core
return app
def init_email_error_handler(app):
"""
Initialize a logger to send emails on error-level messages.
Unhandled exceptions will now send an email message to app.config.ADMINS.
"""
if app.debug: return # Do not send error emails while developing
# Retrieve email settings from app.config
host = app.config['MAIL_SERVER']
port = app.config['MAIL_PORT']
from_addr = app.config['MAIL_DEFAULT_SENDER']
username = app.config['MAIL_USERNAME']
password = app.config['MAIL_PASSWORD']
secure = () if app.config.get('MAIL_USE_TLS') else None
# Retrieve app settings from app.config
to_addr_list = app.config['ADMINS']
subject = app.config.get('APP_SYSTEM_ERROR_SUBJECT_LINE', 'System Error')
# Setup an SMTP mail handler for error-level messages
import logging
from logging.handlers import SMTPHandler
mail_handler = SMTPHandler(
mailhost=(host, port), # Mail host and port
fromaddr=from_addr, # From address
toaddrs=to_addr_list, # To address
subject=subject, # Subject line
credentials=(username, password), # Credentials
secure=secure,
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
# Log errors using: app.logger.error('Some error message')
from datetime import datetime
from app import app, db
from app.core.models import User, Role
def create_users():
""" Create users when app starts """
from app.core.models import User, Role
# Create all tables
db.create_all()
# Adding roles
admin_role = find_or_create_role('admin', u'Admin')
# Add users
user = find_or_create_user(u'Admin', u'Example', u'admin@example.com', 'Password1', admin_role)
user = find_or_create_user(u'User', u'Example', u'user@example.com', 'Password1')
# Save to DB
db.session.commit()
def find_or_create_role(name, label):
""" Find existing role or create new role """
role = Role.query.filter(Role.name == name).first()
if not role:
role = Role(name=name, label=label)
db.session.add(role)
return role
def find_or_create_user(first_name, last_name, email, password, role=None):
""" Find existing user or create new user """
user = User.query.filter(User.email == email).first()
if not user:
user = User(email=email,
first_name=first_name,
last_name=last_name,
password=app.user_manager.hash_password(password),
active=True,
confirmed_at=datetime.utcnow())
if role:
user.roles.append(role)
db.session.add(user)
return user
/**** element styles ****/
hr { border-color: #cccccc; margin: 0px; }
/**** header, main and footer divs ****/
.header-title { font-size: 30px; }
/**** class-based style modifiers ****/
.no-margins { margin: 0px; }
.with-margins { margin: 10px; }
.col-centered { float: none; margin: 0 auto; }
/*! HTML5 Boilerplate v5.2.0 | MIT License | https://html5boilerplate.com/ */
/*
* What follows is the result of much research on cross-browser styling.
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
* Kroc Camen, and the H5BP dev community and team.
*/
/* ==========================================================================
Base styles: opinionated defaults
========================================================================== */
html {
color: #222;
font-size: 1em;
line-height: 1.4;
}
/*
* Remove text-shadow in selection highlight:
* https://twitter.com/miketaylr/status/12228805301
*
* These selection rule sets have to be separate.
* Customize the background color to match your design.
*/
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
/*
* A better looking default horizontal rule
*/
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}
/*
* Remove the gap between audio, canvas, iframes,
* images, videos and the bottom of their containers:
* https://github.com/h5bp/html5-boilerplate/issues/440
*/
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle;
}
/*
* Remove default fieldset styles.
*/
fieldset {
border: 0;
margin: 0;
padding: 0;
}
/*
* Allow only vertical resizing of textareas.
*/
textarea {
resize: vertical;
}
/* ==========================================================================
Browser Upgrade Prompt
========================================================================== */
.browserupgrade {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
/* ==========================================================================
Author's custom styles
========================================================================== */
/* ==========================================================================
Helper classes
========================================================================== */
/*
* Hide visually and from screen readers:
*/
.hidden {
display: none !important;
}
/*
* Hide only visually, but have it available for screen readers:
* http://snook.ca/archives/html_and_css/hiding-content-for-accessibility
*/
.visuallyhidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
/*
* Extends the .visuallyhidden class to allow the element
* to be focusable when navigated to via the keyboard:
* https://www.drupal.org/node/897638
*/
.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto;
}
/*
* Hide visually and from screen readers, but maintain layout
*/
.invisible {
visibility: hidden;
}
/*
* Clearfix: contain floats
*
* For modern browsers
* 1. The space content is one way to avoid an Opera bug when the
* `contenteditable` attribute is included anywhere else in the document.
* Otherwise it causes space to appear at the top and bottom of elements
* that receive the `clearfix` class.
* 2. The use of `table` rather than `block` is only necessary if using
* `:before` to contain the top-margins of child elements.
*/
.clearfix:before,
.clearfix:after {
content: " "; /* 1 */
display: table; /* 2 */
}
.clearfix:after {
clear: both;
}
/* ==========================================================================
EXAMPLE Media Queries for Responsive Design.
These examples override the primary ('mobile first') styles.
Modify as content requires.
========================================================================== */
@media only screen and (min-width: 35em) {
/* Style adjustments for viewports that meet the condition */
}
@media print,
(-webkit-min-device-pixel-ratio: 1.25),
(min-resolution: 1.25dppx),
(min-resolution: 120dpi) {
/* Style adjustments for high resolution devices */
}
/* ==========================================================================
Print styles.
Inlined to avoid the additional HTTP request:
http://www.phpied.com/delay-loading-your-print-css/
========================================================================== */
@media print {
*,
*:before,
*:after {
background: transparent !important;
color: #000 !important; /* Black prints faster:
http://www.sanbeiji.com/archives/953 */
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
/*
* Printing Tables:
* http://css-discuss.incutio.com/wiki/Printing_Tables
*/
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS and IE text size adjust after device orientation change,
* without disabling user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability of focused elements when they are also in an
* active/hover state.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
box-sizing: content-box; /* 2 */
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
// Avoid `console` errors in browsers that lack a console.
(function() {
var method;
var noop = function () {};
var methods = [
'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn'
];
var length = methods.length;
var console = (window.console = window.console || {});
while (length--) {
method = methods[length];
// Only stub undefined methods.
if (!console[method]) {
console[method] = noop;
}
}
}());
// Place any jQuery/helper plugins in here.
# This directory contains Jinja2 template files
This Flask application uses the Jinja2 templating engine to render
data into HTML files.
The template files are organized into the following directories:
common # Common base templates and macros
flask_user # Flask-User template files (register, login, etc.)
pages # Templates for Page objects
users # Templates for User objects
Flask-User makes use of standard template files that reside in
`PATH/TO/VIRTUALENV/lib/PYTHONVERSION/site-packages/flask_user/templates/flask_user/`.
These standard templates can be overruled by placing a copy in the `app/templates/flask_user/` directory.
{% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
{{ field(class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{%- endmacro %}
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
</label>
</div>
{%- endmacro %}
{% macro render_radio_field(field) -%}
{% for value, label, checked in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
{{ label }}
</label>
</div>
{% endfor %}
{%- endmacro %}
{% macro render_submit_field(field, label=None, tabindex=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
{#<button type="submit" class="form-control btn btn-default btn-primary">{{label}}</button>#}
<input type="submit" value="{{label}}"
{% if tabindex %}tabindex="{{ tabindex }}"{% endif %}
>
{%- endmacro %}
{% extends "common/page_base.html" %} {# common/page_base.html extends layout.html #}
{% block content %}
<h1>Admin page</h1>
<p>This page is accessible to authenticated users with the 'admin' role.</p>
{% endblock %}
\ No newline at end of file
{% extends "common/page_base.html" %} {# common/page_base.html extends layout.html #}
{% block content %}
<h1>Home page</h1>
<p>This page is accessible to any user (signed in or not)</p>
{% if not current_user.is_authenticated %}
<p>
To view the User page, you must
<a href="{{ url_for('user.login') }}">Sign in</a> or
<a href="{{ url_for('user.register') }}">Register</a>.
</p>
{% endif %}
<p>
Test users:<br/>
- user@example.com / Password1<br/>
- admin@example.com / Password1
</p>
{% endblock %}
{% extends "common/page_base.html" %} {# common/page_base.html extends layout.html #}
{% block content %}
<h1>User page</h1>
<p>This page is accessible to authenticated user (users that have signed in).</p>
{% endblock %}
\ No newline at end of file
{% extends "common/page_base.html" %} {# common/page_base.html extends layout.html #}
{% block content %}
<h1>User Profile</h1>
<p><a href="{{ url_for('user.change_password') }}">Change password</a></p>
{% from "common/form_macros.html" import render_field, render_submit_field %}
<form action="" method="POST" class="form" role="form">
<div class="row">
<div class="col-sm-6 col-md-5 col-lg-4">
{{ form.hidden_tag() }}
{{ render_field(form.first_name, tabindex=240) }}
{{ render_field(form.last_name, tabindex=250) }}
{{ render_submit_field(form.submit, tabindex=280) }}
</div>
</div>
</form>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block pre_content %}
<div class="container">
<div class="row">
<div class="col-sm-7 col-md-6 col-lg-5 col-centered">
{% endblock %}
{% block post_content %}
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends 'flask_user/public_base.html' %}
{% block content %}
{% from "flask_user/_macros.html" import render_field, render_submit_field %}
<h1>{%trans%}Register{%endtrans%}</h1>
<form action="" method="POST" novalidate formnovalidate class="form" role="form">
{{ form.hidden_tag() }}
{# Username or Email #}
{% set field = form.username if user_manager.enable_username else form.email %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{# Label on left, "Already registered? Sign in." on right #}
<div class="row">
<div class="col-xs-6">
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
</div>
<div class="col-xs-6 text-right">
{% if user_manager.enable_register %}
<a href="{{ url_for('user.login') }}" tabindex='290'>
{%trans%}Already registered? Sign in.{%endtrans%}</a>
{% endif %}
</div>
</div>
{{ field(class_='form-control', tabindex=210) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{% if user_manager.enable_email and user_manager.enable_username %}
{{ render_field(form.email, tabindex=220) }}
{% endif %}
{{ render_field(form.first_name, tabindex=240) }}
{{ render_field(form.last_name, tabindex=250) }}
{{ render_field(form.password, tabindex=260) }}
{% if user_manager.enable_retype_password %}
{{ render_field(form.retype_password, tabindex=270) }}
{% endif %}
{{ render_submit_field(form.submit, tabindex=280) }}
</form>
{% endblock %}
\ No newline at end of file
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>MyApp</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Optional apple-touch-icon
<link rel="apple-touch-icon" href="apple-touch-icon.png"> -->
<!-- Place favicon.ico in the root directory -->
<!-- HTML5 boilerplate CSS -->
<link rel="stylesheet" href="/static/html5boilerplate/css/normalize.css">
<link rel="stylesheet" href="/static/html5boilerplate/css/main.css">
<script src="/static/html5boilerplate/js/modernizr-2.8.3.min.js"></script>
<!-- Application specific CSS-->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/app/app.css">
</head>
<body>
{% block body %}
<!-- Application specific HTML -->
<div id="header-div" class="clearfix with-margins">
<div class="pull-left"><a href="{{ url_for('core.home_page') }}"><span class="header-title">MyApp</span></a></div>
<div class="pull-right">
{% if current_user.is_authenticated %}
<a href="{{ url_for('core.user_profile_page') }}">{{ current_user.first_name or current_user.user_auth.username }}</a>
&nbsp; | &nbsp;
<a href="{{ url_for('user.logout') }}">Sign out</a>
{% else %}
<a href="{{ url_for('user.login') }}">Sign in</a>
{% endif %}
</div>
</div>
<div class="with-margins">
<a href="{{ url_for('core.home_page') }}">Home</a>
&nbsp; | &nbsp; <a href="{{ url_for('core.user_page') }}">User</a>
&nbsp; | &nbsp; <a href="{{ url_for('core.admin_page') }}">Admin</a>
</div>
<hr class="no-margins"/>
<div id="main-div" class="with-margins">
{% block pre_content %}{% endblock %}
{# One-time system messages called Flash messages #}
{% block flash_messages %}
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
{% for category, message in messages %}
{% if category=='error' %}
{% set category='danger' %}
{% endif %}
<div class="alert alert-{{category}}">{{ message|safe }}</div>
{% endfor %}
{% endif %}
{%- endwith %}
{% endblock %}
{% block content %}{% endblock %}
{% block post_content %}{% endblock %}
</div>
<br/>
<hr class="no-margins"/>
<div id="footer-div" class="clearfix with-margins">
<div class="pull-left">MyApp v1.0</div>
<div class="pull-right">&copy; 2014 MyCorp</div>
</div>
<!-- Application specific JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<!-- HTML5 boilerplate JS -->
<script src="/static/html5boilerplate/js/main.js"></script>
<!-- Optional Google Analytics: change UA-XXXXX-X to be your site's ID.
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='https://www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X','auto');ga('send','pageview');
</script>
-->
{% endblock %}
</body>
</html>
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper
def crossdomain(origin=None, methods=None, headers=None,
max_age=21600, attach_to_all=True,
automatic_options=True):
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, str):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, str):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()
def get_methods():
if methods is not None:
return methods
options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']
def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp
h = resp.headers
h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
if headers is not None:
h['Access-Control-Allow-Headers'] = headers
return resp
f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator
\ No newline at end of file
# This file starts the WSGI web application.
# - Heroku starts gunicorn, which loads Procfile, which starts manage.py
# - Developers can run it from the command line: python runserver.py
from app import create_app
app = create_app()
# Start a development web server if executed from the command line
if __name__ == "__main__":
# Manage the command line parameters such as:
# - python manage.py runserver
# - python manage.py db
from app import manager
manager.run()
# migrations directory
This directory has been generated by Flask-Migrate and contains Alembic configuration files.
Alembic is a Database Schema Migration tool.
The 'versions' subdirectory contains the Database Schema Migration files.
Use `python app.py db` to see a list of Flask-Migrate commands.
See also [Flask-Migrate](flask-migrate.readthedocs.org) and [Alembic](alembic.readthedocs.org).
\ No newline at end of file
# 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
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# 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)
# 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'))
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)
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.
"""
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
**current_app.extensions['migrate'].configure_args)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""Initial version
Revision ID: 0001c8ac1a69
Revises: None
Create Date: 2015-07-14 17:46:32.620018
"""
# revision identifiers, used by Alembic.
revision = '0001c8ac1a69'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('role',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=True),
sa.Column('description', sa.String(length=255), server_default='', nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=50), nullable=False),
sa.Column('password', sa.String(length=255), server_default='', nullable=False),
sa.Column('reset_password_token', sa.String(length=100), server_default='', nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
sa.Column('is_active', sa.Boolean(), server_default='0', nullable=False),
sa.Column('first_name', sa.String(length=50), server_default='', nullable=False),
sa.Column('last_name', sa.String(length=50), server_default='', nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
)
op.create_table('user_roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_roles')
op.drop_table('user')
op.drop_table('role')
### end Alembic commands ###
# This file is used by pip to install required python packages
# Usage: pip install -r requirements.txt
# Heroku environment
gunicorn==19.3.0
psycopg2==2.5.4
# Flask Framework
Flask==0.10.1
# Flask Packages
Flask-Migrate==1.4.0
Flask-SQLAlchemy==2.0
Flask-User==0.6.3
Flask-WTF==0.12
# Development tools
pytest==2.7.2
pytest-flask==0.8.1
pytest-cov==1.8.1
#
# from reversal import ReverseProxied
# more: http://flask.pocoo.org/snippets/35/
class ReverseProxied(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name:
environ['SCRIPT_NAME'] = script_name
path_info = environ['PATH_INFO']
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.app(environ, start_response)
\ No newline at end of file
#!/bin/bash
py.test -s --cov-report term-missing --cov-config tests/.coveragerc --cov app tests/
\ No newline at end of file
#!/bin/bash
# Run a Flask development server
# --debug # Run it in DEBUG mode.
# --reload # Restart when a python file changed
python manage.py runserver --debug --reload
#!/bin/bash
py.test -s tests/
\ No newline at end of file
# tests directory
This directory contains all the automated tests for the test tool `py.test`.
**`.coverage`**: Configuration file for the Python coverage tool `coverage`.
**`conftest.py`**: Defines fixtures for py.test.
**`test_*`**: py.test will load any file that starts with the name `test_`
and run any function that starts with the name `test_`.
## Testing the app
# Run all the automated tests in the tests/ directory
./runtests.sh # will run "py.test -s tests/"
## Generating a test coverage report
# Run tests and show a test coverage report
./runcoverage.sh # will run py.test with coverage options
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.
# Intentionally left empty
\ No newline at end of file
# This file contains pytest 'fixtures'.
# If a test functions specifies the name of a fixture function as a parameter,
# the fixture function is called and its result is passed to the test function.
#
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
import pytest
from app import create_app, db as the_db
# Initialize the Flask-App with test-specific settings
the_app = create_app(dict(
TESTING=True, # Propagate exceptions
LOGIN_DISABLED=False, # Enable @register_required
MAIL_SUPPRESS_SEND=True, # Disable Flask-Mail send
SERVER_NAME='localhost', # Enable url_for() without request context
SQLALCHEMY_DATABASE_URI='sqlite:///:memory:', # In-memory SQLite DB
WTF_CSRF_ENABLED=False, # Disable CSRF form validation
))
# Create and populate roles and users tables
from app.startup.create_users import create_users
create_users()
# Setup an application context (since the tests run outside of the webserver context)
the_app.app_context().push()
@pytest.fixture(scope='session')
def app():
return the_app
@pytest.fixture(scope='session')
def db():
"""
Initializes and returns a SQLAlchemy DB object
"""
return the_db
# Copyright 2014 SolidBuilds.com. All rights reserved
#
# Authors: Ling Thio <ling.thio@gmail.com>
from __future__ import print_function # Use print() instead of print
from flask import url_for
def test_page_urls(client):
# Visit home page
response = client.get(url_for('core.home_page'))
assert b'<h1>Home page</h1>' in response.data
# Login as user and visit User page
response = client.post(url_for('user.login'), follow_redirects=True,
data=dict(email='user@example.com', password='Password1'))
assert b'<h1>Home page</h1>' in response.data
response = client.get(url_for('core.user_page'))
assert b'<h1>User page</h1>' in response.data
# Edit User Profile page
response = client.get(url_for('core.user_profile_page'))
assert b'<h1>User Profile</h1>' in response.data
response = client.post(url_for('core.user_profile_page'), follow_redirects=True,
data=dict(first_name='User', last_name='User'))
response = client.get(url_for('core.user_page'))
assert b'<h1>User page</h1>' in response.data
# Logout
response = client.get(url_for('user.logout'), follow_redirects=True)
assert b'<h1>Home page</h1>' in response.data
# Login as admin and visit Admin page
response = client.post(url_for('user.login'), follow_redirects=True,
data=dict(email='admin@example.com', password='Password1'))
assert b'<h1>Home page</h1>' in response.data
response = client.get(url_for('core.admin_page'))
assert b'<h1>Admin page</h1>' in response.data
# Logout
response = client.get(url_for('user.logout'), follow_redirects=True)
assert b'<h1>Home page</h1>' in response.data
[tox]
# Test on the following Python versions
envlist = py27, py33, py34
toxworkdir=../builds/flask_user_starter_app/tox
skipsdist=True
[testenv]
deps = -r{toxinidir}/requirements.txt
# Run automated test suite
commands=
./runtests.sh
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment