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 0000000..d63b2c6 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..26b4d70 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Allow: / \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..47a21a8 --- /dev/null +++ b/static/style.css @@ -0,0 +1,2 @@ +/* Custom project CSS styles */ +/* Pro tip: CSS can be compiled by LESS/SASS */ \ No newline at end of file