├── app
├── utils
│ ├── __init__.py
│ └── db.py
├── database.py
├── static
│ ├── img
│ │ └── favicon.ico
│ └── css
│ │ └── styles.css
├── templates
│ ├── 404.html
│ ├── 500.html
│ ├── messages.html
│ ├── form-errors.html
│ ├── entity
│ │ ├── create.html
│ │ ├── update.html
│ │ ├── index.html
│ │ ├── delete.html
│ │ └── view.html
│ ├── pagination.html
│ ├── base.html
│ └── form-macros.html
├── general
│ └── controllers.py
├── entity
│ ├── forms.py
│ ├── models.py
│ └── controllers.py
├── comment
│ ├── models.py
│ ├── forms.py
│ └── controllers.py
└── __init__.py
├── requipments
├── production.txt
├── development.txt
└── base.txt
├── .env.example
├── .gitignore
├── manage.py
├── README.md
├── config.py
└── LICENSE
/app/utils/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'bosha'
2 |
--------------------------------------------------------------------------------
/requipments/production.txt:
--------------------------------------------------------------------------------
1 | -r base.txt
2 | gunicorn==19.6.0
3 |
--------------------------------------------------------------------------------
/requipments/development.txt:
--------------------------------------------------------------------------------
1 | -r base.txt
2 | Flask-DebugToolbar==0.10.0
3 |
--------------------------------------------------------------------------------
/app/database.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | db = SQLAlchemy()
3 |
--------------------------------------------------------------------------------
/app/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosha/flask-app-structure-example/HEAD/app/static/img/favicon.ico
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
35 | {% endif %}
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/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/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/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/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 |
36 |
37 | {% block pre_container %}
38 |
39 |
40 |
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 |
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/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 |
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 |
50 | {{ field(type='checkbox', **kwargs) }} {{ field.label }}
51 |
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 |
68 | {{ label }}
69 |
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 |
114 | {%- endmacro %}
115 |
--------------------------------------------------------------------------------