├── .editorconfig ├── .gitignore ├── README.rst ├── docker-compose-dev.yml ├── docker-compose.yml ├── envfile ├── envfile.dev ├── envfile.prd ├── flask-vue ├── .bash_history ├── .gitignore ├── Dockerfile ├── Procfile ├── apps │ └── blog │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api.py │ │ ├── bp.py │ │ ├── forms.py │ │ ├── models.py │ │ ├── schemas.py │ │ ├── socket.py │ │ ├── static │ │ └── coffee │ │ │ └── app.coffee │ │ ├── templates │ │ └── blog │ │ │ └── index.html │ │ └── views.py ├── config.py ├── empty.py ├── extensions │ ├── __init__.py │ ├── admin.py │ ├── database.py │ ├── debug.py │ ├── schemas.py │ └── socketio.py ├── flask-vue.ini ├── main.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 7082559427b5_.py ├── package.json ├── static │ └── favicon.ico ├── templates │ ├── base.html │ ├── http │ │ ├── access_forbidden.html │ │ ├── method_not_allowed.html │ │ ├── page_not_found.html │ │ └── server_error.html │ ├── index.html │ └── macros │ │ ├── _flashing.html │ │ └── _formhelpers.html ├── utils │ ├── __init__.py │ └── api.py └── wsgi.py └── ux ├── Dockerfile └── README /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [Makefile] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.py] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | node_modules 4 | # pycharm friendly 5 | .idea 6 | .vscode 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Vuejs-Python 2 | ============ 3 | 4 | This project aims to create various examples showing how to work 5 | with vuejs and python based technologies. As vuejs may be integrated 6 | in different ways, with different advantages for each approach, 7 | having these examples are ideal for the brave-of-heart. 8 | 9 | flask-vue 10 | --------- 11 | 12 | ## quickstart 13 | 14 | docker-compose -f docker-compose.yml -f docker-compose-dev.yml up 15 | 16 | **flask-vue** is the first project made. It shows how to 17 | use flask and vuejs to build a non-SPA (single page 18 | application). This approach is more appropriate for 19 | those with little experience using js/node build tools 20 | like webpack_ and browserify_. You'll mostly have to 21 | handle python and js code. Another important advantage 22 | is that most flask "resources" will still work 23 | out-of-the-box, like debug toolbar and csrf protection. 24 | 25 | A disvantage for this approach is that it is much 26 | less flexible, integrates poorly with other 27 | javascript libraries, less performatic, has cache 28 | invalidation issues and is not appropriate for 29 | larger projects. Depending of how you implement 30 | the frontend, you may also see some flickering. 31 | 32 | So, how does the **flask-vue** was built? Basically 33 | you have a flask application with support to schemas 34 | that loads your vue framework in the index.html page, 35 | mounts your Vue element, queries a endpoint and builds 36 | your view. Even though this approach produces flickering, 37 | it is quite simple to implement. Let's focus on a few 38 | highlights: 39 | 40 | - **The javascript is generated by webassets**, which converts 41 | the coffee files on access. Quite convenient. 42 | - **Marshmallow is used for serialization**, as it is capable 43 | of loading requests into python objects/sqlalchemy objects and 44 | serialize them back. 45 | - **Ajax requests are treated by plain views**, with exception 46 | to the rest api, which is handle by a custom Resource class, 47 | which provides a rest api interface. 48 | 49 | Be warned that there are a few broken links here and there AND 50 | pagination is not implemented visually. 51 | 52 | .. _webpack: https://webpack.github.io/ 53 | .. _browserify: http://browserify.org/ 54 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | webapp: 5 | env_file: 6 | - envfile.dev 7 | ports: 8 | - "5000:5000" 9 | volumes: 10 | - ./flask-vue:/home/usr 11 | ux: 12 | command: ["npm", "run", "dev"] 13 | env_file: 14 | - envfile.dev 15 | volumes: 16 | - ./ux/index.html:/home/node/index.html 17 | - ./ux/package.json:/home/node/package.json 18 | - ./ux/static:/home/node/static 19 | - ./ux/test:/home/node/test 20 | - ./ux/src:/home/node/src 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | webapp: 5 | build: ./flask-vue 6 | env_file: 7 | - envfile 8 | ux: 9 | build: ./ux 10 | command: ["npm", "run", "build"] 11 | env_file: 12 | - envfile 13 | -------------------------------------------------------------------------------- /envfile: -------------------------------------------------------------------------------- 1 | PROJECT=name 2 | FLASK_APP=wsgi.py 3 | FLASK_DEBUG=0 4 | -------------------------------------------------------------------------------- /envfile.dev: -------------------------------------------------------------------------------- 1 | FLASK_DEBUG=1 2 | SERVER_NAME=localhost:5000 3 | -------------------------------------------------------------------------------- /envfile.prd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italomaia/vuejs-python/9058f534bf9b55a7348c839d905d604060aae91d/envfile.prd -------------------------------------------------------------------------------- /flask-vue/.bash_history: -------------------------------------------------------------------------------- 1 | flask shell 2 | exit 3 | -------------------------------------------------------------------------------- /flask-vue/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore cache 2 | /static/.* 3 | 4 | # ignore auto-generated js files 5 | /static/js/*.js 6 | -------------------------------------------------------------------------------- /flask-vue/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | ENV USR usr 4 | ENV HOME /home/$USR 5 | RUN groupadd -g 1000 -r $USR && \ 6 | useradd -u 1000 -d $HOME -m -r -g $USR $USR 7 | 8 | WORKDIR $HOME 9 | COPY . $HOME 10 | RUN chown -R 1000:1000 . 11 | RUN pip install --no-cache-dir\ 12 | eventlet==0.21.0\ 13 | flask==0.12.2\ 14 | flask-admin==1.5.0\ 15 | flask-jsglue==0.3.1\ 16 | flask-marshmallow==0.8.0\ 17 | flask-migrate==2.1.1\ 18 | flask-restful==0.3.6\ 19 | flask-sqlalchemy==2.3.1\ 20 | flask-socketio==2.9.2\ 21 | flask-debugtoolbar==0.10.1\ 22 | marshmallow-sqlalchemy==0.13.1 23 | 24 | # make sure user running the process is a regular user 25 | USER $USR 26 | EXPOSE 5000 27 | CMD ["flask", "run", "-h", "0.0.0.0"] 28 | -------------------------------------------------------------------------------- /flask-vue/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:heroku\(\) -------------------------------------------------------------------------------- /flask-vue/apps/blog/__init__.py: -------------------------------------------------------------------------------- 1 | from .bp import app # noqa:F401 2 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/admin.py: -------------------------------------------------------------------------------- 1 | from extensions.admin import admin 2 | from extensions.database import db 3 | from flask_admin.contrib.sqla import ModelView 4 | 5 | from .models import Post 6 | from .forms import PostForm 7 | 8 | 9 | class PostView(ModelView): 10 | def get_form(self): 11 | return PostForm 12 | 13 | 14 | admin.add_view(PostView(Post, db.session)) 15 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/api.py: -------------------------------------------------------------------------------- 1 | from extensions.schemas import ma 2 | from extensions.database import db 3 | from utils.api import Resource 4 | from blog.models import Post 5 | from .bp import app 6 | 7 | 8 | class PostSchema(ma.ModelSchema): 9 | class Meta: 10 | model = Post 11 | 12 | 13 | class PostResource(Resource): 14 | session = db.session 15 | schema_cls = PostSchema 16 | 17 | 18 | post_api = PostResource.as_view('post_api') 19 | app.add_url_rule( 20 | '/posts', 21 | view_func=post_api, 22 | methods=['GET', 'POST'] 23 | ) 24 | app.add_url_rule( 25 | '/posts/', 26 | view_func=post_api, 27 | methods=['GET', 'PUT', 'DELETE'] 28 | ) 29 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/bp.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | app = Blueprint( 4 | 'blog', __name__, 5 | template_folder='templates', 6 | static_folder='static' 7 | ) 8 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms_alchemy import ModelForm 2 | from .models import Post 3 | 4 | 5 | class PostForm(ModelForm): 6 | class Meta: 7 | model = Post 8 | only = ['title', 'text', 'tags'] 9 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/models.py: -------------------------------------------------------------------------------- 1 | from extensions.database import db 2 | from sqlalchemy import event 3 | 4 | from slugify import slugify 5 | 6 | import mistune 7 | import datetime 8 | 9 | 10 | class Post(db.Model): 11 | __tablename__ = 'posts' 12 | 13 | id = db.Column(db.Integer, primary_key=True) 14 | created = db.Column(db.DateTime, default=datetime.datetime.utcnow) 15 | modified = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) 16 | title = db.Column(db.Unicode(100)) 17 | slug = db.Column(db.Unicode(100)) 18 | text = db.Column(db.Text) 19 | html = db.Column(db.Text) 20 | tags = db.Column(db.Unicode(60), index=True) 21 | 22 | def __str__(self): # noqa: D105 23 | return self.title 24 | 25 | 26 | @event.listens_for(Post.title, 'set') 27 | def post_on_title_set(target, value, oldvalue, initiator): 28 | target.slug = slugify(value) 29 | 30 | 31 | # as we don't want the html to be edited manually 32 | # the html has to be generated for each row update 33 | @event.listens_for(Post.text, 'set') 34 | def post_on_text_set(target, value, oldvalue, initiator): 35 | target.html = mistune.markdown(value) 36 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/schemas.py: -------------------------------------------------------------------------------- 1 | from extensions.schemas import ma 2 | from .models import Post 3 | 4 | 5 | class PostSchema(ma.Schema): 6 | class Meta: 7 | model = Post 8 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/socket.py: -------------------------------------------------------------------------------- 1 | from extensions.socketio import socketio 2 | from flask_sqlalchemy import models_committed 3 | from .schemas import PostSchema 4 | from .models import Post 5 | 6 | namespace = '/blog/io' 7 | post_schema = PostSchema() 8 | 9 | 10 | def post_after_commit(sender, changes): 11 | for model, change in changes: 12 | if isinstance(model, Post) and change in ('insert',): 13 | emit_new_posts() 14 | break 15 | 16 | 17 | def emit_new_posts(): 18 | socketio.emit('new posts', namespace=namespace) 19 | 20 | 21 | models_committed.connect(post_after_commit) 22 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/static/coffee/app.coffee: -------------------------------------------------------------------------------- 1 | VueListModel = Vue.extend 2 | data: -> 3 | items: [] 4 | items_count: 0 5 | has_next: false 6 | has_prev: false 7 | next_num: null 8 | prev_num: null 9 | page: 0 10 | pages: 0 11 | total: 0 12 | 13 | mounted: -> 14 | el = $ @$el 15 | src = el.attr 'src' 16 | $.get(src).done (data) => 17 | for k,v of data 18 | this[k] = v 19 | 20 | socket.on 'new posts', => Materialize.toast('New Post! Press F5 to update.', 3000) 21 | 22 | # on document ready ... 23 | $ -> $('[vue-list]').each -> new VueListModel 24 | # ES6 template string style (this is important!) 25 | delimiters: ['${', '}'] 26 | el: this 27 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 13 | 14 |
15 |
16 |
17 | To add a new post, click here. 18 | The blog has ${total} post${total|pluralize} published right now. 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% endblock %} 32 | 33 | {% block extra_scripts %} 34 | 37 | {{ super() }} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /flask-vue/apps/blog/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from .bp import app 3 | 4 | 5 | @app.route("/") 6 | def index(): 7 | return render_template('blog/index.html') 8 | -------------------------------------------------------------------------------- /flask-vue/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from datetime import timedelta 4 | 5 | project_name = os.getenv('PROJECT') 6 | 7 | 8 | # base config class; extend it to your needs. 9 | # use DEBUG mode? 10 | DEBUG = os.getenv('FLASK_DEBUG') == '1' 11 | DEBUG_TB_INTERCEPT_REDIRECTS = False 12 | 13 | SERVER_NAME = os.getenv("SERVER_NAME") 14 | 15 | # use TESTING mode? 16 | TESTING = os.getenv('FLASK_TESTING') == '1' 17 | 18 | # use server x-sendfile? 19 | USE_X_SENDFILE = os.getenv('FLASK_USE_X_SENDFILE') == '1' 20 | 21 | # DATABASE CONFIGURATION 22 | # see http://docs.sqlalchemy.org/en/rel_0_9/core/engines.html#database-urls 23 | SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') 24 | SQLALCHEMY_ECHO = os.getenv('SQLALCHEMY_ECHO') == '1' 25 | SQLALCHEMY_TRACK_MODIFICATIONS = not DEBUG 26 | 27 | WTF_CSRF_ENABLED = os.getenv('FLASK_WTF_CSRF_ENABLED') == '1' 28 | SECRET_KEY = os.getenv('FLASK_SECRET', 'secret') # import os; os.urandom(24) 29 | 30 | # LOGGING 31 | LOGGER_NAME = "%s_log" % project_name 32 | LOG_FILENAME = "/var/tmp/app.%s.log" % project_name 33 | LOG_LEVEL = logging.INFO 34 | # used by logging.Formatter 35 | LOG_FORMAT = "%(asctime)s %(levelname)s\t: %(message)s" 36 | 37 | PERMANENT_SESSION_LIFETIME = timedelta(days=7) 38 | 39 | # EMAIL CONFIGURATION 40 | MAIL_SERVER = "localhost" 41 | MAIL_PORT = 25 42 | MAIL_USE_TLS = False 43 | MAIL_USE_SSL = False 44 | MAIL_DEBUG = False 45 | MAIL_USERNAME = None 46 | MAIL_PASSWORD = None 47 | DEFAULT_MAIL_SENDER = "example@%s.com" % project_name 48 | 49 | EXTENSIONS = [ 50 | 'extensions.debug.toolbar', 51 | 'extensions.database.db', 52 | 'extensions.database.migrate', 53 | 'extensions.schemas.ma', 54 | 'extensions.socketio.socketio', 55 | 'extensions.admin.admin', 56 | ] 57 | 58 | LOAD_MODULES_EXTENSIONS = [ 59 | 'api', 60 | 'admin', 61 | 'views', 62 | 'models', 63 | 'socket', 64 | ] 65 | 66 | # see example/ for reference 67 | # where app is a Blueprint instance 68 | # 69 | # Examples: 70 | # BLUEPRINTS = ['blog'] # where app is a Blueprint instance 71 | # BLUEPRINTS = [('blog', {'url_prefix': '/myblog'})] 72 | BLUEPRINTS = [] 73 | -------------------------------------------------------------------------------- /flask-vue/empty.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Empty'] 2 | 3 | from flask import Flask, render_template 4 | from werkzeug.utils import import_string 5 | import logging 6 | 7 | 8 | class NoExtensionException(Exception): 9 | pass 10 | 11 | 12 | class BlueprintException(Exception): 13 | pass 14 | 15 | 16 | basestring = getattr(__builtins__, 'basestring', str) 17 | 18 | 19 | class Empty(Flask): 20 | 21 | def configure(self, config=None): 22 | """ 23 | Load configuration class into flask app. 24 | 25 | If environment variable available, overwrites class config. 26 | """ 27 | self.config.from_object(config) 28 | 29 | if config is None: 30 | # could/should be available in server environment 31 | self.config.from_envvar("FLASK_CONFIG", silent=False) 32 | 33 | def add_blueprint(self, name, kw): 34 | for module in self.config['LOAD_MODULES_EXTENSIONS']: 35 | try: 36 | __import__('%s.%s' % (name, module), fromlist=['*']) 37 | except ImportError as err: 38 | print(err) 39 | except AttributeError as err: 40 | print(err) 41 | 42 | blueprint = import_string('%s.%s' % (name, 'app')) 43 | self.register_blueprint(blueprint, **kw) 44 | 45 | def add_blueprint_list(self, bp_list): 46 | for blueprint_config in bp_list: 47 | name, kw = None, dict() 48 | 49 | if isinstance(blueprint_config, basestring): 50 | name = blueprint_config 51 | kw.update({'url_prefix': '/' + name}) 52 | elif isinstance(blueprint_config, (list, tuple)): 53 | name = blueprint_config[0] 54 | kw.update(blueprint_config[1]) 55 | else: 56 | raise BlueprintException( 57 | "Error in BLUEPRINTS setup in config.py;" 58 | "Please, verify if each blueprint setup is " 59 | "either a string or a tuple." 60 | ) 61 | 62 | self.add_blueprint(name, kw) 63 | 64 | def setup(self): 65 | self.configure_logger() 66 | self.configure_error_handlers() 67 | self.configure_database() 68 | self.configure_context_processors() 69 | self.configure_template_extensions() 70 | self.configure_template_filters() 71 | self.configure_extensions() 72 | self.configure_before_request() 73 | self.configure_views() 74 | 75 | def configure_logger(self): 76 | """Configures the app logger.""" 77 | log_filename = self.config['LOG_FILENAME'] 78 | 79 | # Create a file logger since we got a logdir 80 | log_file = logging.FileHandler(filename=log_filename) 81 | formatter = logging.Formatter(self.config['LOG_FORMAT']) 82 | log_file.setFormatter(formatter) 83 | log_file.setLevel(self.config['LOG_LEVEL']) 84 | 85 | logger = self.logger 86 | logger.addHandler(log_file) 87 | logger.info("Logger started") 88 | 89 | def configure_error_handlers(app): 90 | @app.errorhandler(403) 91 | def forbidden_page(error): 92 | """ 93 | Shows a "access forbidden" page. 94 | 95 | The server understood the request, but is refusing to fulfill it. 96 | Authorization will not help and the request SHOULD NOT be repeated. 97 | If the request method was not HEAD and the server wishes to make 98 | public why the request has not been fulfilled, it SHOULD describe 99 | the reason for the refusal in the entity. If the server does not 100 | wish to make this information available to the client, the status 101 | code 404 (Not Found) can be used instead. 102 | """ 103 | return render_template("http/access_forbidden.html"), 403 104 | 105 | @app.errorhandler(404) 106 | def page_not_found(error): 107 | """ 108 | Shows a "not found" page. 109 | 110 | The server has not found anything matching the Request-URI. No 111 | indication is given of whether the condition is temporary or 112 | permanent. The 410 (Gone) status code SHOULD be used if the 113 | server knows, through some internally configurable mechanism, 114 | that an old resource is permanently unavailable and has no 115 | forwarding address. This status code is commonly used when the 116 | server does not wish to reveal exactly why the request has been 117 | refused, or when no other response is applicable. 118 | """ 119 | return render_template("http/page_not_found.html"), 404 120 | 121 | @app.errorhandler(405) 122 | def method_not_allowed_page(error): 123 | """ 124 | Shows a "method not allowed" page. 125 | 126 | The method specified in the Request-Line is not allowed for the 127 | resource identified by the Request-URI. The response MUST 128 | include an Allow header containing a list of valid methods 129 | for the requested resource. 130 | """ 131 | return render_template("http/method_not_allowed.html"), 405 132 | 133 | @app.errorhandler(500) 134 | def server_error_page(error): 135 | """Shows a "server error" page.""" 136 | return render_template("http/server_error.html"), 500 137 | 138 | def configure_database(self): 139 | """Database configuration should be set here.""" 140 | pass 141 | 142 | def configure_context_processors(self): 143 | """Modify templates context here.""" 144 | pass 145 | 146 | def configure_template_extensions(self): 147 | """Add jinja2 extensions here.""" 148 | # 'do' extension. 149 | # see: http://jinja.pocoo.org/docs/extensions/#expression-statement 150 | self.jinja_env.add_extension('jinja2.ext.do') 151 | 152 | def configure_template_filters(self): 153 | """Configures filters and tags for jinja.""" 154 | pass 155 | 156 | def configure_extensions(self): 157 | """Configures extensions like mail and login here.""" 158 | for ext_path in self.config.get('EXTENSIONS', []): 159 | try: 160 | ext = import_string(ext_path) 161 | 162 | # TODO: check if ext is a module for safety 163 | if getattr(ext, 'init_app', False): 164 | ext.init_app(self) 165 | else: 166 | ext(self) 167 | except: 168 | raise NoExtensionException( 169 | 'No {} extension found'.format(ext_path)) 170 | 171 | def configure_before_request(self): 172 | pass 173 | 174 | def configure_views(self): 175 | """You can add some simple views here for fast prototyping.""" 176 | pass 177 | -------------------------------------------------------------------------------- /flask-vue/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italomaia/vuejs-python/9058f534bf9b55a7348c839d905d604060aae91d/flask-vue/extensions/__init__.py -------------------------------------------------------------------------------- /flask-vue/extensions/admin.py: -------------------------------------------------------------------------------- 1 | from flask_admin import Admin 2 | 3 | 4 | admin = Admin(name='VueJS blog', template_mode='bootstrap3') 5 | -------------------------------------------------------------------------------- /flask-vue/extensions/database.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_migrate import Migrate 3 | 4 | db = SQLAlchemy() 5 | migrate = Migrate(db=db) 6 | -------------------------------------------------------------------------------- /flask-vue/extensions/debug.py: -------------------------------------------------------------------------------- 1 | from flask_debugtoolbar import DebugToolbarExtension 2 | 3 | 4 | toolbar = DebugToolbarExtension() 5 | -------------------------------------------------------------------------------- /flask-vue/extensions/schemas.py: -------------------------------------------------------------------------------- 1 | from flask_marshmallow import Marshmallow 2 | 3 | ma = Marshmallow() 4 | -------------------------------------------------------------------------------- /flask-vue/extensions/socketio.py: -------------------------------------------------------------------------------- 1 | from flask_socketio import SocketIO 2 | 3 | socketio = SocketIO() 4 | -------------------------------------------------------------------------------- /flask-vue/flask-vue.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | user-home = project-user-home-here 3 | prj = flask-vue 4 | pp = /path/to/project 5 | 6 | # make sure paths exist 7 | socket = /tmp/%(prj).sock 8 | pidfile = /tmp/%(prj).pid 9 | daemonize = /var/tmp/%(prj).log 10 | touch-reload = %(pp)/wsgi.py 11 | 12 | # suggestion: use virtualenvwrapper 13 | venv = /path/to/virtualenv 14 | 15 | idle = true 16 | harakiri = 30 17 | processes = 4 18 | threads = 2 19 | 20 | wsgi-file = %(pp)/wsgi.py 21 | callable = app 22 | 23 | # server user/group should have read/write rights to the socket 24 | uid = www-data 25 | gid = www-data 26 | -------------------------------------------------------------------------------- /flask-vue/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | import sys 5 | from empty import Empty 6 | 7 | 8 | # apps is a special folder where you can place your blueprints 9 | PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) 10 | sys.path.insert(0, os.path.join(PROJECT_PATH, "apps")) 11 | 12 | basestring = getattr(__builtins__, 'basestring', str) 13 | 14 | 15 | class App(Empty): 16 | def configure_views(self): 17 | @self.route("/") 18 | def index_view(): 19 | return """ 20 | Hello My Friend. This is an example flask view. 21 | Try creatting blueprints inside ./apps and setting 22 | them up in your config.py file.""".strip() 23 | 24 | 25 | def app_factory(config, app_name, blueprints=None): 26 | # you can use Empty directly if you wish 27 | app = App(app_name) 28 | app.configure(config) 29 | app.add_blueprint_list(blueprints or config.BLUEPRINTS) 30 | app.setup() 31 | 32 | return app 33 | 34 | 35 | def heroku(): 36 | import config 37 | # setup app through APP_CONFIG envvar 38 | return app_factory(config, config.project_name) 39 | -------------------------------------------------------------------------------- /flask-vue/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /flask-vue/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /flask-vue/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.readthedocs.org/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | finally: 82 | connection.close() 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /flask-vue/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /flask-vue/migrations/versions/7082559427b5_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7082559427b5 4 | Revises: 5 | Create Date: 2017-03-01 12:28:21.882943 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7082559427b5' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('posts', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('created', sa.DateTime(), nullable=True), 24 | sa.Column('modified', sa.DateTime(), nullable=True), 25 | sa.Column('title', sa.Unicode(length=100), nullable=True), 26 | sa.Column('slug', sa.Unicode(length=100), nullable=True), 27 | sa.Column('text', sa.Text(), nullable=True), 28 | sa.Column('html', sa.Text(), nullable=True), 29 | sa.Column('tags', sa.Unicode(length=60), nullable=True), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | op.create_index(op.f('ix_posts_tags'), 'posts', ['tags'], unique=False) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_index(op.f('ix_posts_tags'), table_name='posts') 39 | op.drop_table('posts') 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /flask-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-vue", 3 | "version": "0.5.0", 4 | "description": "flask example using vuejs", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "coffee-script": "^1.10.0" 12 | }, 13 | "devDependencies": { 14 | "gulp": "^3.9.0", 15 | "gulp-coffee": "^2.3.1", 16 | "gulp-uglify": "^1.5.1", 17 | "gulp-util": "^3.0.7", 18 | "uglify-js": "^2.6.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /flask-vue/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italomaia/vuejs-python/9058f534bf9b55a7348c839d905d604060aae91d/flask-vue/static/favicon.ico -------------------------------------------------------------------------------- /flask-vue/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block site_title %}VueJS Blog with Python{% endblock %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block body %}{% endblock %} 17 | 18 | 23 | 26 | 29 | 34 | 37 | 38 | {% block extra_scripts %} 39 | {% assets "js_all" %} 40 | 41 | {% endassets %} 42 | {% endblock %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /flask-vue/templates/http/access_forbidden.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Http 403 5 | 6 | 7 |

Access Forbidden

8 | 9 | -------------------------------------------------------------------------------- /flask-vue/templates/http/method_not_allowed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Http 405 5 | 6 | 7 |

Method Not Allowed

8 | 9 | -------------------------------------------------------------------------------- /flask-vue/templates/http/page_not_found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Http 404 5 | 6 | 7 |

Page Not Found

8 | 9 | -------------------------------------------------------------------------------- /flask-vue/templates/http/server_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Http 500 5 | 6 | 7 |

Server Error

8 | 9 | -------------------------------------------------------------------------------- /flask-vue/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | -------------------------------------------------------------------------------- /flask-vue/templates/macros/_flashing.html: -------------------------------------------------------------------------------- 1 | 2 | {% macro flash_messages() %} 3 | {% set messages = get_flashed_messages(with_categories=true) %} 4 | {% if messages %} 5 | {% for category, message in messages %} 6 |
{{ message }}
7 | {% endfor %} 8 | {% endif %} 9 | {% endmacro %} 10 | 11 | -------------------------------------------------------------------------------- /flask-vue/templates/macros/_formhelpers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% macro render_field(field) %} 4 | {{ field.label }} 5 | 6 | {% if field.flags.required and kwargs.get('required') == None %} 7 | {% do kwargs.update({'required': 'required'}) %} 8 | {% endif %} 9 | 10 | {{ field(**kwargs) }} 11 | {% if field.errors %} 12 | {% for error in field.errors %} 13 |
{{ error }}
14 | {% endfor %} 15 | {% endif %} 16 | {% endmacro %} 17 | 18 | {% macro render_form(form) %} 19 | {{ form.csrf_token }} 20 | 21 | {% for field in form %} 22 | {{ render_field(field) }} 23 | {% endfor %} 24 | {% endmacro %} 25 | 26 | -------------------------------------------------------------------------------- /flask-vue/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italomaia/vuejs-python/9058f534bf9b55a7348c839d905d604060aae91d/flask-vue/utils/__init__.py -------------------------------------------------------------------------------- /flask-vue/utils/api.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | from flask.views import MethodView 3 | 4 | MAX_PER_PAGE = 20 5 | FIRST_PAGE = 1 6 | 7 | 8 | class Resource(MethodView): 9 | session = None 10 | model = None 11 | schema = None 12 | schema_cls = None 13 | pk_param = 'pk' 14 | pk_type = 'int' 15 | 16 | def __init__(self, session=None, schema_cls=None): 17 | schema_cls = schema_cls or self.schema_cls 18 | self.session = session or self.session 19 | self.schema = schema_cls() 20 | self.model = schema_cls.Meta.model 21 | 22 | def get_page(self): 23 | try: 24 | value = request.args.get('page') 25 | return FIRST_PAGE if value is None else max(FIRST_PAGE, int(value)) 26 | except ValueError: 27 | return FIRST_PAGE 28 | 29 | def get_per_page(self): 30 | try: 31 | value = request.args.get('per_page') 32 | return MAX_PER_PAGE \ 33 | if value is None \ 34 | else min(MAX_PER_PAGE, int(value)) 35 | except ValueError: 36 | return MAX_PER_PAGE 37 | 38 | def get_query(self): 39 | return self.model.query 40 | 41 | def get_all(self): 42 | return self.get_query().all() 43 | 44 | def get_one(self, pk): 45 | return self.get_query().get(pk) 46 | 47 | def get(self, pk=None): 48 | if pk is None: 49 | pagination = self.get_query().paginate( 50 | self.get_page(), 51 | self.get_per_page() 52 | ) 53 | return jsonify(dict( 54 | items=self.schema.dump( 55 | pagination.items, 56 | many=True 57 | ).data, 58 | items_count=len(pagination.items), 59 | has_next=pagination.has_next, 60 | has_prev=pagination.has_prev, 61 | page=pagination.page, 62 | pages=pagination.pages, 63 | next_num=pagination.next_num, 64 | prev_num=pagination.prev_num, 65 | total=pagination.total 66 | )) 67 | 68 | return self.schema.dump(self.get_one(pk)).data 69 | 70 | def put(self, pk): 71 | instance = self.get_one(pk) 72 | errors = self.schema.validate(request.form) 73 | 74 | if errors: 75 | return errors, 400 76 | 77 | for key, value in self.schema.load(request.form): 78 | setattr(instance, key, value) 79 | 80 | self.session.add(instance) 81 | self.session.commit() 82 | return self.schema.dump(instance).data 83 | 84 | def post(self): 85 | errors = self.schema.validate(request.form) 86 | 87 | if errors: 88 | return errors, 400 89 | 90 | data = self.schema.load(request.form) 91 | instance = self.model(**data) 92 | self.db.session.add(instance) 93 | self.db.session.commit() 94 | return self.schema.dump(instance).data 95 | 96 | def delete(self, pk): 97 | instance = self.get_one(pk) 98 | 99 | self.session.delete(instance) 100 | self.session.commit() 101 | return self.schema.dump(instance).data 102 | -------------------------------------------------------------------------------- /flask-vue/wsgi.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from main import app_factory 4 | import config 5 | 6 | app = app_factory(config, config.project_name) 7 | -------------------------------------------------------------------------------- /ux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | # node image already has a regular user called node 4 | ENV USR node 5 | ENV HOME /home/$USR 6 | 7 | WORKDIR $HOME 8 | COPY . . 9 | RUN chown -R 1000:1000 . 10 | 11 | USER $USR 12 | RUN yarn --non-interactive --no-progress --no-lockfile && \ 13 | yarn cache clean 14 | -------------------------------------------------------------------------------- /ux/README: -------------------------------------------------------------------------------- 1 | Your vuejs project should live here. Use vue-cli to generate it. 2 | --------------------------------------------------------------------------------