├── .gitignore
├── flask-crud-part-two.jpg
├── app
├── static
│ ├── img
│ │ ├── favicon.ico
│ │ └── intro-bg.jpg
│ └── css
│ │ └── style.css
├── admin
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
├── auth
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
├── home
│ ├── __init__.py
│ └── views.py
├── templates
│ ├── auth
│ │ ├── register.html
│ │ └── login.html
│ ├── home
│ │ ├── dashboard.html
│ │ ├── index.html
│ │ └── admin_dashboard.html
│ ├── admin
│ │ ├── roles
│ │ │ ├── role.html
│ │ │ └── roles.html
│ │ ├── departments
│ │ │ ├── department.html
│ │ │ └── departments.html
│ │ └── employees
│ │ │ ├── employee.html
│ │ │ └── employees.html
│ └── base.html
├── __init__.py
└── models.py
├── run.py
├── README.md
├── requirements.txt
└── config.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | instance/
4 | migrations/
5 |
--------------------------------------------------------------------------------
/flask-crud-part-two.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mbithenzomo/project-dream-team-two/HEAD/flask-crud-part-two.jpg
--------------------------------------------------------------------------------
/app/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mbithenzomo/project-dream-team-two/HEAD/app/static/img/favicon.ico
--------------------------------------------------------------------------------
/app/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | admin = Blueprint('admin', __name__)
4 |
5 | from . import views
6 |
--------------------------------------------------------------------------------
/app/auth/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | auth = Blueprint('auth', __name__)
4 |
5 | from . import views
6 |
--------------------------------------------------------------------------------
/app/home/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | home = Blueprint('home', __name__)
4 |
5 | from . import views
6 |
--------------------------------------------------------------------------------
/app/static/img/intro-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mbithenzomo/project-dream-team-two/HEAD/app/static/img/intro-bg.jpg
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from app import create_app
4 |
5 | config_name = os.getenv('FLASK_CONFIG')
6 | app = create_app(config_name)
7 |
8 | if __name__ == '__main__':
9 | app.run()
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | The code for Part Two of my three-part tutorial, *Build a CRUD Web App With Python and Flask*.
4 |
--------------------------------------------------------------------------------
/app/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% import "bootstrap/utils.html" as utils %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 | {% extends "base.html" %}
4 | {% block title %}Register{% endblock %}
5 | {% block body %}
6 |
7 |
8 |
Register for an account
9 |
10 | {{ wtf.quick_form(form) }}
11 |
12 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==0.8.9
2 | click==6.6
3 | dominate==2.3.1
4 | Flask==0.11.1
5 | Flask-Bootstrap==3.3.7.0
6 | Flask-Login==0.4.0
7 | Flask-Migrate==2.0.1
8 | Flask-Script==2.0.5
9 | Flask-SQLAlchemy==2.1
10 | Flask-WTF==0.13.1
11 | itsdangerous==0.24
12 | Jinja2==2.8
13 | Mako==1.0.6
14 | MarkupSafe==0.23
15 | MySQL-python==1.2.5
16 | python-editor==1.0.3
17 | SQLAlchemy==1.1.4
18 | visitor==0.1.3
19 | Werkzeug==0.11.11
20 | WTForms==2.1
21 |
--------------------------------------------------------------------------------
/app/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% import "bootstrap/utils.html" as utils %}
2 | {% import "bootstrap/wtf.html" as wtf %}
3 | {% extends "base.html" %}
4 | {% block title %}Login{% endblock %}
5 | {% block body %}
6 |
7 |
8 | {{ utils.flashed_messages() }}
9 |
10 |
11 |
Login to your account
12 |
13 | {{ wtf.quick_form(form) }}
14 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | class Config(object):
2 | """
3 | Common configurations
4 | """
5 |
6 | # Put any configurations here that are common across all environments
7 |
8 |
9 | class DevelopmentConfig(Config):
10 | """
11 | Development configurations
12 | """
13 |
14 | DEBUG = True
15 | SQLALCHEMY_ECHO = True
16 |
17 |
18 | class ProductionConfig(Config):
19 | """
20 | Production configurations
21 | """
22 |
23 | DEBUG = False
24 |
25 | app_config = {
26 | 'development': DevelopmentConfig,
27 | 'production': ProductionConfig
28 | }
29 |
--------------------------------------------------------------------------------
/app/templates/home/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Dashboard{% endblock %}
3 | {% block body %}
4 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/app/templates/home/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Home{% endblock %}
3 | {% block body %}
4 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/app/templates/home/admin_dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Admin Dashboard{% endblock %}
3 | {% block body %}
4 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/app/templates/admin/roles/role.html:
--------------------------------------------------------------------------------
1 | {% import "bootstrap/wtf.html" as wtf %}
2 | {% extends "base.html" %}
3 | {% block title %}
4 | {% if add_department %}
5 | Add Role
6 | {% else %}
7 | Edit Role
8 | {% endif %}
9 | {% endblock %}
10 | {% block body %}
11 |
6 |
7 |
8 |
9 |
10 | {{ utils.flashed_messages() }}
11 |
12 |
Roles
13 | {% if roles %}
14 |
15 |
16 |
17 |
18 |
19 | | Name |
20 | Description |
21 | Employee Count |
22 | Edit |
23 | Delete |
24 |
25 |
26 |
27 | {% for role in roles %}
28 |
29 | | {{ role.name }} |
30 | {{ role.description }} |
31 |
32 | {% if role.employees %}
33 | {{ role.employees.count() }}
34 | {% else %}
35 | 0
36 | {% endif %}
37 | |
38 |
39 |
40 | Edit
41 |
42 | |
43 |
44 |
45 | Delete
46 |
47 | |
48 |
49 | {% endfor %}
50 |
51 |
52 |
53 |
54 | {% else %}
55 |
64 |
65 |
66 |
67 |
68 | {% endblock %}
69 |
--------------------------------------------------------------------------------
/app/templates/admin/departments/departments.html:
--------------------------------------------------------------------------------
1 | {% import "bootstrap/utils.html" as utils %}
2 | {% extends "base.html" %}
3 | {% block title %}Departments{% endblock %}
4 | {% block body %}
5 |
6 |
7 |
8 |
9 |
10 | {{ utils.flashed_messages() }}
11 |
12 |
Departments
13 | {% if departments %}
14 |
15 |
16 |
17 |
18 |
19 | | Name |
20 | Description |
21 | Employee Count |
22 | Edit |
23 | Delete |
24 |
25 |
26 |
27 | {% for department in departments %}
28 |
29 | | {{ department.name }} |
30 | {{ department.description }} |
31 |
32 | {% if department.employees %}
33 | {{ department.employees.count() }}
34 | {% else %}
35 | 0
36 | {% endif %}
37 | |
38 |
39 |
40 | Edit
41 |
42 | |
43 |
44 |
45 | Delete
46 |
47 | |
48 |
49 | {% endfor %}
50 |
51 |
52 |
53 |
54 | {% else %}
55 |
64 |
65 |
66 |
67 |
68 | {% endblock %}
69 |
--------------------------------------------------------------------------------
/app/static/css/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | body, h1, h2, h3 {
7 | font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
8 | font-weight: 700;
9 | }
10 |
11 | a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a {
12 | color: #aec251;
13 | }
14 |
15 | a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover {
16 | color: #687430;
17 | }
18 |
19 | footer {
20 | padding: 50px 0;
21 | background-color: #f8f8f8;
22 | }
23 |
24 | p.copyright {
25 | margin: 15px 0 0;
26 | }
27 |
28 | .alert-info {
29 | width: 50%;
30 | margin: auto;
31 | color: #687430;
32 | background-color: #e6ecca;
33 | border-color: #aec251;
34 | }
35 |
36 | .btn-default {
37 | border-color: #aec251;
38 | color: #aec251;
39 | }
40 |
41 | .btn-default:hover {
42 | background-color: #aec251;
43 | }
44 |
45 | .center {
46 | margin: auto;
47 | width: 50%;
48 | padding: 10px;
49 | }
50 |
51 | .content-section {
52 | padding: 50px 0;
53 | border-top: 1px solid #e7e7e7;
54 | }
55 |
56 | .footer, .push {
57 | clear: both;
58 | height: 4em;
59 | }
60 |
61 | .intro-divider {
62 | width: 400px;
63 | border-top: 1px solid #f8f8f8;
64 | border-bottom: 1px solid rgba(0,0,0,0.2);
65 | }
66 |
67 | .intro-header {
68 | padding-top: 50px;
69 | padding-bottom: 50px;
70 | text-align: center;
71 | color: #f8f8f8;
72 | background: url(../img/intro-bg.jpg) no-repeat center center;
73 | background-size: cover;
74 | height: 100%;
75 | }
76 |
77 | .intro-message {
78 | position: relative;
79 | padding-top: 20%;
80 | padding-bottom: 20%;
81 | }
82 |
83 | .intro-message > h1 {
84 | margin: 0;
85 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
86 | font-size: 5em;
87 | }
88 |
89 | .intro-message > h3 {
90 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
91 | }
92 |
93 | .lead {
94 | font-size: 18px;
95 | font-weight: 400;
96 | }
97 |
98 | .topnav {
99 | font-size: 14px;
100 | }
101 |
102 | .wrapper {
103 | min-height: 100%;
104 | height: auto !important;
105 | height: 100%;
106 | margin: 0 auto -4em;
107 | }
108 |
109 | .outer {
110 | display: table;
111 | position: absolute;
112 | height: 70%;
113 | width: 100%;
114 | }
115 |
116 | .middle {
117 | display: table-cell;
118 | vertical-align: middle;
119 | }
120 |
121 | .inner {
122 | margin-left: auto;
123 | margin-right: auto;
124 | }
125 |
--------------------------------------------------------------------------------
/app/templates/admin/employees/employees.html:
--------------------------------------------------------------------------------
1 | {% import "bootstrap/utils.html" as utils %}
2 | {% extends "base.html" %}
3 | {% block title %}Employees{% endblock %}
4 | {% block body %}
5 |
6 |
7 |
8 |
9 |
10 | {{ utils.flashed_messages() }}
11 |
12 |
Employees
13 | {% if employees %}
14 |
15 |
16 |
17 |
18 |
19 | | Name |
20 | Department |
21 | Role |
22 | Assign |
23 |
24 |
25 |
26 | {% for employee in employees %}
27 | {% if employee.is_admin %}
28 |
29 | | Admin |
30 | N/A |
31 | N/A |
32 | N/A |
33 |
34 | {% else %}
35 |
36 | | {{ employee.first_name }} {{ employee.last_name }} |
37 |
38 | {% if employee.department %}
39 | {{ employee.department.name }}
40 | {% else %}
41 | -
42 | {% endif %}
43 | |
44 |
45 | {% if employee.role %}
46 | {{ employee.role.name }}
47 | {% else %}
48 | -
49 | {% endif %}
50 | |
51 |
52 |
53 | Assign
54 |
55 | |
56 |
57 | {% endif %}
58 | {% endfor %}
59 |
60 |
61 |
62 | {% endif %}
63 |
64 |
65 |
66 |
67 |
68 | {% endblock %}
69 |
--------------------------------------------------------------------------------
/app/models.py:
--------------------------------------------------------------------------------
1 | from flask_login import UserMixin
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 |
4 | from app import db, login_manager
5 |
6 |
7 | class Employee(UserMixin, db.Model):
8 | """
9 | Create an Employee table
10 | """
11 |
12 | # Ensures table will be named in plural and not in singular
13 | # as is the name of the model
14 | __tablename__ = 'employees'
15 |
16 | id = db.Column(db.Integer, primary_key=True)
17 | email = db.Column(db.String(60), index=True, unique=True)
18 | username = db.Column(db.String(60), index=True, unique=True)
19 | first_name = db.Column(db.String(60), index=True)
20 | last_name = db.Column(db.String(60), index=True)
21 | password_hash = db.Column(db.String(128))
22 | department_id = db.Column(db.Integer, db.ForeignKey('departments.id'))
23 | role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
24 | is_admin = db.Column(db.Boolean, default=False)
25 |
26 | @property
27 | def password(self):
28 | """
29 | Prevent pasword from being accessed
30 | """
31 | raise AttributeError('password is not a readable attribute.')
32 |
33 | @password.setter
34 | def password(self, password):
35 | """
36 | Set password to a hashed password
37 | """
38 | self.password_hash = generate_password_hash(password)
39 |
40 | def verify_password(self, password):
41 | """
42 | Check if hashed password matches actual password
43 | """
44 | return check_password_hash(self.password_hash, password)
45 |
46 | def __repr__(self):
47 | return '
'.format(self.username)
48 |
49 |
50 | # Set up user_loader
51 | @login_manager.user_loader
52 | def load_user(user_id):
53 | return Employee.query.get(int(user_id))
54 |
55 |
56 | class Department(db.Model):
57 | """
58 | Create a Department table
59 | """
60 |
61 | __tablename__ = 'departments'
62 |
63 | id = db.Column(db.Integer, primary_key=True)
64 | name = db.Column(db.String(60), unique=True)
65 | description = db.Column(db.String(200))
66 | employees = db.relationship('Employee', backref='department',
67 | lazy='dynamic')
68 |
69 | def __repr__(self):
70 | return ''.format(self.name)
71 |
72 |
73 | class Role(db.Model):
74 | """
75 | Create a Role table
76 | """
77 |
78 | __tablename__ = 'roles'
79 |
80 | id = db.Column(db.Integer, primary_key=True)
81 | name = db.Column(db.String(60), unique=True)
82 | description = db.Column(db.String(200))
83 | employees = db.relationship('Employee', backref='role',
84 | lazy='dynamic')
85 |
86 | def __repr__(self):
87 | return ''.format(self.name)
88 |
--------------------------------------------------------------------------------
/app/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }} | Project Dream Team
5 |
6 |
7 |
8 |
9 |
10 |
11 |
44 |
45 | {% block body %}
46 | {% endblock %}
47 |
48 |
49 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/admin/views.py:
--------------------------------------------------------------------------------
1 | from flask import abort, flash, redirect, render_template, url_for
2 | from flask_login import current_user, login_required
3 |
4 | from . import admin
5 | from forms import DepartmentForm, EmployeeAssignForm, RoleForm
6 | from .. import db
7 | from ..models import Department, Employee, Role
8 |
9 |
10 | def check_admin():
11 | # prevent non-admins from accessing the page
12 | if not current_user.is_admin:
13 | abort(403)
14 |
15 |
16 | # Department Views
17 |
18 |
19 | @admin.route('/departments', methods=['GET', 'POST'])
20 | @login_required
21 | def list_departments():
22 | """
23 | List all departments
24 | """
25 | check_admin()
26 |
27 | departments = Department.query.all()
28 |
29 | return render_template('admin/departments/departments.html',
30 | departments=departments, title="Departments")
31 |
32 |
33 | @admin.route('/departments/add', methods=['GET', 'POST'])
34 | @login_required
35 | def add_department():
36 | """
37 | Add a department to the database
38 | """
39 | check_admin()
40 |
41 | add_department = True
42 |
43 | form = DepartmentForm()
44 | if form.validate_on_submit():
45 | department = Department(name=form.name.data,
46 | description=form.description.data)
47 | try:
48 | # add department to the database
49 | db.session.add(department)
50 | db.session.commit()
51 | flash('You have successfully added a new department.')
52 | except:
53 | # in case department name already exists
54 | flash('Error: department name already exists.')
55 |
56 | # redirect to departments page
57 | return redirect(url_for('admin.list_departments'))
58 |
59 | # load department template
60 | return render_template('admin/departments/department.html', action="Add",
61 | add_department=add_department, form=form,
62 | title="Add Department")
63 |
64 |
65 | @admin.route('/departments/edit/', methods=['GET', 'POST'])
66 | @login_required
67 | def edit_department(id):
68 | """
69 | Edit a department
70 | """
71 | check_admin()
72 |
73 | add_department = False
74 |
75 | department = Department.query.get_or_404(id)
76 | form = DepartmentForm(obj=department)
77 | if form.validate_on_submit():
78 | department.name = form.name.data
79 | department.description = form.description.data
80 | db.session.commit()
81 | flash('You have successfully edited the department.')
82 |
83 | # redirect to the departments page
84 | return redirect(url_for('admin.list_departments'))
85 |
86 | form.description.data = department.description
87 | form.name.data = department.name
88 | return render_template('admin/departments/department.html', action="Edit",
89 | add_department=add_department, form=form,
90 | department=department, title="Edit Department")
91 |
92 |
93 | @admin.route('/departments/delete/', methods=['GET', 'POST'])
94 | @login_required
95 | def delete_department(id):
96 | """
97 | Delete a department from the database
98 | """
99 | check_admin()
100 |
101 | department = Department.query.get_or_404(id)
102 | db.session.delete(department)
103 | db.session.commit()
104 | flash('You have successfully deleted the department.')
105 |
106 | # redirect to the departments page
107 | return redirect(url_for('admin.list_departments'))
108 |
109 | return render_template(title="Delete Department")
110 |
111 |
112 | # Role Views
113 |
114 |
115 | @admin.route('/roles')
116 | @login_required
117 | def list_roles():
118 | check_admin()
119 | """
120 | List all roles
121 | """
122 | roles = Role.query.all()
123 | return render_template('admin/roles/roles.html',
124 | roles=roles, title='Roles')
125 |
126 |
127 | @admin.route('/roles/add', methods=['GET', 'POST'])
128 | @login_required
129 | def add_role():
130 | """
131 | Add a role to the database
132 | """
133 | check_admin()
134 |
135 | add_role = True
136 |
137 | form = RoleForm()
138 | if form.validate_on_submit():
139 | role = Role(name=form.name.data,
140 | description=form.description.data)
141 |
142 | try:
143 | # add role to the database
144 | db.session.add(role)
145 | db.session.commit()
146 | flash('You have successfully added a new role.')
147 | except:
148 | # in case role name already exists
149 | flash('Error: role name already exists.')
150 |
151 | # redirect to the roles page
152 | return redirect(url_for('admin.list_roles'))
153 |
154 | # load role template
155 | return render_template('admin/roles/role.html', add_role=add_role,
156 | form=form, title='Add Role')
157 |
158 |
159 | @admin.route('/roles/edit/', methods=['GET', 'POST'])
160 | @login_required
161 | def edit_role(id):
162 | """
163 | Edit a role
164 | """
165 | check_admin()
166 |
167 | add_role = False
168 |
169 | role = Role.query.get_or_404(id)
170 | form = RoleForm(obj=role)
171 | if form.validate_on_submit():
172 | role.name = form.name.data
173 | role.description = form.description.data
174 | db.session.add(role)
175 | db.session.commit()
176 | flash('You have successfully edited the role.')
177 |
178 | # redirect to the roles page
179 | return redirect(url_for('admin.list_roles'))
180 |
181 | form.description.data = role.description
182 | form.name.data = role.name
183 | return render_template('admin/roles/role.html', add_role=add_role,
184 | form=form, title="Edit Role")
185 |
186 |
187 | @admin.route('/roles/delete/', methods=['GET', 'POST'])
188 | @login_required
189 | def delete_role(id):
190 | """
191 | Delete a role from the database
192 | """
193 | check_admin()
194 |
195 | role = Role.query.get_or_404(id)
196 | db.session.delete(role)
197 | db.session.commit()
198 | flash('You have successfully deleted the role.')
199 |
200 | # redirect to the roles page
201 | return redirect(url_for('admin.list_roles'))
202 |
203 | return render_template(title="Delete Role")
204 |
205 |
206 | # Employee Views
207 |
208 | @admin.route('/employees')
209 | @login_required
210 | def list_employees():
211 | """
212 | List all employees
213 | """
214 | check_admin()
215 |
216 | employees = Employee.query.all()
217 | return render_template('admin/employees/employees.html',
218 | employees=employees, title='Employees')
219 |
220 |
221 | @admin.route('/employees/assign/', methods=['GET', 'POST'])
222 | @login_required
223 | def assign_employee(id):
224 | """
225 | Assign a department and a role to an employee
226 | """
227 | check_admin()
228 |
229 | employee = Employee.query.get_or_404(id)
230 |
231 | # prevent admin from being assigned a department or role
232 | if employee.is_admin:
233 | abort(403)
234 |
235 | form = EmployeeAssignForm(obj=employee)
236 | if form.validate_on_submit():
237 | employee.department = form.department.data
238 | employee.role = form.role.data
239 | db.session.add(employee)
240 | db.session.commit()
241 | flash('You have successfully assigned a department and role.')
242 |
243 | # redirect to the roles page
244 | return redirect(url_for('admin.list_employees'))
245 |
246 | return render_template('admin/employees/employee.html',
247 | employee=employee, form=form,
248 | title='Assign Employee')
249 |
--------------------------------------------------------------------------------