├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── comment │ ├── controllers.py │ ├── forms.py │ └── models.py ├── database.py ├── entity │ ├── controllers.py │ ├── forms.py │ └── models.py ├── general │ └── controllers.py ├── static │ ├── css │ │ └── styles.css │ └── img │ │ └── favicon.ico ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── entity │ │ ├── create.html │ │ ├── delete.html │ │ ├── index.html │ │ ├── update.html │ │ └── view.html │ ├── form-errors.html │ ├── form-macros.html │ ├── messages.html │ └── pagination.html └── utils │ ├── __init__.py │ └── db.py ├── config.py ├── manage.py └── requipments ├── base.txt ├── development.txt └── production.txt /.env.example: -------------------------------------------------------------------------------- 1 | export APP_SETTINGS="config.DevelopmentConfig" 2 | export DATABASE_URL='postgresql://DBUSERNAME:DBPASSWORD@localhost/DBNAME' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .idea/* 3 | 4 | lib/ 5 | lib/* 6 | 7 | *.py[cod] 8 | *.pyc 9 | 10 | bin/ 11 | bin/* 12 | 13 | share/ 14 | share/* 15 | 16 | local/ 17 | local/* 18 | 19 | include/ 20 | include/* 21 | 22 | env/ 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask structure example 2 | 3 | Это законченный пример к записи в моем блоге 4 | [правильная структура flask приложения](https://the-bosha.ru/2016/06/03/python-flask-freimvork-pravilnaia-struktura-prilozheniia/). 5 | 6 | ## Setup 7 | 8 | ``` 9 | git clone https://github.com/bosha/flask-app-structure-example/ 10 | cd flask-app-structure-example 11 | virtualenv -p python3 env 12 | source env/bin/activate 13 | pip install -r requipments/development.txt 14 | export APP_SETTINGS="config.DevelopmentConfig" 15 | # DBUSERNAME, DBPASSWORD и DBNAME необходимо заменить на свои реквизиты доступа к БД 16 | export DATABASE_URL='postgresql://DBUSERNAME:DBPASSWORD@localhost/DBNAME' 17 | python manage.py db init 18 | python manage.py db migrate 19 | python manage.py db upgrade 20 | python manage.py runserver 21 | ``` 22 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | 4 | from .database import db 5 | 6 | def create_app(): 7 | app = Flask(__name__) 8 | app.config.from_object(os.environ['APP_SETTINGS']) 9 | 10 | db.init_app(app) 11 | with app.test_request_context(): 12 | db.create_all() 13 | 14 | if app.debug == True: 15 | try: 16 | from flask_debugtoolbar import DebugToolbarExtension 17 | toolbar = DebugToolbarExtension(app) 18 | except: 19 | pass 20 | 21 | import app.entity.controllers as entity 22 | import app.comment.controllers as comment 23 | import app.general.controllers as general 24 | 25 | app.register_blueprint(general.module) 26 | app.register_blueprint(entity.module) 27 | app.register_blueprint(comment.module) 28 | 29 | return app 30 | -------------------------------------------------------------------------------- /app/comment/controllers.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | request, 4 | flash, 5 | abort, 6 | redirect, 7 | url_for, 8 | current_app, 9 | ) 10 | from sqlalchemy.exc import SQLAlchemyError 11 | 12 | from app.database import db 13 | from .models import Comment 14 | from .forms import CommentAddForm 15 | 16 | 17 | module = Blueprint('comment', __name__, url_prefix='/comment') 18 | 19 | 20 | def log_error(*args, **kwargs): 21 | current_app.logger.error(*args, **kwargs) 22 | 23 | 24 | @module.route('/add/', methods=['POST']) 25 | def add(): 26 | form = CommentAddForm(request.form) 27 | try: 28 | if request.method == 'POST' and form.validate(): 29 | comment = Comment(**form.data) 30 | db.session.add(comment) 31 | db.session.commit() 32 | flash('Comment was successful added!', 'success') 33 | return redirect(url_for('entity.view', id=comment.entity_id)) 34 | except SQLAlchemyError as e: 35 | log_error('There was error while querying database', exc_info=e) 36 | db.session.rollback() 37 | flash('Uncaught exception while querying database', 'danger') 38 | abort(500) 39 | 40 | -------------------------------------------------------------------------------- /app/comment/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import ( 3 | StringField, 4 | TextAreaField, 5 | HiddenField, 6 | ) 7 | from wtforms.validators import ( 8 | DataRequired, 9 | Email, 10 | ) 11 | 12 | class CommentAddForm(Form): 13 | name = StringField( 14 | 'Name', 15 | [ 16 | DataRequired(message="This field is required") 17 | ], 18 | description="Your name" 19 | ) 20 | email = StringField( 21 | 'E-Mail', 22 | [ 23 | Email() 24 | ], 25 | description="Содержимое записи", 26 | ) 27 | content = TextAreaField( 28 | 'Content', 29 | [ 30 | DataRequired(message="This field is required") 31 | ], 32 | description="Content of the comment" 33 | ) 34 | entity_id = HiddenField() 35 | -------------------------------------------------------------------------------- /app/comment/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import event 2 | 3 | from app.database import db 4 | 5 | class Comment(db.Model): 6 | __tablename__ = 'comment' 7 | 8 | id = db.Column(db.Integer, primary_key=True) 9 | name = db.Column(db.String(1000)) 10 | email = db.Column(db.String(1000)) 11 | content = db.Column(db.Text()) 12 | 13 | entity_id = db.Column(db.Integer, db.ForeignKey('entity.id')) 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | 19 | @event.listens_for(Comment, 'after_delete') 20 | def event_after_delete(mapper, connection, target): 21 | # Здесь будет очень важная бизнес логика 22 | # Или нет. На самом деле, старайтесь использовать сигналы только 23 | # тогда, когда других, более правильных вариантов не осталось. 24 | pass 25 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/entity/controllers.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request, 5 | flash, 6 | abort, 7 | redirect, 8 | url_for, 9 | current_app, 10 | ) 11 | from sqlalchemy.exc import SQLAlchemyError 12 | from sqlalchemy.orm import joinedload 13 | 14 | from .models import Entity, db 15 | from .forms import EntityCreateForm 16 | from app.utils.db import sqlalchemy_orm_to_dict 17 | from app.comment.forms import CommentAddForm 18 | from app.comment.models import Comment 19 | 20 | module = Blueprint('entity', __name__) 21 | 22 | def log_error(*args, **kwargs): 23 | current_app.logger.error(*args, **kwargs) 24 | 25 | 26 | @module.route('/', methods=['GET']) 27 | @module.route('/page//', methods=['GET']) 28 | def index(page=1): 29 | entities = None 30 | try: 31 | entities = Entity.query.paginate(page, 1, True) 32 | except SQLAlchemyError as e: 33 | log_error('Error while querying database', exc_info=e) 34 | flash('There was uncaught database query', 'danger') 35 | abort(500) 36 | return render_template('entity/index.html', object_list=entities) 37 | 38 | 39 | @module.route('//view/', methods=['GET']) 40 | def view(id): 41 | entity = None 42 | cmt_form = CommentAddForm(request.form) 43 | try: 44 | entity = db.session.\ 45 | query(Entity).\ 46 | filter(Entity.id == id).\ 47 | options(joinedload(Entity.comments)).\ 48 | first() 49 | except SQLAlchemyError as e: 50 | log_error('Error while querying database', exc_info=e) 51 | flash('There was error while querying database', 'danger') 52 | abort(500) 53 | return render_template('entity/view.html', object=entity, form=cmt_form) 54 | 55 | 56 | @module.route('/create/', methods=['GET', 'POST']) 57 | def create(): 58 | form = EntityCreateForm(request.form) 59 | try: 60 | if request.method == 'POST' and form.validate(): 61 | entity = Entity(**form.data) 62 | db.session.add(entity) 63 | db.session.flush() 64 | id = entity.id 65 | db.session.commit() 66 | flash('The entity was successfully added!', 'success') 67 | return redirect(url_for('entity.view', id=id)) 68 | except SQLAlchemyError as e: 69 | log_error('There was error while querying database', exc_info=e) 70 | db.session.rollback() 71 | flash('There was error while querying database', 'error') 72 | return render_template('entity/create.html', form=form) 73 | 74 | 75 | @module.route('//update/', methods=['GET', 'POST']) 76 | def update(id): 77 | form = EntityCreateForm(request.form) 78 | entity = Entity.query.get_or_404(id) 79 | try: 80 | if request.method == 'POST' and form.validate_on_submit(): 81 | for key, val in form.data.items(): 82 | if hasattr(entity, key): 83 | setattr(entity, key, val) 84 | db.session.commit() 85 | flash('Entity successful updated!', 'success') 86 | return redirect(url_for('entity.view', id=id)) 87 | else: 88 | form = EntityCreateForm(**sqlalchemy_orm_to_dict(entity)) 89 | except SQLAlchemyError as e: 90 | db.session.rollback() 91 | log_error('Uncaught exception while ' 92 | 'querying database at entity.update', exc_info=e) 93 | flash('Uncaught error while querying database', 'danger') 94 | abort(500) 95 | return render_template('entity/update.html', form=form, id=id) 96 | 97 | 98 | @module.route('/view//remove/', methods=['GET', 'POST']) 99 | def remove(id): 100 | entity = None 101 | try: 102 | if request.method == 'POST': 103 | entity = Entity.query.filter_by(id=id).first_or_404() 104 | Comment.query.filter(Comment.entity_id == entity.id).delete() 105 | db.session.delete(entity) 106 | db.session.commit() 107 | flash('Entity was successful removed!', 'success') 108 | return redirect(url_for('entity.index')) 109 | else: 110 | entity = Entity.query.get_or_404(id) 111 | except SQLAlchemyError as e: 112 | db.session.rollback() 113 | log_error('Uncaught exception ' 114 | 'while querying database at entity.remove', exc_info=e) 115 | flash('Uncaught exception while querying database', 'danger') 116 | abort(500) 117 | return render_template('entity/delete.html', object=entity) 118 | -------------------------------------------------------------------------------- /app/entity/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import ( 3 | StringField, 4 | TextAreaField, 5 | ) 6 | from wtforms.validators import DataRequired 7 | 8 | class EntityCreateForm(Form): 9 | name = StringField( 10 | 'Name', 11 | [ 12 | DataRequired(message="This field is required") 13 | ], 14 | description="Name of the entity" 15 | ) 16 | content = TextAreaField( 17 | 'Content', 18 | [], 19 | description="Content of the entity", 20 | ) 21 | -------------------------------------------------------------------------------- /app/entity/models.py: -------------------------------------------------------------------------------- 1 | from slugify import slugify 2 | from sqlalchemy import event 3 | 4 | from app.database import db 5 | 6 | class Entity(db.Model): 7 | __tablename__ = 'entity' 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | name = db.Column(db.String(1000), nullable=False, unique=True) 11 | slug = db.Column(db.String(1000)) 12 | content = db.Column(db.String(5000)) 13 | 14 | comments = db.relationship('Comment', backref='entity') 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | 20 | @event.listens_for(Entity, 'before_insert') 21 | def event_before_insert(mapper, connection, target): 22 | # Здесь будет очень важная бизнес логика 23 | # Или нет. На самом деле, старайтесь использовать сигналы только 24 | # тогда, когда других, более правильных вариантов не осталось. 25 | target.slug = slugify(target.name) 26 | 27 | 28 | @event.listens_for(Entity, 'before_update') 29 | def event_before_update(mapper, connection, target): 30 | target.slug = slugify(target.name) 31 | -------------------------------------------------------------------------------- /app/general/controllers.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | ) 5 | 6 | module = Blueprint('general', __name__) 7 | 8 | 9 | @module.app_errorhandler(404) 10 | def handle_404(err): 11 | return render_template('404.html'), 404 12 | 13 | 14 | @module.app_errorhandler(500) 15 | def handle_500(err): 16 | return render_template('500.html'), 500 -------------------------------------------------------------------------------- /app/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | body { 6 | font-family: Georgia, "Times New Roman", Times, serif; 7 | color: #555; 8 | } 9 | 10 | h1, .h1, 11 | h2, .h2, 12 | h3, .h3, 13 | h4, .h4, 14 | h5, .h5, 15 | h6, .h6 { 16 | margin-top: 0; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | font-weight: normal; 19 | color: #333; 20 | } 21 | 22 | 23 | /* 24 | * Override Bootstrap's default container. 25 | */ 26 | 27 | @media (min-width: 1200px) { 28 | .container { 29 | width: 970px; 30 | } 31 | } 32 | 33 | 34 | /* 35 | * Masthead for nav 36 | */ 37 | 38 | .blog-masthead { 39 | background-color: #428bca; 40 | -webkit-box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 41 | box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 42 | } 43 | 44 | /* Nav links */ 45 | .blog-nav-item { 46 | position: relative; 47 | display: inline-block; 48 | padding: 10px; 49 | font-weight: 500; 50 | color: #cdddeb; 51 | } 52 | .blog-nav-item:hover, 53 | .blog-nav-item:focus { 54 | color: #fff; 55 | text-decoration: none; 56 | } 57 | 58 | /* Active state gets a caret at the bottom */ 59 | .blog-nav .active { 60 | color: #fff; 61 | } 62 | .blog-nav .active:after { 63 | position: absolute; 64 | bottom: 0; 65 | left: 50%; 66 | width: 0; 67 | height: 0; 68 | margin-left: -5px; 69 | vertical-align: middle; 70 | content: " "; 71 | border-right: 5px solid transparent; 72 | border-bottom: 5px solid; 73 | border-left: 5px solid transparent; 74 | } 75 | 76 | 77 | /* 78 | * Blog name and description 79 | */ 80 | 81 | .blog-header { 82 | padding-top: 20px; 83 | padding-bottom: 20px; 84 | } 85 | .blog-title { 86 | margin-top: 30px; 87 | margin-bottom: 0; 88 | font-size: 60px; 89 | font-weight: normal; 90 | } 91 | .blog-description { 92 | font-size: 20px; 93 | color: #999; 94 | } 95 | 96 | 97 | /* 98 | * Main column and sidebar layout 99 | */ 100 | 101 | .blog-main { 102 | font-size: 18px; 103 | line-height: 1.5; 104 | } 105 | 106 | /* Sidebar modules for boxing content */ 107 | .sidebar-module { 108 | padding: 15px; 109 | margin: 0 -15px 15px; 110 | } 111 | .sidebar-module-inset { 112 | padding: 15px; 113 | background-color: #f5f5f5; 114 | border-radius: 4px; 115 | } 116 | .sidebar-module-inset p:last-child, 117 | .sidebar-module-inset ul:last-child, 118 | .sidebar-module-inset ol:last-child { 119 | margin-bottom: 0; 120 | } 121 | 122 | 123 | /* Pagination */ 124 | .pager { 125 | margin-bottom: 60px; 126 | text-align: left; 127 | } 128 | .pager > li > a { 129 | width: 140px; 130 | padding: 10px 20px; 131 | text-align: center; 132 | border-radius: 30px; 133 | } 134 | 135 | 136 | /* 137 | * Blog posts 138 | */ 139 | 140 | .blog-post { 141 | margin-bottom: 60px; 142 | } 143 | .blog-post-title { 144 | margin-bottom: 5px; 145 | font-size: 40px; 146 | } 147 | .blog-post-meta { 148 | margin-bottom: 20px; 149 | color: #999; 150 | } 151 | 152 | 153 | /* 154 | * Footer 155 | */ 156 | 157 | .blog-footer { 158 | padding: 40px 0; 159 | color: #999; 160 | text-align: center; 161 | background-color: #f9f9f9; 162 | border-top: 1px solid #e5e5e5; 163 | } 164 | .blog-footer p:last-child { 165 | margin-bottom: 0; 166 | } 167 | -------------------------------------------------------------------------------- /app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bosha/flask-app-structure-example/3f6fc53617c299f9163ac3134ccf5461acb22615/app/static/img/favicon.ico -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block meta_title %} 404 - Page not found {% endblock %} 4 | 5 | {% block content %} 6 |

The page you requested was not found.

7 | {% endblock content %} 8 | -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block meta_title %} Server error {% endblock %} 4 | 5 | {% block content %} 6 |

There was errors while processing your query. Please try again or contact administrator.

7 | {% endblock content %} 8 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block meta_title %}{% endblock meta_title %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 34 |
35 |
36 | 37 | {% block pre_container %} 38 |
39 | 40 |
41 |

The Bootstrap Blog

42 |

The official example template of creating a blog with Bootstrap.

43 |
44 | 45 |
46 | 47 | {% block pre_content %} 48 |
49 | 50 | {% include "messages.html" %} 51 | 52 | {% block content %} 53 | {% endblock %} 54 | 55 |
56 | {% endblock pre_content %} 57 | 58 | {% block pre_sidebar %} 59 |
60 | 64 | 81 | 89 |
90 | {% endblock pre_sidebar %} 91 | 92 | {% block pagination %} 93 | {% endblock pagination %} 94 | 95 |
96 | 97 | 98 |
99 | {% endblock pre_container %} 100 | 101 | 107 | 108 | 109 | 111 | 112 | 113 | {% block extra_js %} 114 | {% endblock %} 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/templates/entity/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block meta_title %} Create a new entity {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% include 'form-errors.html' %} 8 | {% import 'form-macros.html' as macros %} 9 | 10 |
11 | {% call macros.render_form(form, action_url=url_for("entity.create"), action_text="Create", btn_class="btn btn-primary") %} 12 | {{ macros.render_field(form.name, label_visible=True, placeholder="Name", type="text", show_help=True) }} 13 | {{ macros.render_field(form.content, label_visible=True, placeholder="Content", type="text", show_help=True) }} 14 | {% endcall %} 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/entity/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block meta_title %} Confirm entity removing {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% include "messages.html" %} 8 | 9 | {% if object %} 10 |
11 |
12 |
Information
13 | 14 |
15 |
16 |
ID:
17 |
{{ object.id }}
18 | 19 |
Name:
20 |
{{ object.name }}
21 |
22 |
23 | 24 | 32 |
33 | 34 |
35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/templates/entity/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block meta_title %} List of all entities {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if object_list.items %} 8 | {% for entity in object_list.items %} 9 |
10 |

{{ entity.name }}

11 |
12 | {{ entity.content }} 13 |
14 |
15 | {% endfor %} 16 | {% else %} 17 |
There is no posts added yet. Wanna add some?
18 | {% endif %} 19 | 20 | {% endblock %} 21 | 22 | {% block pagination %} 23 | {% import "pagination.html" as pagination_macros %} 24 | {{ pagination_macros.render_pagination(object_list, 'entity.index') }} 25 | {% endblock pagination %} -------------------------------------------------------------------------------- /app/templates/entity/update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block meta_title %} Update entity #{{ id }} {% endblock meta_title %} 4 | 5 | {% block content %} 6 | 7 | {% include 'form-errors.html' %} 8 | {% import 'form-macros.html' as macros %} 9 | 10 |
11 | {% call macros.render_form(form, action_url=url_for("entity.update", id=id), action_text="Update", btn_class="btn btn-warning") %} 12 | {{ macros.render_field(form.name, label_visible=True, placeholder="Name of the entity", type="text", show_help=True) }} 13 | {{ macros.render_field(form.content, label_visible=True, placeholder="Entity content", type="text", show_help=True) }} 14 | {% endcall %} 15 |
16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/entity/view.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

{{ object.name }}

6 |
7 | {{ object.content }} 8 |
9 |
10 | 14 |
15 | 16 |

Comments!

17 | {% if object.comments|length > 0 %} 18 | {% for comment in object.comments %} 19 | {% if comment.email %} 20 | {{ comment.name }} 21 | {% else %} 22 | {{ commment.name }} 23 | {% endif %} 24 |
25 | {{ comment.content }} 26 |
27 | {% endfor %} 28 | {% else %} 29 |
There is no comment yet. Be the first!
30 | {% endif %} 31 | 32 |

Submit your comment!

33 | {% include 'form-errors.html' %} 34 | {% import 'form-macros.html' as macros %} 35 | {% call macros.render_form(form, action_url=url_for("comment.add"), action_text="Comment!", btn_class="btn btn-primary") %} 36 | {{ macros.render_field(form.name, label_visible=True, placeholder="Name", type="text", show_help=True) }} 37 | {{ macros.render_field(form.email, label_visible=True, placeholder="Name", type="text", show_help=True) }} 38 | {{ macros.render_field(form.content, label_visible=True, placeholder="Content", type="text", show_help=True) }} 39 | {{ macros.render_field(form.entity_id, label_visible=False, type="hidden", show_help=False, value=object.id) }} 40 | {% endcall %} 41 | 42 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/form-errors.html: -------------------------------------------------------------------------------- 1 | {% if form.errors is defined %} 2 | {% for field, errors in form.errors.items() %} 3 |
4 | {{ form[field].label.text }}: {{ ', '.join(errors) }} 5 |
6 | {% endfor %} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /app/templates/form-macros.html: -------------------------------------------------------------------------------- 1 | {# Renders field for bootstrap 3 standards. 2 | 3 | Params: 4 | field - WTForm field 5 | kwargs - pass any arguments you want in order to put them into the html attributes. 6 | There are few exceptions: for - for_, class - class_, class__ - class_ 7 | 8 | Example usage: 9 | {{ macros.render_field(form.email, placeholder='Input email', type='email') }} 10 | #} 11 | {% macro render_field(field, label_visible=true, glyphicon=None, inline_errors=False, show_help=False) -%} 12 | 13 |
14 | {% if (field.type != 'HiddenField' and field.type !='CSRFTokenField') and label_visible %} 15 | 16 | {% endif %} 17 | {% if glyphicon %} 18 |
19 | 20 | 21 | 22 | {% endif %} 23 | {{ field(class_='form-control', **kwargs) }} 24 | {% if field.description and show_help == True %} 25 | {{ field.description }} 26 | {% endif %} 27 | {% if field.errors and inline_errors %} 28 | {% for e in field.errors %} 29 |

{{ e }}

30 | {% endfor %} 31 | {% endif %} 32 | {% if glyphicon %} 33 |
34 | {% endif %} 35 |
36 | {%- endmacro %} 37 | 38 | {# Renders checkbox fields since they are represented differently in bootstrap 39 | Params: 40 | field - WTForm field (there are no check, but you should put here only BooleanField. 41 | kwargs - pass any arguments you want in order to put them into the html attributes. 42 | There are few exceptions: for - for_, class - class_, class__ - class_ 43 | 44 | Example usage: 45 | {{ macros.render_checkbox_field(form.remember_me) }} 46 | #} 47 | {% macro render_checkbox_field(field) -%} 48 |
49 | 52 |
53 | {%- endmacro %} 54 | 55 | {# Renders radio field 56 | Params: 57 | field - WTForm field (there are no check, but you should put here only BooleanField. 58 | kwargs - pass any arguments you want in order to put them into the html attributes. 59 | There are few exceptions: for - for_, class - class_, class__ - class_ 60 | 61 | Example usage: 62 | {{ macros.render_radio_field(form.answers) }} 63 | #} 64 | {% macro render_radio_field(field) -%} 65 | {% for value, label, _ in field.iter_choices() %} 66 |
67 | 70 |
71 | {% endfor %} 72 | {%- endmacro %} 73 | 74 | {# Renders WTForm in bootstrap way. There are two ways to call function: 75 | - as macros: it will render all field forms using cycle to iterate over them 76 | - as call: it will insert form fields as you specify: 77 | e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login', 78 | class_='login-form') %} 79 | {{ macros.render_field(form.email, placeholder='Input email', type='email') }} 80 | {{ macros.render_field(form.password, placeholder='Input password', type='password') }} 81 | {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }} 82 | {% endcall %} 83 | 84 | Params: 85 | form - WTForm class 86 | action_url - url where to submit this form 87 | action_text - text of submit button 88 | class_ - sets a class for form 89 | #} 90 | {% macro render_form(form, action_url='', method="POST", action_text='Submit', class_='', btn_class='btn btn-default') -%} 91 | 92 |
93 | {{ form.csrf_token }} 94 | {% if caller %} 95 | {{ caller() }} 96 | {% else %} 97 | {% for f in form %} 98 | {% if f.type == 'BooleanField' %} 99 | {{ render_checkbox_field(f) }} 100 | {% elif f.type == 'RadioField' %} 101 | {{ render_radio_field(f) }} 102 | {% else %} 103 | {{ render_field(f) }} 104 | {% endif %} 105 | {% endfor %} 106 | {% endif %} 107 | 108 | {% if xsrf_token %} 109 | {{ xsrf_form_html()|safe }} 110 | {% endif %} 111 | 112 | 113 |
114 | {%- endmacro %} 115 | -------------------------------------------------------------------------------- /app/templates/messages.html: -------------------------------------------------------------------------------- 1 | {% with messages = get_flashed_messages(with_categories=true) %} 2 | {% if messages %} 3 | {% for category, message in messages %} 4 |
{{ message }}
5 | {% endfor %} 6 | {% endif %} 7 | {% endwith %} -------------------------------------------------------------------------------- /app/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, endpoint, ep_kwargs={}) %} 2 | {% if pagination.pages > 1 %} 3 |
4 | 21 |
22 | {% endif %} 23 | {% endmacro %} -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'bosha' 2 | -------------------------------------------------------------------------------- /app/utils/db.py: -------------------------------------------------------------------------------- 1 | def sqlalchemy_orm_to_dict(model): 2 | """ 3 | Converts sqlalchemy model to dictionary 4 | :param model: declarative sqlalchemy model 5 | :return: Sqlalchemy model as dictionary 6 | :rtype: dict 7 | :raise RuntimeError: if passed object not a sqlalchemy model 8 | """ 9 | if not hasattr(model, '__table__') or not hasattr(model.__table__, 'columns'): 10 | raise RuntimeError( 11 | "{} not JSON serializable. Probably, not sqlalchemy model?".format(model.__name__) 12 | ) 13 | 14 | def columns(): 15 | return [c.name for c in model.__table__.columns] 16 | 17 | return dict([(c, getattr(model, c)) for c in columns()]) 18 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Config(object): 4 | # Определяет, включен ли режим отладки 5 | # В случае если включен, flask будет показывать 6 | # подробную отладочную информацию. Если выключен - 7 | # - 500 ошибку без какой либо дополнительной информации. 8 | DEBUG = False 9 | # Включение защиты против "Cross-site Request Forgery (CSRF)" 10 | CSRF_ENABLED = True 11 | # Случайный ключ, которые будет исползоваться для подписи 12 | # данных, например cookies. 13 | SECRET_KEY = 'YOUR_RANDOM_SECRET_KEY' 14 | # URI используемая для подключения к базе данных 15 | SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | 18 | 19 | class ProductionConfig(Config): 20 | DEBUG = False 21 | 22 | 23 | class DevelopmentConfig(Config): 24 | DEVELOPMENT = True 25 | DEBUG = True 26 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from flask_script import Manager 4 | from flask.ext.migrate import Migrate, MigrateCommand 5 | 6 | from app import create_app 7 | from app.database import db 8 | 9 | app = create_app() 10 | app.config.from_object(os.environ['APP_SETTINGS']) 11 | manager = Manager(app) 12 | migrate = Migrate(app, db) 13 | 14 | manager.add_command('db', MigrateCommand) 15 | 16 | if __name__ == '__main__': 17 | manager.run() 18 | -------------------------------------------------------------------------------- /requipments/base.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | Flask==0.11.1 3 | Flask-Script==2.0.5 4 | Flask-SQLAlchemy==2.1 5 | Flask-WTF==0.12 6 | itsdangerous==0.24 7 | Jinja2==2.8 8 | MarkupSafe==0.23 9 | psycopg2==2.6.1 10 | SQLAlchemy==1.0.13 11 | Werkzeug==0.11.10 12 | WTForms==2.1 13 | Flask-Migrate==1.8.0 14 | python-slugify==1.2.0 15 | -------------------------------------------------------------------------------- /requipments/development.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | Flask-DebugToolbar==0.10.0 3 | -------------------------------------------------------------------------------- /requipments/production.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | gunicorn==19.6.0 3 | --------------------------------------------------------------------------------