From 342597643dbe3591b643f56ab29084e1885d9aec Mon Sep 17 00:00:00 2001 From: Mikhail Kashkin Date: Fri, 11 Jul 2014 18:31:02 +0300 Subject: [PATCH] Flask project base structure --- .gitignore | 17 ++ Dockerfile | 19 ++ Makefile | 30 +++ README.md | 3 + babel.cfg | 3 + local.example.cfg | 26 +++ manage.py | 42 +++++ migrations/README | 1 + migrations/alembic.ini | 48 +++++ migrations/env.py | 90 +++++++++ migrations/script.py.mako | 22 +++ packages.txt | 16 ++ project/__init__.py | 1 + project/api/__init__.py | 3 + project/api/views.py | 15 ++ project/app.py | 178 ++++++++++++++++++ project/auth/__init__.py | 3 + project/auth/forms.py | 37 ++++ project/auth/models.py | 44 +++++ project/auth/templates/auth/index.html | 17 ++ project/auth/templates/auth/macros.html | 25 +++ project/auth/templates/auth/profile.html | 34 ++++ project/auth/templates/auth/settings.html | 32 ++++ project/auth/views.py | 59 ++++++ project/config.py | 48 +++++ project/docs/index.md | 5 + project/extensions.py | 26 +++ project/frontend/__init__.py | 1 + .../frontend/templates/frontend/splash.html | 8 + .../templates/frontend/user_profile.html | 14 ++ project/frontend/views.py | 79 ++++++++ project/models.py | 3 + project/templates/base.html | 64 +++++++ project/templates/counter.html | 12 ++ project/templates/macros.html | 18 ++ project/templates/misc/403.html | 7 + project/templates/misc/404.html | 7 + project/templates/misc/405.html | 7 + project/templates/misc/500.html | 7 + project/templates/misc/base.html | 10 + project/templates/nav.html | 28 +++ project/templates/page.html | 9 + project/utils.py | 41 ++++ requirements.txt | 39 ++++ setup.py | 23 +++ static/.bowerrc | 3 + static/.gitattributes | 1 + static/bower.json | 10 + static/code.js | 3 + static/favicon.png | Bin 0 -> 552 bytes static/robots.txt | 2 + static/style.css | 2 + 52 files changed, 1242 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 babel.cfg create mode 100644 local.example.cfg create mode 100644 manage.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 packages.txt create mode 100644 project/__init__.py create mode 100644 project/api/__init__.py create mode 100644 project/api/views.py create mode 100644 project/app.py create mode 100644 project/auth/__init__.py create mode 100644 project/auth/forms.py create mode 100644 project/auth/models.py create mode 100644 project/auth/templates/auth/index.html create mode 100644 project/auth/templates/auth/macros.html create mode 100644 project/auth/templates/auth/profile.html create mode 100644 project/auth/templates/auth/settings.html create mode 100644 project/auth/views.py create mode 100644 project/config.py create mode 100644 project/docs/index.md create mode 100644 project/extensions.py create mode 100644 project/frontend/__init__.py create mode 100644 project/frontend/templates/frontend/splash.html create mode 100644 project/frontend/templates/frontend/user_profile.html create mode 100644 project/frontend/views.py create mode 100644 project/models.py create mode 100644 project/templates/base.html create mode 100644 project/templates/counter.html create mode 100644 project/templates/macros.html create mode 100644 project/templates/misc/403.html create mode 100644 project/templates/misc/404.html create mode 100644 project/templates/misc/405.html create mode 100644 project/templates/misc/500.html create mode 100644 project/templates/misc/base.html create mode 100644 project/templates/nav.html create mode 100644 project/templates/page.html create mode 100644 project/utils.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 static/.bowerrc create mode 100644 static/.gitattributes create mode 100644 static/bower.json create mode 100644 static/code.js create mode 100644 static/favicon.png create mode 100644 static/robots.txt create mode 100644 static/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7da849 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.py[co] +__pycache__ + +# Translations compiled files +*.mo + +# Local development ignores +venv +data.sqlite + +# Production config +local.cfg + +# Static +static/libs/* + +# Other stuff here diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bc061e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Fresh installation test +# +# To run this: sudo docker build . + +FROM stackbrew/ubuntu:saucy +MAINTAINER Flask developers + +RUN apt-get update + +ADD ./ /code/ + +ENV DEBIAN_FRONTEND noninteractive +RUN cat /code/packages.txt | xargs apt-get -y --force-yes install +RUN npm install -g bower +RUN ldconfig + +RUN cd /code/ && make setup + +RUN cd /code/src/ && python manage.py test \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..42ca7f7 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +all: setup + +venv/bin/activate: + virtualenv-2.7 venv + +run: venv/bin/activate requirements.txt + . venv/bin/activate; python manage.py runserver + +static/bower.json: + cd static && bower install + +setup: venv/bin/activate requirements.txt static/bower.json + . venv/bin/activate; pip install -Ur requirements.txt + +init: venv/bin/activate requirements.txt + . venv/bin/activate; python manage.py init + +babel: venv/bin/activate + . venv/bin/activate; pybabel extract -F babel.cfg -o project/translations/messages.pot project + +# lazy babel scan +lazybabel: venv/bin/activate + . venv/bin/activate; pybabel extract -F babel.cfg -k lazy_gettext -o project/translations/messages.pot project + +# run $LANG=ru +addlang: venv/bin/activate + . venv/bin/activate; pybabel init -i project/translations/messages.pot -d project/translations -l $(LANG) + +updlang: venv/bin/activate + . venv/bin/activate; pybabel update -i project/translations/messages.pot -d project/translations diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f66ef9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Flask project template + +Flask project template. diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..aab7bff --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/local.example.cfg b/local.example.cfg new file mode 100644 index 0000000..2e4e3f6 --- /dev/null +++ b/local.example.cfg @@ -0,0 +1,26 @@ +# production mode, yopta +DEBUG = False + +SQLALCHEMY_ECHO = True +SQLALCHEMY_RECORD_QUERIES = False + +# this is example deployment configuration + +# run python and execute this code +# >>> import os; os.urandom(24) +# value place here as result + +# SECRET_KEY = '' + +# BROKER_URL = "mongodb://localhost" +# CELERY_RESULT_BACKEND = 'mongodb' +# CELERY_IMPORTS = ('backend.tasks',) +# CELERY_MONGODB_BACKEND_SETTINGS = { +# 'host': 'localhost', +# 'port': 27017, +# 'database': 'digdata_celery', +# #'user': user, +# #'password': password, +# 'taskmeta_collection': 'teskmeta' +# } +# CELERY_ANNOTATIONS = {"*": {"rate_limit": "10/s"}} diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..5a38229 --- /dev/null +++ b/manage.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from flask import json + +from flask.ext.script import Manager +from flask.ext.migrate import MigrateCommand + +from project import create_app +from project.extensions import db + +from sqlalchemy.exc import OperationalError +from sqlalchemy.ext.serializer import dumps, loads + +manager = Manager(create_app) +# Add Flask-Migrate commands under `db` prefix, for example: +# $ python manage.py db init +# +# $ python manage.py db migrate +# + +manager.add_command('db', MigrateCommand) + + +@manager.command +def init(): + """Run in local machine.""" + syncdb() + + +@manager.command +def syncdb(): + """Init/reset database.""" + db.drop_all() + db.create_all() + + +manager.add_option('-c', '--config', dest="config", required=False, + help="config file") + +if __name__ == "__main__": + manager.run() diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..e8f9fc4 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,48 @@ +# 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 + +[alembic:exclude] +tables = \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..321264f --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,90 @@ +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 exclude_tables_from_config(config_): + tables_ = config_.get("tables", None) + if tables_ is not None: + tables = tables_.split(",") + return tables + +exclude_tables = exclude_tables_from_config(config.get_section('alembic:exclude')) + + +def include_object(object, name, type_, reflected, compare_to): + if type_ == "table" and name in exclude_tables: + return False + else: + return True + + +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 + ) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..9570201 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${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"} diff --git a/packages.txt b/packages.txt new file mode 100644 index 0000000..5ebfadb --- /dev/null +++ b/packages.txt @@ -0,0 +1,16 @@ +build-essential +python +python-software-properties +python-pip +python-setuptools +python-dev +python-virtualenv +python-pip +python-psycopg2 +postgresql +curl +git +git-core +nodejs +openssl +libssl-dev \ No newline at end of file diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..819ccf0 --- /dev/null +++ b/project/__init__.py @@ -0,0 +1 @@ +from .app import create_app diff --git a/project/api/__init__.py b/project/api/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/project/api/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/project/api/views.py b/project/api/views.py new file mode 100644 index 0000000..baf7822 --- /dev/null +++ b/project/api/views.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from flask import (Blueprint, render_template, current_app) + +from ..extensions import manager +# from ..models import MyModel + + +def initialize_api(): + # List all Flask-Restless APIs here + # model_api = manager.create_api(MyModel, methods=['GET']) + pass + + +api = Blueprint('api', __name__, url_prefix='/api') diff --git a/project/app.py b/project/app.py new file mode 100644 index 0000000..78d2482 --- /dev/null +++ b/project/app.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +#pylint:disable-msg=W0612 + +import os +from flask import Flask, request, render_template, g +from flask.ext import login + +from .extensions import (db, mail, pages, manager, login_manager, babel, + migrate, csrf) + +# blueprints +from .frontend import frontend +from .auth import auth +from .api import api, initialize_api + +from social.apps.flask_app.routes import social_auth +from social.apps.flask_app.models import init_social +from social.apps.flask_app.template_filters import backends + +__all__ = ['create_app'] + +BLUEPRINTS = ( + frontend, + social_auth, + auth, + api +) + + +def create_app(config=None, app_name='project', blueprints=None): + app = Flask(app_name, + static_folder=os.path.join(os.path.dirname(__file__), '..', 'static'), + template_folder="templates" + ) + + app.config.from_object('vm.config') + app.config.from_pyfile('../local.cfg', silent=True) + if config: + app.config.from_pyfile(config) + + if blueprints is None: + blueprints = BLUEPRINTS + + blueprints_fabrics(app, blueprints) + extensions_fabrics(app) + api_fabrics() # this must be called after extensions_fabrics + configure_logging(app) + + error_pages(app) + gvars(app) + + return app + + +def blueprints_fabrics(app, blueprints): + """Configure blueprints in views.""" + + for blueprint in blueprints: + app.register_blueprint(blueprint) + + +def extensions_fabrics(app): + db.init_app(app) + mail.init_app(app) + babel.init_app(app) + pages.init_app(app) + init_social(app, db) + login_manager.init_app(app) + manager.init_app(app, db) + migrate.init_app(app, db) + + csrf.init_app(app) + + +def api_fabrics(): + initialize_api() + + +def error_pages(app): + # HTTP error pages definitions + + @app.errorhandler(403) + def forbidden_page(error): + return render_template("misc/403.html"), 403 + + @app.errorhandler(404) + def page_not_found(error): + return render_template("misc/404.html"), 404 + + @app.errorhandler(405) + def method_not_allowed(error): + return render_template("misc/405.html"), 404 + + @app.errorhandler(500) + def server_error_page(error): + return render_template("misc/500.html"), 500 + + +def gvars(app): + @app.before_request + def gdebug(): + if app.debug: + g.debug = True + else: + g.debug = False + + app.context_processor(backends) + + from .models import User + + @login_manager.user_loader + def load_user(userid): + try: + return User.query.get(int(userid)) + except (TypeError, ValueError): + pass + + @app.before_request + def guser(): + g.user = login.current_user + + @app.context_processor + def inject_user(): + try: + return {'user': g.user} + except AttributeError: + return {'user': None} + + @babel.localeselector + def get_locale(): + if g.user: + if hasattr(g.user, 'ui_lang'): + return g.user.ui_lang + + accept_languages = app.config.get('ACCEPT_LANGUAGES') + return request.accept_languages.best_match(accept_languages) + + +def configure_logging(app): + """Configure file(info) and email(error) logging.""" + + if app.debug or app.testing: + # Skip debug and test mode. Just check standard output. + return + + import logging + from logging.handlers import SMTPHandler + + # Set info level on logger, which might be overwritten by handers. + # Suppress DEBUG messages. + app.logger.setLevel(logging.INFO) + + info_log = os.path.join(app.config['LOG_FOLDER'], 'info.log') + info_file_handler = logging.handlers.RotatingFileHandler(info_log, maxBytes=100000, backupCount=10) + info_file_handler.setLevel(logging.INFO) + info_file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]') + ) + app.logger.addHandler(info_file_handler) + + # Testing + #app.logger.info("testing info.") + #app.logger.warn("testing warn.") + #app.logger.error("testing error.") + + mail_handler = SMTPHandler(app.config['MAIL_SERVER'], + app.config['MAIL_USERNAME'], + app.config['ADMINS'], + 'O_ops... %s failed!' % app.config['PROJECT'], + (app.config['MAIL_USERNAME'], + app.config['MAIL_PASSWORD'])) + mail_handler.setLevel(logging.ERROR) + mail_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]') + ) + app.logger.addHandler(mail_handler) diff --git a/project/auth/__init__.py b/project/auth/__init__.py new file mode 100644 index 0000000..facdd37 --- /dev/null +++ b/project/auth/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import auth diff --git a/project/auth/forms.py b/project/auth/forms.py new file mode 100644 index 0000000..9d31b70 --- /dev/null +++ b/project/auth/forms.py @@ -0,0 +1,37 @@ +import re +from flask.ext.wtf import Form +from flask.ext.babel import lazy_gettext + +from wtforms.fields import TextField +from wtforms.fields.html5 import URLField +from wtforms.validators import url, length, regexp, optional +from ..widgets import Select2Field, Select2MultipleField + + +class SettingsForm(Form): + """docstring for SettingsForm""" + + ui_lang = Select2Field( + label=lazy_gettext("Primary site language"), + description=lazy_gettext("Site will try to show UI labels using this " + + "language. User data will be shown in original languages."), + ) + url = URLField( + label=lazy_gettext("Personal site URL"), + description=lazy_gettext("If you have personal site and want to share " + + "with other people, please fill this field"), + validators=[optional(), url(message=lazy_gettext("Invalid URL."))]) + username = TextField( + label=lazy_gettext("Public profile address"), + description=lazy_gettext("Will be part of your public profile URL. Can " + + "be from 2 up to 40 characters length, can start start from [a-z] " + + "and contains only latin [0-9a-zA-Z] chars."), + validators=[ + length(2, 40, message=lazy_gettext("Field must be between 2 and 40" + + " characters long.")), + regexp(r"[a-zA-Z]{1}[0-9a-zA-Z]*", + re.IGNORECASE, + message=lazy_gettext("Username should start from [a-z] and " + + "contains only latin [0-9a-zA-Z] chars")) + ] + ) diff --git a/project/auth/models.py b/project/auth/models.py new file mode 100644 index 0000000..52d8675 --- /dev/null +++ b/project/auth/models.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from hashlib import md5 +from ..extensions import db +from flask.ext.login import UserMixin + + +class User(db.Model, UserMixin): + __tablename__ = 'users' + # tables name convention is hard topick, usually I'm against plural forms + # when name tables, but user is reserved word in post databases, + # so this is only case when it is allowed to use plural in my teams. + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(200), unique=True) + password = db.Column(db.String(200), default='') + name = db.Column(db.String(100)) + email = db.Column(db.String(200)) + active = db.Column(db.Boolean, default=True) + ui_lang = db.Column(db.String(2), default='en') + url = db.Column(db.String(200)) + + def gavatar(self, size=14): + if self.email: + return 'http://www.gravatar.com/avatar/{hashd}?d=mm&s={size}'.format( + hashd=md5(self.email).hexdigest(), size=str(size)) + else: + return None + + def is_active(self): + return self.active + + def get_access_token(self, provider, param_name='access_token'): + """ Method can be used for social network API access. + + >>> import requests + >>> user = User.query.one() + >>> r = requests.get('https://api.github.com/user', + ... params={'access_token': user.get_access_token('github')}) + >>> r.json()['html_url'] + u'https://github.com/xen' + + """ + # provider should be from social providers list, for example 'github' + s = self.social_auth.filter_by(provider=provider).one() + return s.extra_data.get(param_name, None) diff --git a/project/auth/templates/auth/index.html b/project/auth/templates/auth/index.html new file mode 100644 index 0000000..a14748c --- /dev/null +++ b/project/auth/templates/auth/index.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% set page_title = _('Login') %} + +{% block content %} +

{{ _("Social login") }}

+

{{ _("Login using your favorite social service ")}}

+{% from 'auth/macros.html' import login_button with context %} +
+{{ login_button(provider='google-oauth2') }} +{{ login_button(provider='twitter') }} +{{ login_button(provider='facebook') }} +{{ login_button(provider='yahoo-oauth') }} +{{ login_button(provider='github') }} +
+ +{% endblock %} + diff --git a/project/auth/templates/auth/macros.html b/project/auth/templates/auth/macros.html new file mode 100644 index 0000000..eb3128f --- /dev/null +++ b/project/auth/templates/auth/macros.html @@ -0,0 +1,25 @@ +{% macro login_button(provider='') -%} +{% if provider == 'google-oauth2' %} + Google +{% elif provider == 'github' %} + Github +{% else %} +{{ provider }} +{% endif %} +{% endmacro -%} + + +{% macro logout_button(provider='', id=None) -%} +
+ +
+ +{% endmacro -%} + diff --git a/project/auth/templates/auth/profile.html b/project/auth/templates/auth/profile.html new file mode 100644 index 0000000..653bf4c --- /dev/null +++ b/project/auth/templates/auth/profile.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% set page_title = _('Your profile') %} + +{% block content %} +{% from 'auth/macros.html' import subnav with context%} +{{ subnav('profile') }} + +

{{ _("Social profiles") }}

+ +

{{ _("You are logged in as %(user)s!", user = user.username) }}!

+ +

{{ _("Associate to this profile: ") }}

+ +{% from 'auth/macros.html' import login_button with context%} +

{{ _("For your convenience, add new social services to your account")}}

+
+{% for name in backends.not_associated %} +{{ login_button(provider=name) }} +{% else %} +

{{ _("Wee! You connected all available social services!") }}

+{% endfor %} +
+ +

{{ _("Associated: ") }}

+

{{ _("If you want to disconnect any service from the list then click item from the list") }}

+{% from 'auth/macros.html' import logout_button with context%} +
+{% for assoc in backends.associated %} +{{ logout_button(provider=assoc.provider, id=assoc.id) }} +{% endfor %} +
+ + +{% endblock %} diff --git a/project/auth/templates/auth/settings.html b/project/auth/templates/auth/settings.html new file mode 100644 index 0000000..f9f9edd --- /dev/null +++ b/project/auth/templates/auth/settings.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% set page_title = _('Settings') %} + +{% block content %} +{% from 'auth/macros.html' import subnav with context%} +{{ subnav('settings') }} + +

{{ _("Personal settings") }}

+ +{% from 'macros.html' import render_bootstrap_field with context%} + +
+ + {{ form.csrf_token }} + {{ render_bootstrap_field(form.ui_lang) }} + {{ render_bootstrap_field(form.url) }} + {{ render_bootstrap_field(form.username) }} + {{ render_bootstrap_field(form.citizen) }} + + + +
+
+ +
+
+ +
+ +{% endblock %} diff --git a/project/auth/views.py b/project/auth/views.py new file mode 100644 index 0000000..afec676 --- /dev/null +++ b/project/auth/views.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from flask import Blueprint, render_template, redirect, request, current_app, g, flash, url_for +from flask.ext.login import login_required, logout_user +from flask.ext.babel import gettext as _ +from .models import User +from ..extensions import db +from .forms import SettingsForm + +auth = Blueprint('auth', __name__, url_prefix='/auth/', template_folder="templates") + + +@auth.route('login') +def login(): + next_url = request.args.get('next') or request.referrer or None + return render_template('auth/index.html', next=next_url) + + +@auth.route('loggedin') +def loggedin(): + return redirect(request.args.get('next') or url_for('frontend.index')) + + +@auth.route('profile') +@login_required +def profile(): + return render_template('auth/profile.html') + + +@auth.route('set_lang') +@login_required +def set_lang(): + if request.args.get('lang') in current_app.config['LANGUAGES']: + user = User.query.get_or_404(g.user.id) + user.ui_lang = request.args.get('lang') + db.session.add(user) + db.session.commit() + return redirect('/') + + +@auth.route('settings', methods=['GET', 'POST']) +@login_required +def settings(): + form = SettingsForm(request.form, g.user) + form.ui_lang.choices = current_app.config['LANGUAGES'].items() + + if form.validate_on_submit(): + form.populate_obj(g.user) + db.session.add(g.user) + db.session.commit() + flash(_("Settings saved")) + + return render_template('auth/settings.html', languages=current_app.config['LANGUAGES'], form=form) + + +@auth.route('logout') +def logout(): + logout_user() + return redirect('/') diff --git a/project/config.py b/project/config.py new file mode 100644 index 0000000..c6f6219 --- /dev/null +++ b/project/config.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +DEBUG = True +SECRET_KEY = '' + +from os.path import dirname, abspath, join +SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/data.sqlite' % dirname(abspath(__file__)) +SQLALCHEMY_ECHO = True + +# flatpages +FLATPAGES_EXTENSION = '.md' +FLATPAGES_ROOT = join(dirname(__file__), 'docs') +del dirname, abspath, join + +# default babel values +BABEL_DEFAULT_LOCALE = 'en' +BABEL_DEFAULT_TIMEZONE = 'UTC' +ACCEPT_LANGUAGES = ['en', 'ru', ] + +# available languages +LANGUAGES = { + 'en': u'English', + 'ru': u'Русский' +} + +# make sure that you have started debug mail server using command +# $ make mail +MAIL_SERVER = 'localhost' +MAIL_PORT = 20025 +MAIL_USE_SSL = False +MAIL_USERNAME = 'your@email.address' +#MAIL_PASSWORD = 'topsecret' + +# Auth +SESSION_COOKIE_NAME = 'session' + +SOCIAL_AUTH_LOGIN_URL = '/' +SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/auth/loggedin' +SOCIAL_AUTH_USER_MODEL = 'project.models.User' +SOCIAL_AUTH_AUTHENTICATION_BACKENDS = ( + 'social.backends.github.GithubOAuth2', +) + +# Documnetation http://psa.matiasaguirre.net/docs/backends/index.html +# https://github.com/settings/applications/ +SOCIAL_AUTH_GITHUB_KEY = '' +SOCIAL_AUTH_GITHUB_SECRET = '' +SOCIAL_AUTH_GITHUB_SCOPE = ['user:email'] diff --git a/project/docs/index.md b/project/docs/index.md new file mode 100644 index 0000000..16bddaf --- /dev/null +++ b/project/docs/index.md @@ -0,0 +1,5 @@ +title: Page title + +# About the project + +Welcome to our new Flask site. diff --git a/project/extensions.py b/project/extensions.py new file mode 100644 index 0000000..5240719 --- /dev/null +++ b/project/extensions.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from flask.ext.mail import Mail +mail = Mail() + +from flask.ext.sqlalchemy import SQLAlchemy +db = SQLAlchemy() + +from flask_flatpages import FlatPages +pages = FlatPages() + +import flask.ext.restless +manager = flask.ext.restless.APIManager() + +from flask.ext.login import LoginManager +login_manager = LoginManager() +login_manager.login_view = 'auth.login' + +from flask.ext.babel import Babel +babel = Babel() + +from flask.ext.migrate import Migrate +migrate = Migrate() + +from flask.ext.wtf.csrf import CsrfProtect +csrf = CsrfProtect() diff --git a/project/frontend/__init__.py b/project/frontend/__init__.py new file mode 100644 index 0000000..af09a11 --- /dev/null +++ b/project/frontend/__init__.py @@ -0,0 +1 @@ +from .views import frontend diff --git a/project/frontend/templates/frontend/splash.html b/project/frontend/templates/frontend/splash.html new file mode 100644 index 0000000..cebfc89 --- /dev/null +++ b/project/frontend/templates/frontend/splash.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% set page_title = _('Welcome to our new site') %} + +{% block content %} +

{{ _("Welcome to our new site") }}

+

This is special splash page.

+ +{% endblock %} diff --git a/project/frontend/templates/frontend/user_profile.html b/project/frontend/templates/frontend/user_profile.html new file mode 100644 index 0000000..db9f747 --- /dev/null +++ b/project/frontend/templates/frontend/user_profile.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% set page_title = user_profile.name %} + +{% block include_css %} +{# #} +{% endblock %} + +{% block include_js %} +{# #} +{% endblock %} + +{% block content %} +

{{ user_profile.name }}

+{% endblock %} diff --git a/project/frontend/views.py b/project/frontend/views.py new file mode 100644 index 0000000..f0ed3c3 --- /dev/null +++ b/project/frontend/views.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +from flask import (Blueprint, render_template, g, request, url_for, + current_app, send_from_directory, json, redirect, make_response, abort) + +from flask.ext.login import login_required + +from ..extensions import pages, csrf +from ..models import Country, UserCountry, User + +frontend = Blueprint('frontend', __name__, template_folder="templates") + + +@frontend.before_request +def before_request(): + # Add to global vars list of the projects owned by current user + if g.user: + pass + # g.my_countries = Project.query.filter_by(login=g.user.login).all() + + +@frontend.route('/') +def index(): + cdata = { + country.iso.lower(): country.name + for country in Country.query.order_by("name asc").all() + } + country = request.headers.get('CF-IPCountry') or 'ua' + return render_template('frontend/splash.html', cdata=cdata, + from_country=country, country_list=json.dumps(cdata)) + + +@frontend.route('/docs/', defaults={'path': 'index'}) +@frontend.route('/docs//', endpoint='page') +def page(path): + # Documentation views + _page = pages.get_or_404(path) + return render_template('page.html', page=_page) + + +@frontend.route('/robots.txt') +def static_from_root(): + # Static items + return send_from_directory(current_app.static_folder, request.path[1:]) + + +@frontend.route('/favicon.ico') +def favicon(): + return redirect('/static/favicon.png') + + +@csrf.exempt +@frontend.route('/search') +def search(): + if request.args.get('from') and request.args.get('to'): + return redirect(url_for('country.visa', cfrom=request.args.get('from'), + cto=request.args.get('to'))) + elif request.args.get('from'): + return redirect(url_for('country.visa_list', + code=request.args.get('from'))) + elif request.args.get('to'): + return redirect(url_for('country.info', code=request.args.get('to'))) + + return redirect(url_for('frontend.index')) + + +@frontend.route('/user/') +def user_profile(username): + user_profile = User.query.filter_by(username=username).first_or_404() + user_country = UserCountry.query.filter_by(user=user_profile).all() + return render_template('frontend/user_profile.html', + user_profile=user_profile, user_country=user_country) + + +@frontend.route('/my-list') +@login_required +def mylist(): + countries = Country.query.order_by("name asc").all() + return render_template('country/all.html', countries=countries) diff --git a/project/models.py b/project/models.py new file mode 100644 index 0000000..00a5997 --- /dev/null +++ b/project/models.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .auth.models import User diff --git a/project/templates/base.html b/project/templates/base.html new file mode 100644 index 0000000..27bbe04 --- /dev/null +++ b/project/templates/base.html @@ -0,0 +1,64 @@ + + + + + + + + + + + {% block title %}{{ page_title|default(_('New site')) }}{% endblock %} + + + {% block include_css %} + {% endblock %} + + + + + {% block include_js %} + {% endblock %} + + + +{% block extramedia %}{% endblock %} +{% include "counter.html" %} + + + +
+ + {% include "nav.html" %} + + {% block fullcontent %} + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +
+ + {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + {# Body #} + {% block content %}{% endblock %} +
+ {% endblock %} + +
+ + + + + diff --git a/project/templates/counter.html b/project/templates/counter.html new file mode 100644 index 0000000..02f0f46 --- /dev/null +++ b/project/templates/counter.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/project/templates/macros.html b/project/templates/macros.html new file mode 100644 index 0000000..5a5990d --- /dev/null +++ b/project/templates/macros.html @@ -0,0 +1,18 @@ +{% macro render_bootstrap_field(field) %} + +
+ + +
+ {{ field(class='form-control')|safe }} + {% if field.errors %} + {% for error in field.errors %} + {{ error }}
+ {% endfor %} + {% endif %} + {% if field.description %} + {{field.description}} + {% endif %} +
+
+{% endmacro %} diff --git a/project/templates/misc/403.html b/project/templates/misc/403.html new file mode 100644 index 0000000..aa9b4ab --- /dev/null +++ b/project/templates/misc/403.html @@ -0,0 +1,7 @@ +{% extends "misc/base.html" %} +{% set page_title = _('No access') %} +{% block content %} +

{{_("Error 403")}}

+

{{ _("You don't have access to page %(url)s.", url=request.url) }}

+

{{ _("← Back to front page"}}

+{% endblock %} diff --git a/project/templates/misc/404.html b/project/templates/misc/404.html new file mode 100644 index 0000000..4939a1f --- /dev/null +++ b/project/templates/misc/404.html @@ -0,0 +1,7 @@ +{% extends "misc/base.html" %} +{% set page_title = _('Page not found') %} +{% block content %} +

{{ _("Error 404") }}

+

{{ _("The page %(url)s you are looking for is not found.", url=request.url) }}

+

{{ _("← Back to front page") }}

+{% endblock %} diff --git a/project/templates/misc/405.html b/project/templates/misc/405.html new file mode 100644 index 0000000..941696f --- /dev/null +++ b/project/templates/misc/405.html @@ -0,0 +1,7 @@ +{% extends "misc/base.html" %} +{% set page_title = _('Method not allowed') %} +{% block content %} +

{{ _("Error 405") }}

+

{{_("You are trying to access page by methods that is not allowed.")}}

+

{{ _("← Back to front page") }}

+{% endblock %} diff --git a/project/templates/misc/500.html b/project/templates/misc/500.html new file mode 100644 index 0000000..8aa4970 --- /dev/null +++ b/project/templates/misc/500.html @@ -0,0 +1,7 @@ +{% extends "misc/base.html" %} +{% set page_title = _('Server error') %} +{% block content %} +

{{ _("Error 500") }}

+

{{ _("Error generating page %(url)s. Please contact us.", url=request.url, contact=url_for('splash.conacts')) }}

+

{{ _("← Back to front page" ) }}

+{% endblock %} diff --git a/project/templates/misc/base.html b/project/templates/misc/base.html new file mode 100644 index 0000000..ea703c3 --- /dev/null +++ b/project/templates/misc/base.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +{% endblock %} + +{% block footer %} +{% endblock %} + +{% block js_btm %} +{% endblock %} diff --git a/project/templates/nav.html b/project/templates/nav.html new file mode 100644 index 0000000..9a6ce06 --- /dev/null +++ b/project/templates/nav.html @@ -0,0 +1,28 @@ + + diff --git a/project/templates/page.html b/project/templates/page.html new file mode 100644 index 0000000..b8bd643 --- /dev/null +++ b/project/templates/page.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% set page_title = page.title %} + +{% block content %} +

{{ page.title }}

+ +{{ page.html|safe }} + +{% endblock %} \ No newline at end of file diff --git a/project/utils.py b/project/utils.py new file mode 100644 index 0000000..9f23c5e --- /dev/null +++ b/project/utils.py @@ -0,0 +1,41 @@ +import sys + +PY2 = sys.version_info[0] == 2 +VER = sys.version_info + +if not PY2: + text_type = str + string_types = (str,) + integer_types = (int, ) + + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) + + def as_unicode(s): + if isinstance(s, bytes): + return s.decode('utf-8') + + return str(s) + + # Various tools + from functools import reduce + from urllib.parse import urljoin, urlparse +else: + text_type = unicode + string_types = (str, unicode) + integer_types = (int, long) + + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() + + def as_unicode(s): + if isinstance(s, str): + return s.decode('utf-8') + + return unicode(s) + + # Helpers + reduce = __builtins__['reduce'] if isinstance(__builtins__, dict) else __builtins__.reduce + from urlparse import urljoin, urlparse diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bb3f4d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,39 @@ +# base +Babel +Flask +Flask-Babel +Flask-FlatPages +Flask-Mail +Flask-Script +Flask-WTF +Jinja2 +Markdown +PyYAML +Werkzeug +itsdangerous +pytz==2014.4 +requests +speaklater + +# SQLAlchemy +flask-restless +Flask-SQLAlchemy +flask-migrate + +# auth +python_social_auth +Flask-Login + +# trie +# marisa_trie + +# Docs +# sphinx +# sphinx_rtd_theme + +# Testing +# pytest +# factory_boy == 2.3.1 +# newrelic +# raven[flask] +# selenium diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..825eced --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +setup( + name="flask-project-template", + version='0.1', + url='', + description='', + author='Mikhail Kashkin', + author_email='mkashkin@gmail.com', + packages=["project"], + include_package_data=True, + zip_safe=False, + install_requires=[ + 'Flask', + ], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + ] +) diff --git a/static/.bowerrc b/static/.bowerrc new file mode 100644 index 0000000..4e5e51b --- /dev/null +++ b/static/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "libs" +} diff --git a/static/.gitattributes b/static/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/static/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/static/bower.json b/static/bower.json new file mode 100644 index 0000000..729dd1e --- /dev/null +++ b/static/bower.json @@ -0,0 +1,10 @@ +{ + "name": "vmstatic", + "private": true, + "dependencies": { + "bootstrap": "*", + "jquery": "*", + "font-awesome": "*" + }, + "devDependencies": {} +} diff --git a/static/code.js b/static/code.js new file mode 100644 index 0000000..5eaa6c2 --- /dev/null +++ b/static/code.js @@ -0,0 +1,3 @@ +/* Custom project Javascript */ +/* Pro tip: you can use Grunt or other automation tools + to compile or minimize for production environment */ \ No newline at end of file diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d63b2c67f0e15189dcc0e572b3b279810555e29c GIT binary patch literal 552 zcmV+@0@wYCP)G1?+jI=fdl z$3nrOgG(_q{tu0~xL6cIrBiLwsY4usn9kkAN}!9B6mfEL?ykHfe@!;{^ z-F@zRci%g%TCI{J?G{2}^(RCJi(0MbfjC7`f?$*5I7^o0Qyef1!`|(7*HDUXPF2;8 zu-R;QfXn%Op2E#$^8#QLAgk4ilE4rcBYv(@shB80f?#NBqtVE*-P-iHL88TC5&Jg~ zZusknFCMO5uYb`Kumzq)u~_T{(jDu*Pp8udWOQBkaKd`MK5#o4jXow4iIOOa^uDHP zmWIRO-(sVlgY_oFgS;pzz&AvIKKd&jmP7wcswrP!d9zgk47U6 z1b%?=@SG$`fp&vwirYjm^_5!RZhL=XHk-wq@jYYr$TNC4Ft@vZ#vHhN$-XUN0zsk& z9R14BQi33mNF+kQgA@cG0Dt?rOeVvlfCH0nnx@?&1rs`Egu~&mTa@@Y%d#8}-gtjM qo@IyS9Ei9@Kipie*Q4CO00RJiq9!j(=z!$_0000