├── app
├── main
│ ├── forms.py
│ ├── __init__.py
│ ├── errors.py
│ └── views.py
├── admin
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
├── account
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
├── resources
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
├── static
│ ├── fonts
│ │ └── vendor
│ │ │ └── semantic
│ │ │ ├── icons.eot
│ │ │ ├── icons.otf
│ │ │ ├── icons.ttf
│ │ │ ├── icons.woff
│ │ │ └── icons.woff2
│ ├── images
│ │ └── vendor
│ │ │ └── semantic
│ │ │ └── flags.png
│ └── styles
│ │ └── app.css
├── templates
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── account
│ │ ├── email
│ │ │ ├── change_email.txt
│ │ │ ├── confirm.txt
│ │ │ ├── reset_password.txt
│ │ │ ├── invite.txt
│ │ │ ├── change_email.html
│ │ │ ├── confirm.html
│ │ │ ├── reset_password.html
│ │ │ └── invite.html
│ │ ├── join_invite.html
│ │ ├── reset_password.html
│ │ ├── unconfirmed.html
│ │ ├── donate.html
│ │ ├── login.html
│ │ ├── register.html
│ │ ├── manage.html
│ │ ├── edit_profile.html
│ │ └── profile.html
│ ├── partials
│ │ ├── _flashes.html
│ │ └── _head.html
│ ├── layouts
│ │ └── base.html
│ ├── main
│ │ ├── index.html
│ │ ├── map.html
│ │ └── help.html
│ ├── resources
│ │ ├── create_review.html
│ │ ├── index.html
│ │ ├── create_resource.html
│ │ └── read_resource.html
│ ├── admin
│ │ ├── index.html
│ │ ├── new_user.html
│ │ ├── manage_user.html
│ │ └── registered_users.html
│ └── macros
│ │ ├── form_macros.html
│ │ └── nav_macros.html
├── models
│ ├── __init__.py
│ ├── attribute.py
│ ├── resource.py
│ ├── location.py
│ └── user.py
├── utils.py
├── assets.py
├── decorators.py
├── email.py
├── assets
│ ├── styles
│ │ └── app.scss
│ └── scripts
│ │ ├── app.js
│ │ └── vendor
│ │ └── tablesort.min.js
└── __init__.py
├── Procfile
├── requirements.txt
├── requirements
├── dev.txt
└── common.txt
├── ss1.jpg
├── ss2.jpg
├── ss3.jpg
├── ss4.jpg
├── ss5.jpg
├── ss6.jpg
├── .gitignore
├── tests
├── test_basics.py
└── test_user_model.py
├── LICENSE.md
├── config.py
├── manage.py
└── README.md
/app/main/forms.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn manage:app
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -r requirements/common.txt
--------------------------------------------------------------------------------
/requirements/dev.txt:
--------------------------------------------------------------------------------
1 | -r common.txt
2 | fake-factory==0.5.3
3 |
--------------------------------------------------------------------------------
/ss1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss1.jpg
--------------------------------------------------------------------------------
/ss2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss2.jpg
--------------------------------------------------------------------------------
/ss3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss3.jpg
--------------------------------------------------------------------------------
/ss4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss4.jpg
--------------------------------------------------------------------------------
/ss5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss5.jpg
--------------------------------------------------------------------------------
/ss6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/ss6.jpg
--------------------------------------------------------------------------------
/app/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | admin = Blueprint('admin', __name__)
4 |
5 | from . import views # noqa
6 |
--------------------------------------------------------------------------------
/app/account/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | account = Blueprint('account', __name__)
4 |
5 | from . import views # noqa
6 |
--------------------------------------------------------------------------------
/app/main/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | main = Blueprint('main', __name__)
4 |
5 | from . import views, errors # noqa
6 |
--------------------------------------------------------------------------------
/app/resources/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | resources = Blueprint('resources', __name__)
4 |
5 | from . import views # noqa
6 |
--------------------------------------------------------------------------------
/app/static/fonts/vendor/semantic/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/fonts/vendor/semantic/icons.eot
--------------------------------------------------------------------------------
/app/static/fonts/vendor/semantic/icons.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/fonts/vendor/semantic/icons.otf
--------------------------------------------------------------------------------
/app/static/fonts/vendor/semantic/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/fonts/vendor/semantic/icons.ttf
--------------------------------------------------------------------------------
/app/static/fonts/vendor/semantic/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/fonts/vendor/semantic/icons.woff
--------------------------------------------------------------------------------
/app/static/fonts/vendor/semantic/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/fonts/vendor/semantic/icons.woff2
--------------------------------------------------------------------------------
/app/static/images/vendor/semantic/flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hack4impact-upenn/women-veterans-rock/HEAD/app/static/images/vendor/semantic/flags.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .webassets-cache/
2 | webassets-external/
3 | .DS_Store
4 | *.pyc
5 | *.pyo
6 | env
7 | venv
8 | env*
9 | dist
10 | *.egg
11 | *.egg-info
12 | *.sqlite
13 | .idea
14 |
--------------------------------------------------------------------------------
/app/templates/errors/403.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/errors/404.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/errors/500.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | These imports enable us to make all defined models members of the models
3 | module (as opposed to just their python files)
4 | """
5 |
6 | from user import * # noqa
7 | from location import * # noqa
8 | from resource import * # noqa
9 | from attribute import * # noqa
10 |
--------------------------------------------------------------------------------
/app/templates/account/email/change_email.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | To confirm your new email address click on the following link:
4 |
5 | {{ url_for('account.change_email', token=token, _external=True) }}
6 |
7 | Sincerely,
8 |
9 | The {{ config.APP_NAME }} Team
10 |
11 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/templates/account/email/confirm.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | Welcome to {{ config.APP_NAME }}!
4 |
5 | To confirm your account, please click on the following link:
6 |
7 | {{ url_for('account.confirm', token=token, _external=True) }}
8 |
9 | Sincerely,
10 |
11 | The {{ config.APP_NAME }} Team
12 |
13 | Note: replies to this email address are not monitored.
14 |
15 |
--------------------------------------------------------------------------------
/app/templates/account/email/reset_password.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | To reset your password, click on the following link:
4 |
5 | {{ url_for('account.reset_password', token=token, _external=True) }}
6 |
7 | If you have not requested a password reset, simply ignore this message.
8 |
9 | Sincerely,
10 |
11 | The {{ config.APP_NAME }} Team
12 |
13 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/templates/account/join_invite.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | {{ f.render_form(form) }}
9 |
10 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/account/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | {{ f.render_form(form) }}
9 |
10 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/app/main/errors.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 | from . import main
3 |
4 |
5 | @main.app_errorhandler(403)
6 | def forbidden(_):
7 | return render_template('errors/403.html'), 403
8 |
9 |
10 | @main.app_errorhandler(404)
11 | def page_not_found(_):
12 | return render_template('errors/404.html'), 404
13 |
14 |
15 | @main.app_errorhandler(500)
16 | def internal_server_error(_):
17 | return render_template('errors/500.html'), 500
18 |
--------------------------------------------------------------------------------
/app/templates/account/email/invite.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | You are invited to join {{ config.APP_NAME }}!
4 |
5 | To set your password, please click on the following link:
6 |
7 | {{ url_for('account.join_from_invite', user_id=user_id, token=token, _external=True) }}
8 |
9 | Once completed, please log in as {{ user.email }} with the password you set.
10 |
11 | Sincerely,
12 |
13 | The {{ config.APP_NAME }} Team
14 |
15 | Note: replies to this email address are not monitored.
16 |
17 |
--------------------------------------------------------------------------------
/app/templates/account/email/change_email.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | To confirm your new email address click here .
4 |
5 | Alternatively, you can paste the following link in your browser's address bar:
6 |
7 | {{ url_for('account.change_email', token=token, _external=True) }}
8 |
9 | Sincerely,
10 |
11 | The {{ config.APP_NAME }} Team
12 |
13 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/requirements/common.txt:
--------------------------------------------------------------------------------
1 | Flask==0.10.1
2 | Flask-Assets==0.10
3 | Flask-Compress==1.2.1
4 | Flask-Login==0.2.11
5 | Flask-Mail==0.9.1
6 | Flask-Migrate==1.4.0
7 | Flask-SQLAlchemy==2.0
8 | Flask-SSLify==0.1.5
9 | Flask-Script==2.0.5
10 | Flask-WTF==0.11
11 | geopy==1.11.0
12 | gunicorn==19.3.0
13 | Jinja2==2.7.3
14 | jsmin==2.1.6
15 | Mako==1.0.1
16 | MarkupSafe==0.23
17 | psycopg2==2.6.1
18 | SQLAlchemy==1.0.6
19 | WTForms==2.0.2
20 | Werkzeug==0.10.4
21 | alembic==0.7.6
22 | blinker==1.3
23 | itsdangerous==0.24
24 | webassets==0.10.1
25 | wsgiref==0.1.2
26 |
--------------------------------------------------------------------------------
/app/utils.py:
--------------------------------------------------------------------------------
1 | from flask import url_for
2 |
3 |
4 | def register_template_utils(app):
5 | """Register Jinja 2 helpers (called from __init__.py)."""
6 |
7 | @app.template_test()
8 | def equalto(value, other):
9 | return value == other
10 |
11 | @app.template_global()
12 | def is_hidden_field(field):
13 | from wtforms.fields import HiddenField
14 | return isinstance(field, HiddenField)
15 |
16 | app.add_template_global(index_for_role)
17 |
18 |
19 | def index_for_role(role):
20 | return url_for(role.index)
21 |
--------------------------------------------------------------------------------
/app/assets.py:
--------------------------------------------------------------------------------
1 | from flask.ext.assets import Bundle
2 |
3 | app_css = Bundle(
4 | 'app.scss',
5 | filters='scss',
6 | output='styles/app.css'
7 | )
8 |
9 | app_js = Bundle(
10 | 'app.js',
11 | filters='jsmin',
12 | output='scripts/app.js'
13 | )
14 |
15 | vendor_css = Bundle(
16 | 'vendor/semantic.min.css',
17 | output='styles/vendor.css'
18 | )
19 |
20 | vendor_js = Bundle(
21 | 'vendor/jquery.min.js',
22 | 'vendor/semantic.min.js',
23 | 'vendor/tablesort.min.js',
24 | filters='jsmin',
25 | output='scripts/vendor.js'
26 | )
27 |
--------------------------------------------------------------------------------
/app/templates/account/email/confirm.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | Welcome to {{ config.APP_NAME }} !
4 |
5 | To confirm your account, please click here .
6 |
7 | Alternatively, you can paste the following link in your browser's address bar:
8 |
9 | {{ url_for('account.confirm', token=token, _external=True) }}
10 |
11 | Sincerely,
12 |
13 | The {{ config.APP_NAME }} Team
14 |
15 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/templates/account/email/reset_password.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | To reset your password, click here .
4 |
5 | Alternatively, you can paste the following link in your browser's address bar:
6 |
7 | {{ url_for('account.reset_password', token=token, _external=True) }}
8 |
9 | If you have not requested a password reset, simply ignore this message.
10 |
11 | Sincerely,
12 |
13 | The {{ config.APP_NAME }} Team
14 |
15 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/templates/partials/_flashes.html:
--------------------------------------------------------------------------------
1 | {% macro render_flashes(class) %}
2 | {% with msgs = get_flashed_messages(category_filter=[class]) %}
3 | {% for msg in msgs %}
4 |
5 |
6 | {{ msg }}
7 |
8 | {% endfor %}
9 | {% endwith %}
10 | {% endmacro %}
11 |
12 |
13 |
14 | {{ render_flashes('error') }}
15 | {{ render_flashes('warning') }}
16 | {{ render_flashes('info') }}
17 | {{ render_flashes('success') }}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/decorators.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 |
3 | from flask import abort
4 | from flask.ext.login import current_user
5 | from .models import Permission
6 |
7 |
8 | def permission_required(permission):
9 | """Restrict a view to users with the given permission."""
10 | def decorator(f):
11 | @wraps(f)
12 | def decorated_function(*args, **kwargs):
13 | if not current_user.can(permission):
14 | abort(403)
15 | return f(*args, **kwargs)
16 | return decorated_function
17 | return decorator
18 |
19 |
20 | def admin_required(f):
21 | return permission_required(Permission.ADMINISTER)(f)
22 |
--------------------------------------------------------------------------------
/tests/test_basics.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask import current_app
3 | from app import create_app, db
4 |
5 |
6 | class BasicsTestCase(unittest.TestCase):
7 | def setUp(self):
8 | self.app = create_app('testing')
9 | self.app_context = self.app.app_context()
10 | self.app_context.push()
11 | db.create_all()
12 |
13 | def tearDown(self):
14 | db.session.remove()
15 | db.drop_all()
16 | self.app_context.pop()
17 |
18 | def test_app_exists(self):
19 | self.assertFalse(current_app is None)
20 |
21 | def test_app_is_testing(self):
22 | self.assertTrue(current_app.config['TESTING'])
23 |
--------------------------------------------------------------------------------
/app/templates/partials/_head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block page_title %}{{ config.APP_NAME }}{% endblock %}
5 |
6 | {% assets 'vendor_css' %} {% endassets %}
7 | {% assets 'app_css' %} {% endassets %}
8 |
9 | {% assets 'vendor_js' %}{% endassets %}
10 | {% assets 'app_js' %}{% endassets %}
11 |
--------------------------------------------------------------------------------
/app/templates/layouts/base.html:
--------------------------------------------------------------------------------
1 | {% import 'macros/nav_macros.html' as nav %}
2 |
3 |
4 |
5 | {% include 'partials/_head.html' %}
6 |
7 |
8 | {% block nav %}
9 | {{ nav.render_nav(current_user) }}
10 | {% endblock %}
11 |
12 | {% include 'partials/_flashes.html' %}
13 |
14 | {% block content %}
15 | {% endblock %}
16 |
17 | {# Implement CSRF protection for site #}
18 | {% if csrf_token() %}
19 |
20 |
21 |
22 | {% endif %}
23 |
24 |
--------------------------------------------------------------------------------
/app/templates/account/email/invite.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.full_name() }},
2 |
3 | You are invited to join {{ config.APP_NAME }} !
4 |
5 | To set your password, please click here .
6 |
7 | Alternatively, you can paste the following link in your browser's address bar:
8 |
9 | {{ url_for('account.join_from_invite', user_id=user_id, token=token, _external=True) }}
10 |
11 | Once completed, please log in as {{ user.email }} with the password you set.
12 |
13 | Sincerely,
14 |
15 | The {{ config.APP_NAME }} Team
16 |
17 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/email.py:
--------------------------------------------------------------------------------
1 | from threading import Thread
2 |
3 | from flask import current_app, render_template
4 | from flask.ext.mail import Message
5 | from . import mail
6 |
7 |
8 | def send_async_email(app, msg):
9 | with app.app_context():
10 | mail.send(msg)
11 |
12 |
13 | def send_email(to, subject, template, **kwargs):
14 | app = current_app._get_current_object()
15 | msg = Message(app.config['EMAIL_SUBJECT_PREFIX'] + ' ' + subject,
16 | sender=app.config['EMAIL_SENDER'], recipients=[to])
17 | msg.body = render_template(template + '.txt', **kwargs)
18 | msg.html = render_template(template + '.html', **kwargs)
19 | thr = Thread(target=send_async_email, args=[app, msg])
20 | thr.start()
21 | return thr
22 |
--------------------------------------------------------------------------------
/app/templates/account/unconfirmed.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
You need to confirm your account before continuing.
9 |
10 |
When you created an account, you should have received an email from us at {{ current_user.email }}
11 | with a link to confirm your email address.
12 |
13 |
14 |
15 |
I didn't receive an email!
16 |
17 |
No problem! Just click the button below and we'll send you another one.
18 |
Resend confirmation email
19 |
20 |
21 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/account/donate.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
9 |
10 |
11 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/app/templates/main/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
Hello, world
6 |
7 |
This is Hack4Impact 's web application template. We'll use this as a
8 | foundation for each of our Flask projects. The goal is to reduce the work
9 | necessary to get a new project off the ground, by providing boilerplate code (such as account management
10 | facilities), as well as uncontroversial and extensible defaults. We have integrated an
11 | SCSS -based asset pipeline based around a (fantastic!) framework
12 | called Semantic UI . Much of this code is appropriated from the examples
13 | in Miguel Grinberg's book, Flask Web Development .
14 |
15 |
{{ lipsum(6) }}
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/app/assets/styles/app.scss:
--------------------------------------------------------------------------------
1 | // Semantic UI breakpoints
2 | $mobile-breakpoint: 768px;
3 | $tablet-breakpoint: 992px;
4 | $small-monitor-breakpoint: 1200px;
5 |
6 | // General nav
7 | nav {
8 | margin-bottom: 40px !important;
9 | }
10 |
11 | // Desktop nav
12 | .ui.fixed.main.menu .right.menu > .item:last-of-type {
13 | border-right: 1px solid rgba(34, 36, 38, 0.0980392);
14 | }
15 |
16 | // Mobile nav
17 | nav .mobile.only.row {
18 | .ui.vertical.menu {
19 | margin-top: 40px;
20 | display: none;
21 | }
22 | }
23 |
24 | // Flashes
25 | .flashes > .ui.message:last-of-type {
26 | margin-bottom: 2rem;
27 | }
28 |
29 | // Tables
30 | table.selectable tr:hover {
31 | cursor: pointer !important;
32 | }
33 |
34 | // main/map.html
35 | .map-grid {
36 | #map {
37 | display: block;
38 | width: 100%;
39 | height: 500px;
40 | -moz-box-shadow: 0px 5px 20px #ccc;
41 | -webkit-box-shadow: 0px 5px 20px #ccc;
42 | box-shadow: 0px 5px 20px #ccc;
43 | }
44 | .ui.button {
45 | text-align: center;
46 | margin: 0 auto;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Hack4Impact
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/main/views.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 | from flask import Response
3 | from . import main
4 | import json
5 | from ..models import User
6 | from flask.ext.login import login_required
7 |
8 |
9 | @main.route('/')
10 | def index():
11 | return render_template('main/index.html')
12 |
13 |
14 | @main.route('/users')
15 | @login_required
16 | def user_map():
17 | return render_template('main/map.html', users=User.query.all())
18 |
19 |
20 | @main.route('/search/')
21 | @login_required
22 | def search_query(query):
23 | looking_for = '%'+query+'%'
24 | users = User.query.filter((User.first_name.ilike(looking_for)) |
25 | User.last_name.ilike(looking_for))\
26 | .order_by(User.first_name).all()
27 | data = dict()
28 | data['results'] = [{'title': u.full_name(),
29 | 'url': '/account/profile/' + str(u.id)} for u in users]
30 | json_data = json.dumps(data)
31 | return Response(response=json_data, status=200,
32 | mimetype='application/json')
33 |
34 |
35 | @main.route('/help')
36 | def help():
37 | return render_template('main/help.html')
38 |
--------------------------------------------------------------------------------
/app/static/styles/app.css:
--------------------------------------------------------------------------------
1 | @media -sass-debug-info{filename{}line{font-family:\000037}}
2 | nav {
3 | margin-bottom: 40px !important;
4 | }
5 |
6 | @media -sass-debug-info{filename{}line{font-family:\0000312}}
7 | .ui.fixed.main.menu .right.menu > .item:last-of-type {
8 | border-right: 1px solid rgba(34, 36, 38, 0.09804);
9 | }
10 |
11 | @media -sass-debug-info{filename{}line{font-family:\0000318}}
12 | nav .mobile.only.row .ui.vertical.menu {
13 | margin-top: 40px;
14 | display: none;
15 | }
16 |
17 | @media -sass-debug-info{filename{}line{font-family:\0000325}}
18 | .flashes > .ui.message:last-of-type {
19 | margin-bottom: 2rem;
20 | }
21 |
22 | @media -sass-debug-info{filename{}line{font-family:\0000330}}
23 | table.selectable tr:hover {
24 | cursor: pointer !important;
25 | }
26 |
27 | @media -sass-debug-info{filename{}line{font-family:\0000336}}
28 | .map-grid #map {
29 | display: block;
30 | width: 100%;
31 | height: 500px;
32 | -moz-box-shadow: 0px 5px 20px #ccc;
33 | -webkit-box-shadow: 0px 5px 20px #ccc;
34 | box-shadow: 0px 5px 20px #ccc;
35 | }
36 | @media -sass-debug-info{filename{}line{font-family:\0000344}}
37 | .map-grid .ui.button {
38 | text-align: center;
39 | margin: 0 auto;
40 | }
41 |
--------------------------------------------------------------------------------
/app/templates/main/map.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
12 |
13 |
14 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/app/templates/resources/create_review.html:
--------------------------------------------------------------------------------
1 | {% extends 'resources/read_resource.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block form %}
5 |
6 |
7 |
8 | {% set flashes = {
9 | 'error': get_flashed_messages(category_filter=['form-error']),
10 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
11 | 'info': get_flashed_messages(category_filter=['form-info']),
12 | 'success': get_flashed_messages(category_filter=['form-success'])
13 | } %}
14 |
15 | {{ f.begin_form(form, flashes) }}
16 |
17 | {{ f.render_form_field(form.rating) }}
18 | {{ f.render_form_field(form.content) }}
19 | {{ f.render_form_field(form.submit) }}
20 |
21 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
22 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
23 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
24 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
25 |
26 | {{ f.end_form(form) }}
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/app/assets/scripts/app.js:
--------------------------------------------------------------------------------
1 | // Semantic UI breakpoints
2 | var mobileBreakpoint = '768px';
3 | var tabletBreakpoint = '992px';
4 | var smallMonitorBreakpoint = '1200px';
5 |
6 | $(document).ready(function () {
7 |
8 | // Enable dismissable flash messages
9 | $('.message .close').on('click', function () {
10 | $(this).closest('.message').transition('fade');
11 | });
12 |
13 | // Enable mobile navigation
14 | $('#open-nav').on('click', function () {
15 | $('.mobile.only .vertical.menu').transition('slide down');
16 | });
17 |
18 | // Enable sortable tables
19 | $('table.ui.sortable').tablesort();
20 |
21 | // Enable dropdowns
22 | $('.dropdown').dropdown();
23 | $('select').dropdown();
24 | });
25 |
26 |
27 | // Add a case-insensitive version of jQuery :contains pseduo
28 | // Used in table filtering
29 | (function ($) {
30 | function icontains(elem, text) {
31 | return (elem.textContent || elem.innerText || $(elem).text() || "")
32 | .toLowerCase().indexOf((text || "").toLowerCase()) > -1;
33 | }
34 |
35 | $.expr[':'].icontains = $.expr.createPseudo ?
36 | $.expr.createPseudo(function (text) {
37 | return function (elem) {
38 | return icontains(elem, text);
39 | };
40 | }) :
41 | function (elem, i, match) {
42 | return icontains(elem, match[3]);
43 | };
44 | })(jQuery);
45 |
46 |
--------------------------------------------------------------------------------
/app/templates/main/help.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
Contact Women Veterans Rock!
6 |
7 |
8 |
9 |
Philadelphia Headquarters
10 |
The Healthy Caregiver Community Foundation
11 | c/o “Women Veterans ROCK!”
12 | 7782 Crittenden Street – P.O. Box 27774
13 | Philadelphia, PA 19118-0774
14 |
15 |
Telephone (215) 836-4262
16 |
Email – Info@WomenVetsRock.org
17 |
18 |
19 |
20 |
21 |
Washington D.C. Satellite Team
22 |
Washington, D.C. Satellite Team
23 | Douglas Memorial United Methodist Church
24 | 800 11th Street, NE
25 | Washington, DC 20002
26 |
27 |
28 |
29 |
30 |
Frequently Asked Questions
31 |
34 |
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/app/templates/admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% macro dashboard_option(title, endpoint, description=None, icon=None) %}
4 |
5 |
6 |
19 |
20 |
21 | {% endmacro %}
22 |
23 | {% block content %}
24 |
25 |
26 |
29 |
30 | {{ dashboard_option('Registered Users', 'admin.registered_users',
31 | description='View and manage user accounts', icon='users icon') }}
32 | {{ dashboard_option('Add New User', 'admin.new_user',
33 | description='Create a new user account', icon='add user icon') }}
34 | {{ dashboard_option('Invite New User', 'admin.invite_user',
35 | description='Invites a new user to create their own account', icon='add user icon') }}
36 |
37 |
38 |
39 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | {% set flashes = {
10 | 'error': get_flashed_messages(category_filter=['form-error']),
11 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
12 | 'info': get_flashed_messages(category_filter=['form-info']),
13 | 'success': get_flashed_messages(category_filter=['form-success'])
14 | } %}
15 |
16 | {{ f.begin_form(form, flashes) }}
17 | {{ f.render_form_field(form.email) }}
18 | {{ f.render_form_field(form.password) }}
19 |
20 | {{ f.render_form_field(form.remember_me, extra_classes='column') }}
21 |
24 |
25 |
26 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
27 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
28 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
29 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
30 |
31 | {{ f.render_form_field(form.submit) }}
32 | {{ f.end_form(form) }}
33 |
34 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/app/resources/forms.py:
--------------------------------------------------------------------------------
1 | from flask.ext.wtf import Form
2 | from wtforms.fields import (
3 | StringField,
4 | IntegerField,
5 | SubmitField
6 | )
7 | from wtforms.validators import InputRequired, Length, URL, Optional,\
8 | NumberRange
9 |
10 |
11 | class ResourceForm(Form):
12 | address_autocomplete = StringField('Enter the address')
13 | name = StringField('Name', validators=[
14 | InputRequired(),
15 | Length(1, 64)
16 | ])
17 | description = StringField('Description', validators=[
18 | InputRequired(),
19 | ])
20 | website = StringField('Website', validators=[
21 | Optional(),
22 | URL()
23 | ])
24 |
25 | street_number = IntegerField('Street Number', validators=[
26 | InputRequired()
27 | ])
28 | # Google Place Autocomplete example divs named for Google address schema.
29 | route = StringField('Street Address', validators=[
30 | InputRequired()
31 | ])
32 | locality = StringField('City', validators=[
33 | InputRequired()
34 | ])
35 | administrative_area_level_1 = StringField('State', validators=[
36 | InputRequired(),
37 | Length(2, 2)
38 | ])
39 | postal_code = StringField('ZIP Code', validators=[
40 | InputRequired(),
41 | Length(5, 5)
42 | ])
43 | submit = SubmitField('Add Resource')
44 |
45 |
46 | class ReviewForm(Form):
47 | rating = IntegerField('Rating (1-5)', validators=[
48 | InputRequired(),
49 | NumberRange(1, 5)
50 | ])
51 | content = StringField('Content', validators=[
52 | InputRequired()
53 | ])
54 | submit = SubmitField('Finish Your Review')
55 |
56 |
57 | class ClosedResourceDetailForm(Form):
58 | explanation = StringField('Your Explanation')
59 | connection = StringField('Your Connection')
60 | submit = SubmitField('Submit')
61 |
--------------------------------------------------------------------------------
/app/templates/account/register.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | {% set flashes = {
10 | 'error': get_flashed_messages(category_filter=['form-error']),
11 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
12 | 'info': get_flashed_messages(category_filter=['form-info']),
13 | 'success': get_flashed_messages(category_filter=['form-success'])
14 | } %}
15 |
16 | {{ f.begin_form(form, flashes) }}
17 |
18 |
19 | {{ f.render_form_field(form.first_name) }}
20 | {{ f.render_form_field(form.last_name) }}
21 |
22 |
23 | {{ f.render_form_field(form.email) }}
24 |
25 |
26 | {{ f.render_form_field(form.password) }}
27 | {{ f.render_form_field(form.password2) }}
28 |
29 |
30 | {{ f.render_form_field(form.zip_code) }}
31 |
32 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
33 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
34 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
35 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
36 |
37 | {% for field in form | selectattr('type', 'equalto', 'SubmitField') %}
38 | {{ f.render_form_field(field) }}
39 | {% endfor %}
40 |
41 | {{ f.end_form(form) }}
42 |
43 |
44 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/account/manage.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% set endpoints = [
5 | ('account.manage', 'Account information'),
6 | ('account.change_email_request', 'Change email address'),
7 | ('account.change_password', 'Change password'),
8 | ('account.logout', 'Log out')
9 | ] %}
10 |
11 | {% macro navigation(items) %}
12 |
20 | {% endmacro %}
21 |
22 | {% macro user_info(user) %}
23 |
24 | Full name {{ '%s %s' % (user.first_name, user.last_name) }}
25 | Email address {{ user.email }}
26 | Account type {{ user.role.name }}
27 |
28 | {% endmacro %}
29 |
30 | {% block content %}
31 |
32 |
33 |
37 |
38 |
39 |
40 | {{ navigation(endpoints) }}
41 |
42 |
43 | {% if form %}
44 | {{ f.render_form(form, extra_classes='fluid') }}
45 | {% else %}
46 | {{ user_info(user) }}
47 | {% endif %}
48 |
49 |
50 |
51 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/account/edit_profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 | {% set flashes = {
6 | 'error': get_flashed_messages(category_filter=['form-error']),
7 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
8 | 'info': get_flashed_messages(category_filter=['form-info']),
9 | 'success': get_flashed_messages(category_filter=['form-success'])
10 | } %}
11 |
12 |
13 |
14 |
17 | {{ f.begin_form(form, flashes) }}
18 |
19 |
{{ f.render_form_field(form.first_name) }}
20 |
{{ f.render_form_field(form.last_name) }}
21 |
22 |
23 |
{{ f.render_form_field(form.bio) }}
24 |
{{ f.render_form_field(form.birthday) }}
25 |
{{ f.render_form_field(form.facebook_link) }}
26 |
{{ f.render_form_field(form.linkedin_link) }}
27 |
{{ f.render_form_field(form.affiliations) }}
28 |
29 |
30 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
31 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
32 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
33 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
34 |
35 | {{ f.render_form_field(form.submit) }}
36 | {{ f.end_form(form) }}
37 |
38 |
39 |
40 | {% endblock %}
41 |
--------------------------------------------------------------------------------
/app/templates/account/profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 | {% if is_current %}
11 |
16 | {% endif %}
17 |
18 |
19 |
Email
20 | {{ user.email }}
21 |
22 |
23 |
Role
24 | {{ user.role.name }}
25 |
26 |
27 |
About Me
28 | {{ user.bio }}
29 |
30 |
31 |
Birthday
32 | {{ user.birthday }}
33 |
34 |
35 |
Affiliations
36 | {% if user.tags %}
37 |
38 | {% for affiliation in user.tags %}
39 | {% if affiliation.type == "AffiliationTag" %}
40 |
41 | {{ affiliation.name }}
42 |
43 | {% endif %}
44 | {% endfor %}
45 |
46 | {% else %}
47 | No affiliations.
48 | {% endif %}
49 |
50 |
51 |
52 | {% endblock %}
53 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | basedir = os.path.abspath(os.path.dirname(__file__))
3 |
4 | from flask import Flask
5 | from flask.ext.mail import Mail
6 | from flask.ext.sqlalchemy import SQLAlchemy
7 | from flask.ext.login import LoginManager
8 | from flask.ext.assets import Environment
9 | from flask.ext.wtf import CsrfProtect
10 | from flask.ext.compress import Compress
11 | from config import config
12 | from assets import app_css, app_js, vendor_css, vendor_js
13 |
14 | mail = Mail()
15 | db = SQLAlchemy()
16 | csrf = CsrfProtect()
17 | compress = Compress()
18 |
19 | # Set up Flask-Login
20 | login_manager = LoginManager()
21 | login_manager.session_protection = 'strong'
22 | login_manager.login_view = 'account.login'
23 |
24 |
25 | def create_app(config_name):
26 | app = Flask(__name__)
27 | app.config.from_object(config[config_name])
28 | config[config_name].init_app(app)
29 |
30 | # Set up extensions
31 | mail.init_app(app)
32 | db.init_app(app)
33 | login_manager.init_app(app)
34 | csrf.init_app(app)
35 | compress.init_app(app)
36 |
37 | # Register Jinja template functions
38 | from utils import register_template_utils
39 | register_template_utils(app)
40 |
41 | # Set up asset pipeline
42 | assets_env = Environment(app)
43 | dirs = ['assets/styles', 'assets/scripts']
44 | for path in dirs:
45 | assets_env.append_path(os.path.join(basedir, path))
46 | assets_env.url_expire = True
47 |
48 | assets_env.register('app_css', app_css)
49 | assets_env.register('app_js', app_js)
50 | assets_env.register('vendor_css', vendor_css)
51 | assets_env.register('vendor_js', vendor_js)
52 |
53 | # Configure SSL if platform supports it
54 | if not app.debug and not app.testing and not app.config['SSL_DISABLE']:
55 | from flask.ext.sslify import SSLify
56 | SSLify(app)
57 |
58 | # Create app blueprints
59 | from main import main as main_blueprint
60 | app.register_blueprint(main_blueprint)
61 |
62 | from account import account as account_blueprint
63 | app.register_blueprint(account_blueprint, url_prefix='/account')
64 |
65 | from admin import admin as admin_blueprint
66 | app.register_blueprint(admin_blueprint, url_prefix='/admin')
67 |
68 | from resources import resources as resources_blueprint
69 | app.register_blueprint(resources_blueprint, url_prefix='/resources')
70 |
71 | return app
72 |
--------------------------------------------------------------------------------
/app/templates/admin/new_user.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block scripts %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
11 |
12 | Back to dashboard
13 |
14 |
18 |
19 | {% set flashes = {
20 | 'error': get_flashed_messages(category_filter=['form-error']),
21 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
22 | 'info': get_flashed_messages(category_filter=['form-info']),
23 | 'success': get_flashed_messages(category_filter=['form-success'])
24 | } %}
25 |
26 | {{ f.begin_form(form, flashes) }}
27 |
28 | {{ f.render_form_field(form.role) }}
29 |
30 |
31 | {{ f.render_form_field(form.first_name) }}
32 | {{ f.render_form_field(form.last_name) }}
33 |
34 |
35 | {{ f.render_form_field(form.email) }}
36 |
37 | {% if form.password %}
38 |
39 |
40 | {{ f.render_form_field(form.password) }}
41 | {{ f.render_form_field(form.password2) }}
42 |
43 |
44 | {% endif %}
45 |
46 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
47 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
48 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
49 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
50 |
51 | {% for field in form | selectattr('type', 'equalto', 'SubmitField') %}
52 | {{ f.render_form_field(field) }}
53 | {% endfor %}
54 |
55 | {{ f.end_form() }}
56 |
57 |
58 | {% endblock %}
--------------------------------------------------------------------------------
/app/admin/forms.py:
--------------------------------------------------------------------------------
1 | from flask.ext.wtf import Form
2 | from wtforms.fields import StringField, PasswordField, SubmitField
3 | from wtforms.fields.html5 import EmailField
4 | from wtforms.ext.sqlalchemy.fields import QuerySelectField
5 | from wtforms.validators import InputRequired, Length, Email, EqualTo
6 | from wtforms import ValidationError
7 | from ..models import User, Role
8 | from .. import db
9 |
10 |
11 | class ChangeUserEmailForm(Form):
12 | email = EmailField('New email', validators=[
13 | InputRequired(),
14 | Length(1, 64),
15 | Email()
16 | ])
17 | submit = SubmitField('Update email')
18 |
19 | def validate_email(self, field):
20 | if User.query.filter_by(email=field.data).first():
21 | raise ValidationError('Email already registered.')
22 |
23 |
24 | class ChangeAccountTypeForm(Form):
25 | role = QuerySelectField('New account type',
26 | validators=[InputRequired()],
27 | get_label='name',
28 | query_factory=lambda: db.session.query(Role).
29 | order_by('permissions'))
30 | submit = SubmitField('Update role')
31 |
32 |
33 | class InviteUserForm(Form):
34 | role = QuerySelectField('Account type',
35 | validators=[InputRequired()],
36 | get_label='name',
37 | query_factory=lambda: db.session.query(Role).
38 | order_by('permissions'))
39 | first_name = StringField('First name', validators=[InputRequired(),
40 | Length(1, 64)])
41 | last_name = StringField('Last name', validators=[InputRequired(),
42 | Length(1, 64)])
43 | email = EmailField('Email', validators=[InputRequired(), Length(1, 64),
44 | Email()])
45 | submit = SubmitField('Invite')
46 |
47 | def validate_email(self, field):
48 | if User.query.filter_by(email=field.data).first():
49 | raise ValidationError('Email already registered.')
50 |
51 |
52 | class NewUserForm(InviteUserForm):
53 | password = PasswordField('Password', validators=[
54 | InputRequired(), EqualTo('password2',
55 | 'Passwords must match.')
56 | ])
57 | password2 = PasswordField('Confirm password', validators=[InputRequired()])
58 |
59 | submit = SubmitField('Create')
60 |
--------------------------------------------------------------------------------
/app/templates/resources/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
35 |
36 |
37 |
56 | {% endblock %}
57 |
--------------------------------------------------------------------------------
/app/assets/scripts/vendor/tablesort.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | A simple, lightweight jQuery plugin for creating sortable tables.
3 | https://github.com/kylefox/jquery-tablesort
4 | Version 0.0.2
5 | */
6 | !function(a) { a.tablesort = function(b, c) { var d = this; this.$table = b, this.settings = a.extend({}, a.tablesort.defaults, c), this.$table.find("thead th").bind("click.tablesort", function() { a(this).hasClass("disabled") || d.sort(a(this)) }), this.index = null, this.$th = null, this.direction = [] }, a.tablesort.prototype = { sort: function(b, c) { var d = new Date, e = this, f = this.$table, g = f.find("tbody tr"), h = b.index(), i = [], j = a("
"), k = function(a, b, c) { var d; return a.data().sortBy ? (d = a.data().sortBy, "function" == typeof d ? d(a, b, c) : d) : b.data("sort") ? b.data("sort") : b.text() }, l = function(a, b) { var q, r, s, c = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, d = /(^[ ]*|[ ]*$)/g, e = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, f = /^0x[0-9a-f]+$/i, g = /^0/, i = function(a) { return ("" + a).toLowerCase().replace(",", "") }, j = i(a).replace(d, "") || "", k = i(b).replace(d, "") || "", l = j.replace(c, "\0$1\0").replace(/\0$/, "").replace(/^\0/, "").split("\0"), m = k.replace(c, "\0$1\0").replace(/\0$/, "").replace(/^\0/, "").split("\0"), n = Math.max(l.length, m.length), o = parseInt(j.match(f), 10) || 1 != l.length && j.match(e) && Date.parse(j), p = parseInt(k.match(f), 10) || o && k.match(e) && Date.parse(k) || null; if (p) { if (p > o) return - 1; if (o > p) return 1 } for (s = 0; n > s; s++) { if (q=!(l[s] || "").match(g) && parseFloat(l[s]) || l[s] || 0, r=!(m[s] || "").match(g) && parseFloat(m[s]) || m[s] || 0, isNaN(q) !== isNaN(r)) return isNaN(q) ? 1 : - 1; if (typeof q != typeof r && (q += "", r += ""), r > q) return - 1; if (q > r) return 1 } return 0 }; 0 !== g.length && (e.$table.find("thead th").removeClass(e.settings.asc + " " + e.settings.desc), this.$th = b, this.direction[h] = this.index != h ? "desc" : "asc" !== c && "desc" !== c ? "desc" === this.direction[h] ? "asc" : "desc" : c, this.index = h, c = "asc" == this.direction[h] ? 1 : - 1, e.$table.trigger("tablesort:start", [e]), e.log("Sorting by " + this.index + " " + this.direction[h]), g.sort(function(d, f) { var g = a(d), h = a(f), j = g.index(), m = h.index(); return i[j] ? d = i[j] : (d = k(b, e.cellToSort(d), e), i[j] = d), i[m] ? f = i[m] : (f = k(b, e.cellToSort(f), e), i[m] = f), l(d, f) * c }), g.each(function(a, b) { j.append(b) }), f.append(j.html()), b.addClass(e.settings[e.direction[h]]), e.log("Sort finished in " + ((new Date).getTime() - d.getTime()) + "ms"), e.$table.trigger("tablesort:complete", [e])) }, cellToSort: function(b) { return a(a(b).find("td").get(this.index)) }, log: function(b) { (a.tablesort.DEBUG || this.settings.debug) && console && console.log && console.log("[tablesort] " + b) }, destroy: function() { return this.$table.find("thead th").unbind("click.tablesort"), this.$table.data("tablesort", null), null } }, a.tablesort.DEBUG=!1, a.tablesort.defaults = { debug: a.tablesort.DEBUG, asc: "sorted ascending", desc: "sorted descending" }, a.fn.tablesort = function(b) { var c, e; return this.each(function() { c = a(this), e = c.data("tablesort"), e && e.destroy(), c.data("tablesort", new a.tablesort(c, b)) }) } }(jQuery);
7 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | basedir = os.path.abspath(os.path.dirname(__file__))
4 |
5 |
6 | class Config:
7 | APP_NAME = 'Women Veterans ROCK'
8 | SECRET_KEY = os.environ.get('SECRET_KEY') or \
9 | 'SjefBOa$1FgGco0SkfPO392qqH9%a492'
10 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
11 | SSL_DISABLE = True
12 |
13 | MAIL_SERVER = 'smtp.googlemail.com'
14 | MAIL_PORT = 587
15 | MAIL_USE_TLS = True
16 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
17 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
18 |
19 | ADMIN_EMAIL = 'flask-base-admin@example.com'
20 | EMAIL_SUBJECT_PREFIX = '[{}]'.format(APP_NAME)
21 | EMAIL_SENDER = '{app_name} Admin <{email}>'.format(app_name=APP_NAME,
22 | email=MAIL_USERNAME)
23 |
24 | @staticmethod
25 | def init_app(app):
26 | pass
27 |
28 |
29 | class DevelopmentConfig(Config):
30 | DEBUG = True
31 | ASSETS_DEBUG = True
32 | SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
33 | 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
34 |
35 |
36 | class TestingConfig(Config):
37 | TESTING = True
38 | SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
39 | 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
40 | WTF_CSRF_ENABLED = False
41 |
42 |
43 | class ProductionConfig(Config):
44 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
45 | 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
46 |
47 | @classmethod
48 | def init_app(cls, app):
49 | Config.init_app(app)
50 |
51 | # Email errors to administators
52 | import logging
53 | from logging.handlers import SMTPHandler
54 | credentials = None
55 | secure = None
56 | if getattr(cls, 'MAIL_USERNAME', None) is not None:
57 | credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
58 | if getattr(cls, 'MAIL_USE_TLS', None):
59 | secure = ()
60 | mail_handler = SMTPHandler(
61 | mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
62 | fromaddr=cls.EMAIL_SENDER,
63 | toaddrs=[cls.ADMIN_EMAIL],
64 | subject=cls.EMAIL_SUBJECT_PREFIX + ' Application Error',
65 | credentials=credentials,
66 | secure=secure
67 | )
68 | mail_handler.setLevel(logging.ERROR)
69 | app.logger.addHandler(mail_handler)
70 |
71 |
72 | class HerokuConfig(ProductionConfig):
73 | SSL_DISABLE = bool(os.environ.get('SSL_DISABLE'))
74 |
75 | @classmethod
76 | def init_app(cls, app):
77 | ProductionConfig.init_app(app)
78 |
79 | # Handle proxy server headers
80 | from werkzeug.contrib.fixers import ProxyFix
81 | app.wsgi_app = ProxyFix(app.wsgi_app)
82 |
83 |
84 | class UnixConfig(ProductionConfig):
85 | @classmethod
86 | def init_app(cls, app):
87 | ProductionConfig.init_app(app)
88 |
89 | # Log to syslog
90 | import logging
91 | from logging.handlers import SysLogHandler
92 | syslog_handler = SysLogHandler()
93 | syslog_handler.setLevel(logging.WARNING)
94 | app.logger.addHandler(syslog_handler)
95 |
96 |
97 | config = {
98 | 'development': DevelopmentConfig,
99 | 'testing': TestingConfig,
100 | 'production': ProductionConfig,
101 | 'default': DevelopmentConfig
102 | }
103 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | from app import create_app, db
4 | from app.models import User, Role, ZIPCode, Address, DonorLevel, Resource, \
5 | ResourceReview, Tag, AffiliationTag, ResourceCategoryTag
6 | from flask.ext.script import Manager, Shell
7 | from flask.ext.migrate import Migrate, MigrateCommand
8 |
9 | # Import settings from .env file. Must define FLASK_CONFIG
10 | if os.path.exists('.env'):
11 | print('Importing environment from .env file')
12 | for line in open('.env'):
13 | var = line.strip().split('=')
14 | if len(var) == 2:
15 | os.environ[var[0]] = var[1]
16 |
17 | app = create_app(os.getenv('FLASK_CONFIG') or 'default')
18 | manager = Manager(app)
19 | migrate = Migrate(app, db)
20 |
21 |
22 | def make_shell_context():
23 | return dict(app=app, db=db, User=User, Role=Role, ZIPCode=ZIPCode,
24 | Address=Address, DonorLevel=DonorLevel, Tag=Tag,
25 | ResourceCategoryTag=ResourceCategoryTag,
26 | AffiliationTag=AffiliationTag)
27 |
28 |
29 | manager.add_command('shell', Shell(make_context=make_shell_context))
30 | manager.add_command('db', MigrateCommand)
31 |
32 |
33 | @manager.command
34 | def test():
35 | """Run the unit tests."""
36 | import unittest
37 |
38 | tests = unittest.TestLoader().discover('tests')
39 | unittest.TextTestRunner(verbosity=2).run(tests)
40 |
41 |
42 | @manager.command
43 | def recreate_db():
44 | """
45 | Recreates a local database. You probably should not use this on
46 | production.
47 | """
48 | db.drop_all()
49 | db.create_all()
50 | db.session.commit()
51 |
52 |
53 | @manager.option('-n',
54 | '--fake-count',
55 | default=10,
56 | type=int,
57 | help='Number of each model type to create',
58 | dest='count')
59 | def add_fake_data(count):
60 | """
61 | Adds fake data to the database.
62 | """
63 | User.generate_fake(count=count)
64 | ZIPCode.generate_fake()
65 | Resource.generate_fake()
66 | ResourceReview.generate_fake(count=count)
67 | Address.generate_fake()
68 | AffiliationTag.generate_default()
69 |
70 | # Set a random zip for each user without one.
71 | User.set_random_zip_codes(User.query.filter_by(zip_code=None).all(),
72 | ZIPCode.query.all())
73 | # Set a random affiliation tag for each user.
74 | User.set_random_affiliation_tags(User.query.all(),
75 | AffiliationTag.query.all())
76 |
77 |
78 | @manager.command
79 | def setup_dev():
80 | """Runs the set-up needed for local development."""
81 | setup_general()
82 |
83 | admin_email = 'wvr@gmail.com'
84 | if User.query.filter_by(email=admin_email).first() is None:
85 | User.create_confirmed_admin('Default',
86 | 'Admin',
87 | admin_email,
88 | 'password',
89 | ZIPCode.create_zip_code('19104'))
90 |
91 |
92 | @manager.command
93 | def setup_prod():
94 | """Runs the set-up needed for production."""
95 | setup_general()
96 |
97 |
98 | def setup_general():
99 | """Runs the set-up needed for both local development and production."""
100 | Role.insert_roles()
101 | DonorLevel.insert_donor_levels()
102 |
103 | if __name__ == '__main__':
104 | manager.run()
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flask-base [](https://circleci.com/gh/hack4impact/women-veterans-rock)
2 |
3 | ## Synopsis
4 |
5 | A Flask application template with the boilerplate code already done for you.
6 |
7 | ## What's included?
8 |
9 | * Blueprints
10 | * User and permissions management
11 | * Flask-SQLAlchemy for databases
12 | * Flask-WTF for forms
13 | * Flask-Assets for asset management and SCSS compilation
14 | * Flask-Mail for sending emails
15 | * Automatic SSL + gzip compression
16 |
17 | ## Setting up
18 |
19 | 1. Clone the repo
20 |
21 | ```
22 | $ git clone https://github.com/hack4impact/flask-base.git
23 | $ cd flask-base
24 | ```
25 |
26 | 2. Initialize a virtualenv
27 |
28 | ```
29 | $ pip install virtualenv
30 | $ virtualenv env
31 | $ source env/bin/activate
32 | ```
33 |
34 | 3. Install the dependencies
35 |
36 | ```
37 | $ pip install -r requirements/common.txt
38 | $ pip install -r requirements/dev.txt
39 | ```
40 |
41 | 4. Create the database
42 |
43 | ```
44 | $ python manage.py recreate_db
45 | ```
46 |
47 | 5. Other setup (e.g. creating roles in database)
48 |
49 | ```
50 | $ python manage.py setup_dev
51 | ```
52 |
53 | 6. [Optional] Add fake data to the database
54 |
55 | ```
56 | $ python manage.py add_fake_data
57 | ```
58 |
59 | ## Running the app
60 |
61 | ```
62 | $ source env/bin/activate
63 | $ python manage.py runserver
64 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
65 | * Restarting with stat
66 | ```
67 |
68 | ## Project Structure
69 |
70 |
71 | ```
72 | ├── Procfile
73 | ├── README.md
74 | ├── app
75 | │ ├── __init__.py
76 | │ ├── account
77 | │ │ ├── __init__.py
78 | │ │ ├── forms.py
79 | │ │ └── views.py
80 | │ ├── admin
81 | │ │ ├── __init__.py
82 | │ │ ├── forms.py
83 | │ │ └── views.py
84 | │ ├── assets
85 | │ │ ├── scripts
86 | │ │ │ ├── app.js
87 | │ │ │ └── vendor
88 | │ │ │ ├── jquery.min.js
89 | │ │ │ ├── semantic.min.js
90 | │ │ │ └── tablesort.min.js
91 | │ │ └── styles
92 | │ │ ├── app.scss
93 | │ │ └── vendor
94 | │ │ └── semantic.min.css
95 | │ ├── assets.py
96 | │ ├── decorators.py
97 | │ ├── email.py
98 | │ ├── main
99 | │ │ ├── __init__.py
100 | │ │ ├── errors.py
101 | │ │ ├── forms.py
102 | │ │ └── views.py
103 | │ ├── models.py
104 | │ ├── static
105 | │ │ ├── fonts
106 | │ │ │ └── vendor
107 | │ │ ├── images
108 | │ │ └── styles
109 | │ │ └── app.css
110 | │ ├── templates
111 | │ │ ├── account
112 | │ │ │ ├── email
113 | │ │ │ ├── login.html
114 | │ │ │ ├── manage.html
115 | │ │ │ ├── register.html
116 | │ │ │ ├── reset_password.html
117 | │ │ │ └── unconfirmed.html
118 | │ │ ├── admin
119 | │ │ │ ├── index.html
120 | │ │ │ ├── manage_user.html
121 | │ │ │ ├── new_user.html
122 | │ │ │ └── registered_users.html
123 | │ │ ├── errors
124 | │ │ ├── layouts
125 | │ │ │ └── base.html
126 | │ │ ├── macros
127 | │ │ │ ├── form_macros.html
128 | │ │ │ └── nav_macros.html
129 | │ │ ├── main
130 | │ │ │ └── index.html
131 | │ │ └── partials
132 | │ │ ├── _flashes.html
133 | │ │ └── _head.html
134 | │ └── utils.py
135 | ├── config.py
136 | ├── manage.py
137 | ├── requirements
138 | │ ├── common.txt
139 | │ └── dev.txt
140 | └── tests
141 | ├── test_basics.py
142 | └── test_user_model.py
143 | ```
144 |
145 | ## License
146 | [MIT License](LICENSE.md)
147 |
--------------------------------------------------------------------------------
/app/templates/admin/manage_user.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% set deletion_endpoint = 'admin.delete_user_request' %}
5 |
6 | {% set endpoints = [
7 | ('admin.user_info', 'User information'),
8 | ('admin.change_user_email', 'Change email address'),
9 | ('admin.change_account_type', 'Change account type'),
10 | (deletion_endpoint, 'Delete user')
11 | ] %}
12 |
13 | {% macro navigation(items) %}
14 |
22 | {% endmacro %}
23 |
24 | {% macro user_info(user) %}
25 |
26 | Full name {{ '%s %s' % (user.first_name, user.last_name) }}
27 | Email address {{ user.email }}
28 | Account type {{ user.role.name }}
29 |
30 | {% endmacro %}
31 |
32 | {% block content %}
33 |
34 |
44 |
45 |
46 | {{ navigation(endpoints) }}
47 |
48 |
49 | {% if request.endpoint == deletion_endpoint %}
50 |
60 |
61 |
72 |
73 | {% elif form %}
74 | {{ f.render_form(form) }}
75 | {% else %}
76 | {{ user_info(user) }}
77 | {% endif %}
78 |
79 |
80 |
81 |
82 |
93 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/macros/form_macros.html:
--------------------------------------------------------------------------------
1 | {# WTForms macros heavily inspired by Flask-Bootstrap.
2 | # Consult their docs if you are confused about anything here:
3 | # http://pythonhosted.org/Flask-Bootstrap/macros.html?highlight=quick_form#quick_form #}
4 |
5 | {# Render a flask.ext.wtforms.Form object.
6 | Parameters:
7 | form – The form to output.
8 | method –
57 | {% endmacro %}
58 |
59 | {# Render a message for the form #}
60 | {% macro form_message(messages, header=none, class='') %}
61 | {% if messages %}
62 |
63 | {% if header is not none %}
64 |
65 | {% endif %}
66 | {% if messages %}
67 |
68 | {% for message in messages %}
69 | {{ message | safe }}
70 | {% endfor %}
71 |
72 | {% endif %}
73 |
74 | {% endif %}
75 | {% endmacro %}
76 |
77 | {# Render a field for the form #}
78 | {% macro render_form_field(field, extra_classes='') %}
79 | {% if field.type == 'Radio Field' %}
80 | {% set extra_classes = extra_classes + ' grouped fields' %}
81 | {% endif %}
82 |
90 | {% endmacro %}
91 |
92 | {% macro render_form_input(field) %}
93 | {% if field.widget.input_type == 'checkbox' %}
94 |
95 | {{ field }}
96 | {{ field.label }}
97 |
98 | {% elif field.type == 'RadioField' %}
99 | {% for item in field %}
100 |
101 | {{ field }}
102 | {{ field.label }}
103 |
104 | {% endfor %}
105 | {% elif field.type == 'SubmitField' %}
106 | {{ field(class='ui button') }}
107 | {% elif field.type == 'FormField' %}
108 | {{ render_form(field) }}
109 | {% elif field.type == 'DateField' %}
110 | {{ field.label }}
111 | {{ field(placeholder=field.description) }}
112 | {% else %}
113 | {{ field.label }}
114 | {% if field.description != "" %}
115 | {{ field(placeholder=field.description) }}
116 | {% else %}
117 | {{ field(placeholder=field.label.text) }}
118 | {% endif %}
119 | {% endif %}
120 | {% endmacro %}
121 |
--------------------------------------------------------------------------------
/app/templates/resources/create_resource.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | {% set flashes = {
10 | 'error': get_flashed_messages(category_filter=['form-error']),
11 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
12 | 'info': get_flashed_messages(category_filter=['form-info']),
13 | 'success': get_flashed_messages(category_filter=['form-success'])
14 | } %}
15 |
16 | {{ f.begin_form(form, flashes) }}
17 |
18 |
19 | {{ f.render_form_field(form.name) }}
20 | {{ f.render_form_field(form.website) }}
21 |
22 |
23 | {{ f.render_form_field(form.description) }}
24 |
25 | {{ f.render_form_field(form.address_autocomplete) }}
26 |
27 |
28 | {{ f.render_form_field(form.street_number) }}
29 | {{ f.render_form_field(form.route) }}
30 |
31 |
32 | {{ f.render_form_field(form.locality) }}
33 |
34 |
35 | {{ f.render_form_field(form.administrative_area_level_1) }}
36 | {{ f.render_form_field(form.postal_code) }}
37 |
38 |
39 | {{ f.render_form_field(form.submit) }}
40 | {{ f.end_form(form) }}
41 |
42 |
43 |
44 |
120 |
122 | {% endblock %}
123 |
--------------------------------------------------------------------------------
/tests/test_user_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import time
3 | from app import create_app, db
4 | from app.models import User, AnonymousUser, Permission, Role
5 |
6 |
7 | class UserModelTestCase(unittest.TestCase):
8 | def setUp(self):
9 | self.app = create_app('testing')
10 | self.app_context = self.app.app_context()
11 | self.app_context.push()
12 | db.create_all()
13 |
14 | def tearDown(self):
15 | db.session.remove()
16 | db.drop_all()
17 | self.app_context.pop()
18 |
19 | def test_password_setter(self):
20 | u = User(password='password')
21 | self.assertTrue(u.password_hash is not None)
22 |
23 | def test_no_password_getter(self):
24 | u = User(password='password')
25 | with self.assertRaises(AttributeError):
26 | u.password()
27 |
28 | def test_password_verification(self):
29 | u = User(password='password')
30 | self.assertTrue(u.verify_password('password'))
31 | self.assertFalse(u.verify_password('notpassword'))
32 |
33 | def test_password_salts_are_random(self):
34 | u = User(password='password')
35 | u2 = User(password='password')
36 | self.assertTrue(u.password_hash != u2.password_hash)
37 |
38 | def test_valid_confirmation_token(self):
39 | u = User(password='password')
40 | db.session.add(u)
41 | db.session.commit()
42 | token = u.generate_confirmation_token()
43 | self.assertTrue(u.confirm_account(token))
44 |
45 | def test_invalid_confirmation_token(self):
46 | u1 = User(password='password')
47 | u2 = User(password='notpassword')
48 | db.session.add(u1)
49 | db.session.add(u2)
50 | db.session.commit()
51 | token = u1.generate_confirmation_token()
52 | self.assertFalse(u2.confirm_account(token))
53 |
54 | def test_expired_confirmation_token(self):
55 | u = User(password='password')
56 | db.session.add(u)
57 | db.session.commit()
58 | token = u.generate_confirmation_token(1)
59 | time.sleep(2)
60 | self.assertFalse(u.confirm_account(token))
61 |
62 | def test_valid_reset_token(self):
63 | u = User(password='password')
64 | db.session.add(u)
65 | db.session.commit()
66 | token = u.generate_password_reset_token()
67 | self.assertTrue(u.reset_password(token, 'notpassword'))
68 | self.assertTrue(u.verify_password('notpassword'))
69 |
70 | def test_invalid_reset_token(self):
71 | u1 = User(password='password')
72 | u2 = User(password='notpassword')
73 | db.session.add(u1)
74 | db.session.add(u2)
75 | db.session.commit()
76 | token = u1.generate_password_reset_token()
77 | self.assertFalse(u2.reset_password(token, 'notnotpassword'))
78 | self.assertTrue(u2.verify_password('notpassword'))
79 |
80 | def test_valid_email_change_token(self):
81 | u = User(email='user@example.com', password='password')
82 | db.session.add(u)
83 | db.session.commit()
84 | token = u.generate_email_change_token('otheruser@example.org')
85 | self.assertTrue(u.change_email(token))
86 | self.assertTrue(u.email == 'otheruser@example.org')
87 |
88 | def test_invalid_email_change_token(self):
89 | u1 = User(email='user@example.com', password='password')
90 | u2 = User(email='otheruser@example.org', password='notpassword')
91 | db.session.add(u1)
92 | db.session.add(u2)
93 | db.session.commit()
94 | token = u1.generate_email_change_token('otherotheruser@example.net')
95 | self.assertFalse(u2.change_email(token))
96 | self.assertTrue(u2.email == 'otheruser@example.org')
97 |
98 | def test_duplicate_email_change_token(self):
99 | u1 = User(email='user@example.com', password='password')
100 | u2 = User(email='otheruser@example.org', password='notpassword')
101 | db.session.add(u1)
102 | db.session.add(u2)
103 | db.session.commit()
104 | token = u2.generate_email_change_token('user@example.com')
105 | self.assertFalse(u2.change_email(token))
106 | self.assertTrue(u2.email == 'otheruser@example.org')
107 |
108 | def test_roles_and_permissions(self):
109 | Role.insert_roles()
110 | u = User(email='user@example.com', password='password')
111 | self.assertTrue(u.can(Permission.GENERAL))
112 | self.assertFalse(u.can(Permission.ADMINISTER))
113 |
114 | def test_make_administrator(self):
115 | Role.insert_roles()
116 | u = User(email='user@example.com', password='password')
117 | self.assertFalse(u.can(Permission.ADMINISTER))
118 | u.role = Role.query.filter_by(
119 | permissions=Permission.ADMINISTER).first()
120 | self.assertTrue(u.can(Permission.ADMINISTER))
121 |
122 | def test_administrator(self):
123 | Role.insert_roles()
124 | r = Role.query.filter_by(permissions=Permission.ADMINISTER).first()
125 | u = User(email='user@example.com', password='password', role=r)
126 | self.assertTrue(u.can(Permission.ADMINISTER))
127 | self.assertTrue(u.can(Permission.GENERAL))
128 | self.assertTrue(u.is_admin())
129 |
130 | def test_anonymous(self):
131 | u = AnonymousUser()
132 | self.assertFalse(u.can(Permission.GENERAL))
133 |
--------------------------------------------------------------------------------
/app/models/attribute.py:
--------------------------------------------------------------------------------
1 | from .. import db
2 |
3 | user_tag_associations_table = db.Table(
4 | 'user_tag_associations', db.Model.metadata,
5 | db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')),
6 | db.Column('user_id', db.Integer, db.ForeignKey('users.id'))
7 | )
8 |
9 | resource_tag_associations_table = db.Table(
10 | 'resource_tag_associations', db.Model.metadata,
11 | db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')),
12 | db.Column('resource_id', db.Integer, db.ForeignKey('resources.id'))
13 | )
14 |
15 |
16 | class Tag(db.Model):
17 | __tablename__ = 'tags'
18 | id = db.Column(db.Integer, primary_key=True)
19 | name = db.Column(db.String(30), unique=True)
20 | users = db.relationship('User', secondary=user_tag_associations_table,
21 | backref='tags', lazy='dynamic')
22 | resources = db.relationship('Resource',
23 | secondary=resource_tag_associations_table,
24 | backref='tags', lazy='dynamic')
25 | type = db.Column(db.String(50))
26 | is_primary = db.Column(db.Boolean, default=False)
27 |
28 | __mapper_args__ = {
29 | 'polymorphic_on': type
30 | }
31 |
32 | def __init__(self, name, is_primary=False):
33 | """
34 | If possible, the helper methods get_by_name and create_tag
35 | should be used instead of explicitly using this constructor.
36 | """
37 | self.name = name
38 | self.is_primary = is_primary
39 |
40 | @staticmethod
41 | def get_by_name(name):
42 | """Helper for searching by Tag name."""
43 | result = Tag.query.filter_by(name=name).first()
44 | return result
45 |
46 | def __repr__(self):
47 | return '<%s \'%s\'>' % (self.type, self.name)
48 |
49 |
50 | class ResourceCategoryTag(Tag):
51 | __tablename__ = 'resource_category_tags'
52 | id = db.Column(db.Integer, db.ForeignKey('tags.id'), primary_key=True)
53 |
54 | __mapper_args__ = {
55 | 'polymorphic_identity': 'ResourceCategoryTag',
56 | }
57 |
58 | @staticmethod
59 | def create_resource_category_tag(name):
60 | """
61 | Helper to create a ResourceCategoryTag entry. Returns the newly
62 | created ResourceCategoryTag or the existing entry if name is already
63 | in the table.
64 | """
65 | result = Tag.get_by_name(name)
66 | # Tags must have unique names, so if a Tag that is not a
67 | # ResourceCategoryTag already has the name `name`, then an error is
68 | # raised.
69 | if result is not None and result.type != 'ResourceCategoryTag':
70 | raise ValueError("A tag with this name already exists.")
71 | if result is None:
72 | result = ResourceCategoryTag(name)
73 | db.session.add(result)
74 | db.session.commit()
75 | return result
76 |
77 | @staticmethod
78 | def generate_fake(count=10):
79 | """Generate count fake Tags for testing."""
80 | from faker import Faker
81 |
82 | fake = Faker()
83 |
84 | for i in range(count):
85 | created = False
86 | while not created:
87 | try:
88 | ResourceCategoryTag.\
89 | create_resource_category_tag(fake.word())
90 | created = True
91 | except ValueError:
92 | created = False
93 |
94 |
95 | class AffiliationTag(Tag):
96 | __tablename__ = 'affiliation_tags'
97 | id = db.Column(db.Integer, db.ForeignKey('tags.id'), primary_key=True)
98 |
99 | __mapper_args__ = {
100 | 'polymorphic_identity': 'AffiliationTag',
101 | }
102 |
103 | @staticmethod
104 | def create_affiliation_tag(name, is_primary=False):
105 | """
106 | Helper to create a AffiliationTag entry. Returns the newly
107 | created AffiliationTag or the existing entry if name is already
108 | in the table.
109 | """
110 | result = Tag.get_by_name(name)
111 | # Tags must have unique names, so if a Tag that is not an
112 | # AffiliationTag already has the name `name`, then an error is raised.
113 | if result is not None and result.type != 'AffiliationTag':
114 | raise ValueError("A tag with this name already exists.")
115 | if result is None:
116 | result = AffiliationTag(name, is_primary)
117 | db.session.add(result)
118 | db.session.commit()
119 | return result
120 |
121 | @staticmethod
122 | def generate_fake(count=10):
123 | """Generate count fake AffiliationTags for testing."""
124 | from faker import Faker
125 |
126 | fake = Faker()
127 |
128 | for i in range(count):
129 | created = False
130 | while not created:
131 | try:
132 | AffiliationTag.create_affiliation_tag(fake.word())
133 | created = True
134 | except ValueError:
135 | created = False
136 |
137 | @staticmethod
138 | def generate_default():
139 | """Generate default AffiliationTags."""
140 | default_affiliation_tags = [
141 | 'Veteran', 'Active Duty', 'National Guard', 'Reservist', 'Spouse',
142 | 'Dependent', 'Family Member', 'Supporter', 'Other'
143 | ]
144 | for tag in default_affiliation_tags:
145 | AffiliationTag.create_affiliation_tag(tag, is_primary=True)
146 |
--------------------------------------------------------------------------------
/app/templates/macros/nav_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_menu_items(endpoints) %}
2 | {% for endpoint, name, icon in endpoints %}
3 |
4 | {% if icon %}
5 |
6 | {% endif %}
7 | {{ name | safe }}
8 |
9 | {% endfor %}
10 | {% endmacro %}
11 |
12 | {% macro header_items(current_user) %}
13 |
14 | {% if current_user.is_authenticated() %}
15 | {% set href = url_for(current_user.role.index + '.index') %}
16 | {{ current_user.role.name }} Dashboard
17 | {% endif %}
18 | {% endmacro %}
19 |
20 | {% macro page_items(current_user) %}
21 | {% if current_user.is_authenticated() %}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
36 | Users
37 | Donate
38 |
39 |
46 | {% endif %}
47 | {% endmacro %}
48 |
49 | {% macro account_items_logged_in(current_user) %}
50 | View Profile
51 | Account Settings
52 | Help
53 | Log out
54 | {% endmacro %}
55 |
56 | {% macro account_items_logged_out() %}
57 | Register
58 | Log in
59 | Help
60 | {% endmacro %}
61 |
62 | {% macro mobile_nav(current_user, endpoints=None) %}
63 |
64 |
65 | {{ header_items(current_user) }}
66 |
69 |
70 |
71 | {# The menu items which will be shown when open-nav is clicked #}
72 |
83 |
84 | {% endmacro %}
85 |
86 | {# If `count` and `endpoints` are specified, the endpoints will be put into a
87 | # secondary menu. `count` should be the string (e.g. 'four') number of endpoints. #}
88 | {% macro desktop_nav(current_user, endpoints=None, count=None) %}
89 |
90 |
91 |
92 | {{ header_items(current_user) }}
93 |
107 |
108 |
109 |
110 | {# Endpoints go into a submenu #}
111 | {% if endpoints %}
112 |
117 | {% endif %}
118 |
119 | {% endmacro %}
120 |
121 | {% macro render_nav(current_user, count, endpoints) %}
122 |
126 | {% endmacro %}
127 |
--------------------------------------------------------------------------------
/app/models/resource.py:
--------------------------------------------------------------------------------
1 | from .. import db
2 | from random import randint
3 | from . import Address
4 |
5 |
6 | class Resource(db.Model):
7 | __tablename__ = 'resources'
8 | id = db.Column(db.Integer, primary_key=True)
9 | name = db.Column(db.String(64))
10 | description = db.Column(db.Text)
11 | website = db.Column(db.Text)
12 | address_id = db.Column(db.Integer, db.ForeignKey('addresses.id'))
13 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
14 | reviews = db.relationship('ResourceReview', backref='resource',
15 | lazy='dynamic')
16 | closed_resource_details = db.relationship('ClosedResourceDetail',
17 | backref='resource')
18 |
19 | def __init__(self, name, description, website):
20 | self.name = name
21 | self.description = description
22 | self.website = website
23 |
24 | @staticmethod
25 | def get_by_resource(name, description, website):
26 | """Helper for searching by all resource fields."""
27 | result = Resource.query.filter_by(name=name,
28 | description=description,
29 | website=website).first()
30 | return result
31 |
32 | @staticmethod
33 | def create_resource(name, description, website):
34 | """
35 | Helper to create an Resource entry. Returns the newly created Resource
36 | or the existing entry if all resource fields are already in the table.
37 | """
38 | result = Resource.get_by_resource(name,
39 | description,
40 | website)
41 | if result is None:
42 | result = Resource(name=name,
43 | description=description,
44 | website=website)
45 | db.session.add(result)
46 | db.session.commit()
47 | return result
48 |
49 | @staticmethod
50 | def generate_fake():
51 | # TODO: make sure fake resources have users
52 |
53 | """Generate count fake Resources for testing."""
54 | from faker import Faker
55 |
56 | fake = Faker()
57 |
58 | unused_addresses = [address for address in Address.query.all()
59 | if len(address.resources.all()) == 0]
60 |
61 | for address in unused_addresses:
62 | r = Resource(
63 | name=fake.name(),
64 | description=fake.text(),
65 | website=fake.url()
66 | )
67 | r.address = address
68 | db.session.add(r)
69 | db.session.commit()
70 |
71 | def __repr__(self):
72 | return '' % self.name
73 |
74 |
75 | class ResourceReview(db.Model):
76 | __tablename__ = 'resource_reviews'
77 | id = db.Column(db.Integer, primary_key=True)
78 | timestamp = db.Column(db.DateTime)
79 | content = db.Column(db.Text)
80 | rating = db.Column(db.Integer) # 1 to 5
81 | count_likes = db.Column(db.Integer, default=0)
82 | count_dislikes = db.Column(db.Integer, default=0)
83 | resource_id = db.Column(db.Integer, db.ForeignKey('resources.id'))
84 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
85 |
86 | def __init__(self, timestamp, content, rating):
87 | self.timestamp = timestamp
88 | self.content = content
89 | self.rating = rating
90 |
91 | @staticmethod
92 | def generate_fake(count=10):
93 | """Generate count fake Reviews for testing."""
94 | from faker import Faker
95 |
96 | fake = Faker()
97 |
98 | # TODO: make sure fake reviews have users and resources
99 | for i in range(count):
100 | r = ResourceReview(
101 | timestamp=fake.date_time(),
102 | content=fake.text(),
103 | rating=randint(1, 5)
104 | )
105 | r.count_likes = randint(1, 500)
106 | r.count_dislikes = randint(1, 500)
107 | db.session.add(r)
108 | db.session.commit()
109 |
110 | def __repr__(self):
111 | return ' \'%s\'>' %\
112 | (self.resource_id, self.content)
113 |
114 |
115 | class ClosedResourceDetail(db.Model):
116 | __tablename__ = 'closed_resource_details'
117 | id = db.Column(db.Integer, primary_key=True)
118 | explanation = db.Column(db.Text)
119 | connection = db.Column(db.Text)
120 | resource_id = db.Column(db.Integer, db.ForeignKey('resources.id'))
121 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
122 |
123 | @staticmethod
124 | def create_closed_resource(explanation, connection,
125 | resource_id=None, user_id=None):
126 | """Helper to create a ClosedResourceDetail entry."""
127 | result = ClosedResourceDetail(explanation=explanation,
128 | connection=connection)
129 |
130 | if resource_id:
131 | result.resource_id = resource_id
132 | if user_id:
133 | result.user_id = user_id
134 | db.session.add(result)
135 | db.session.commit()
136 | return result
137 |
138 | def __init__(self, explanation, connection):
139 | self.explanation = explanation
140 | self.connection = connection
141 |
142 | def __repr__(self):
143 | return ' \'%s\'>' %\
144 | (self.resource_id, self.explanation)
145 |
--------------------------------------------------------------------------------
/app/templates/admin/registered_users.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 | Back to dashboard
8 |
9 |
15 |
35 |
36 | {# Use overflow-x: scroll so that mobile views don't freak out
37 | # when the table is too wide #}
38 |
39 |
40 |
41 |
42 | Full Name
43 | Email address
44 | Account type
45 | Donor Level
46 |
47 |
48 |
49 | {% for u in users | sort(attribute='last_name') %}
50 |
51 | {{ u.full_name() }}
52 | {{ u.email }}
53 | {{ u.role.name }}
54 |
55 |
56 |
57 |
58 |
{{ u.donor_level.name }}
59 |
64 |
65 |
66 |
67 | {% endfor %}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
108 | {% endblock %}
109 |
--------------------------------------------------------------------------------
/app/account/forms.py:
--------------------------------------------------------------------------------
1 | from flask import url_for
2 | from flask.ext.wtf import Form
3 | from wtforms.fields import (
4 | StringField,
5 | PasswordField,
6 | BooleanField,
7 | SubmitField,
8 | TextAreaField,
9 | DateField,
10 | SelectMultipleField
11 | )
12 | from wtforms.fields.html5 import EmailField
13 | from wtforms.validators import (
14 | Length,
15 | Email,
16 | EqualTo,
17 | URL,
18 | InputRequired,
19 | Optional,
20 | )
21 | from wtforms import ValidationError
22 | from ..models import User, AffiliationTag
23 |
24 |
25 | class LoginForm(Form):
26 | email = EmailField('Email', validators=[
27 | InputRequired(),
28 | Length(1, 64),
29 | Email()
30 | ])
31 | password = PasswordField('Password', validators=[InputRequired()])
32 | remember_me = BooleanField('Keep me logged in')
33 | submit = SubmitField('Log in')
34 |
35 |
36 | class RegistrationForm(Form):
37 | first_name = StringField('First name', validators=[
38 | InputRequired(),
39 | Length(1, 64)
40 | ])
41 | last_name = StringField('Last name', validators=[
42 | InputRequired(),
43 | Length(1, 64)
44 | ])
45 | email = EmailField('Email', validators=[
46 | InputRequired(),
47 | Length(1, 64),
48 | Email()
49 | ])
50 | password = PasswordField('Password', validators=[
51 | InputRequired(),
52 | EqualTo('password2', 'Passwords must match')
53 | ])
54 | password2 = PasswordField('Confirm password', validators=[InputRequired()])
55 | zip_code = StringField('ZIP Code', validators=[
56 | InputRequired(),
57 | Length(5, 5)
58 | ])
59 | submit = SubmitField('Register')
60 |
61 | def validate_email(self, field):
62 | if User.query.filter_by(email=field.data).first():
63 | raise ValidationError('Email already registered. (Did you mean to '
64 | 'log in instead?)'
65 | .format(url_for('account.login')))
66 |
67 |
68 | class RequestResetPasswordForm(Form):
69 | email = EmailField('Email', validators=[
70 | InputRequired(),
71 | Length(1, 64),
72 | Email()])
73 | submit = SubmitField('Reset password')
74 |
75 | # We don't validate the email address so we don't confirm to attackers
76 | # that an account with the given email exists.
77 |
78 |
79 | class ResetPasswordForm(Form):
80 | email = EmailField('Email', validators=[
81 | InputRequired(),
82 | Length(1, 64),
83 | Email()])
84 | new_password = PasswordField('New password', validators=[
85 | InputRequired(),
86 | EqualTo('new_password2', 'Passwords must match.')
87 | ])
88 | new_password2 = PasswordField('Confirm new password',
89 | validators=[InputRequired()])
90 | submit = SubmitField('Reset password')
91 |
92 | def validate_email(self, field):
93 | if User.query.filter_by(email=field.data).first() is None:
94 | raise ValidationError('Unknown email address.')
95 |
96 |
97 | class CreatePasswordForm(Form):
98 | password = PasswordField('Password', validators=[
99 | InputRequired(),
100 | EqualTo('password2', 'Passwords must match.')
101 | ])
102 | password2 = PasswordField('Confirm new password',
103 | validators=[InputRequired()])
104 | submit = SubmitField('Set password')
105 |
106 |
107 | class ChangePasswordForm(Form):
108 | old_password = PasswordField('Old password', validators=[InputRequired()])
109 | new_password = PasswordField('New password', validators=[
110 | InputRequired(),
111 | EqualTo('new_password2', 'Passwords must match.')
112 | ])
113 | new_password2 = PasswordField('Confirm new password',
114 | validators=[InputRequired()])
115 | submit = SubmitField('Update password')
116 |
117 |
118 | class ChangeEmailForm(Form):
119 | email = EmailField('New email', validators=[
120 | InputRequired(),
121 | Length(1, 64),
122 | Email()])
123 | password = PasswordField('Password', validators=[InputRequired()])
124 | submit = SubmitField('Update email')
125 |
126 | def validate_email(self, field):
127 | if User.query.filter_by(email=field.data).first():
128 | raise ValidationError('Email already registered.')
129 |
130 |
131 | class EditProfileForm(Form):
132 | first_name = StringField('First name', validators=[
133 | InputRequired(),
134 | Length(1, 64)
135 | ])
136 | last_name = StringField('Last name', validators=[
137 | InputRequired(),
138 | Length(1, 64)
139 | ])
140 | bio = TextAreaField('About Me')
141 | birthday = DateField(
142 | label='Birthday',
143 | description="YYYY-MM-DD",
144 | format="%Y-%m-%d", validators=[Optional()])
145 | facebook_link = StringField(
146 | 'Facebook Profile',
147 | description="https://",
148 | validators=[URL(), Optional()]
149 | )
150 | linkedin_link = StringField(
151 | 'LinkedIn Profile',
152 | description="https://",
153 | validators=[URL(), Optional()]
154 | )
155 | affiliations = SelectMultipleField(
156 | 'Affiliations',
157 | default=[]
158 | )
159 | submit = SubmitField('Update profile')
160 |
161 | def __init__(self, *args):
162 | super(EditProfileForm, self).__init__(*args)
163 | self.affiliations.choices = (
164 | [(str(affiliation.id), str(affiliation.name))
165 | for affiliation in AffiliationTag.query.all()]
166 | )
167 |
--------------------------------------------------------------------------------
/app/templates/resources/read_resource.html:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.html' %}
2 | {% import 'macros/form_macros.html' as f %}
3 |
4 | {% block content %}
5 |
6 |
7 | {% if closed_details|length is not equalto 0 %}
8 |
9 |
10 |
11 | {% if closed_details|length is equalto 1 %}
12 | 1 person has reported that this resource is no longer available.
13 | {% else %}
14 | {{ closed_details|length }} people have reported that this resource is no longer available.
15 | {% endif %}
16 |
17 |
18 | {% for closed_detail in closed_details %}
19 | {% if closed_detail.explanation or closed_detail.connection %}
20 |
21 | {% if closed_detail.explanation %}
22 | Explanation : {{ closed_detail.explanation }}
23 | {% endif %}
24 | {% if closed_detail.connection %}
25 | Connection : {{ closed_detail.connection }}
26 | {% endif %}
27 |
28 | {% endif %}
29 | {% endfor %}
30 |
31 |
32 | {% endif %}
33 |
34 |
Is this resource no longer available?
35 |
36 |
37 |
38 |
41 |
42 |
43 | {% set flashes = {
44 | 'error': get_flashed_messages(category_filter=['form-error']),
45 | 'warning': get_flashed_messages(category_filter=['form-check-email']),
46 | 'info': get_flashed_messages(category_filter=['form-info']),
47 | 'success': get_flashed_messages(category_filter=['form-success'])
48 | } %}
49 | {{ f.begin_form(closed_form, flashes, action='/resources/close/' + resource.id|string) }}
50 | {{ f.render_form_field(closed_form.explanation) }}
51 | {{ f.render_form_field(closed_form.connection) }}
52 | {{ f.render_form_field(closed_form.submit) }}
53 |
54 | {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }}
55 | {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }}
56 | {{ f.form_message(flashes['info'], header='Information', class='info') }}
57 | {{ f.form_message(flashes['success'], header='Success!', class='success') }}
58 |
59 | {{ f.end_form(closed_form) }}
60 |
61 |
62 |
63 |
64 |
Resource
65 |
66 |
67 | Name
68 | {{ resource.name }}
69 |
70 |
71 | Description
72 | {{ resource.description }}
73 |
74 |
75 | Website
76 | {{ resource.website }}
77 |
78 |
79 | Address
80 | {{ resource.address.street_address }}
81 |
82 |
83 | User
84 | {{ resource.user.full_name() }}
85 |
86 |
87 |
88 | Write a Review
89 |
90 |
91 |
92 | {% block form %}
93 | {% endblock %}
94 |
95 |
96 |
Reviews
97 | {% for review in reviews|sort(attribute='id', reverse=True) %}
98 |
User: {{ review.user.full_name() }}
99 |
100 |
101 | Rating
102 | {{ review.rating }}
103 |
104 |
105 | Content
106 | {{ review.content }}
107 |
108 |
109 | {% if review.user_id is equalto current_user_id %}
110 |
111 |
112 |
113 |
114 |
115 |
116 | {% endif %}
117 | {% else %}
118 |
Sorry, no reviews for this resource yet.
119 | {% endfor %}
120 |
121 |
122 |
123 |
132 | {% endblock %}
133 |
--------------------------------------------------------------------------------
/app/admin/views.py:
--------------------------------------------------------------------------------
1 | from ..decorators import admin_required
2 |
3 | from flask import render_template, abort, redirect, flash, url_for, request
4 | from flask.ext.login import login_required, current_user
5 |
6 | from forms import (
7 | ChangeUserEmailForm,
8 | NewUserForm,
9 | ChangeAccountTypeForm,
10 | InviteUserForm,
11 | )
12 | from . import admin
13 | from ..models import User, Role, DonorLevel
14 | from .. import db
15 | from ..email import send_email
16 |
17 |
18 | @admin.route('/')
19 | @login_required
20 | @admin_required
21 | def index():
22 | """Admin dashboard page."""
23 | return render_template('admin/index.html')
24 |
25 |
26 | @admin.route('/new-user', methods=['GET', 'POST'])
27 | @login_required
28 | @admin_required
29 | def new_user():
30 | """Create a new user."""
31 | form = NewUserForm()
32 | if form.validate_on_submit():
33 | user = User(role=form.role.data,
34 | first_name=form.first_name.data,
35 | last_name=form.last_name.data,
36 | email=form.email.data,
37 | password=form.password.data)
38 | db.session.add(user)
39 | db.session.commit()
40 | flash('User {} successfully created'.format(user.full_name()),
41 | 'form-success')
42 | return render_template('admin/new_user.html', form=form)
43 |
44 |
45 | @admin.route('/invite-user', methods=['GET', 'POST'])
46 | @login_required
47 | @admin_required
48 | def invite_user():
49 | """Invites a new user to create an account and set their own password."""
50 | form = InviteUserForm()
51 | if form.validate_on_submit():
52 | user = User(role=form.role.data,
53 | first_name=form.first_name.data,
54 | last_name=form.last_name.data,
55 | email=form.email.data)
56 | db.session.add(user)
57 | db.session.commit()
58 | token = user.generate_confirmation_token()
59 | send_email(user.email,
60 | 'You Are Invited To Join',
61 | 'account/email/invite',
62 | user=user,
63 | user_id=user.id,
64 | token=token)
65 | flash('User {} successfully invited'.format(user.full_name()),
66 | 'form-success')
67 | return render_template('admin/new_user.html', form=form)
68 |
69 |
70 | @admin.route('/users')
71 | @login_required
72 | @admin_required
73 | def registered_users():
74 | """View all registered users."""
75 | users = User.query.all()
76 | roles = Role.query.all()
77 | donor_levels = DonorLevel.query.all()
78 | return render_template('admin/registered_users.html', users=users,
79 | roles=roles, donor_levels=donor_levels)
80 |
81 |
82 | @admin.route('/user/')
83 | @admin.route('/user//info')
84 | @login_required
85 | @admin_required
86 | def user_info(user_id):
87 | """View a user's profile."""
88 | user = User.query.filter_by(id=user_id).first()
89 | if user is None:
90 | abort(404)
91 | return render_template('admin/manage_user.html', user=user)
92 |
93 |
94 | @admin.route('/user//change-email', methods=['GET', 'POST'])
95 | @login_required
96 | @admin_required
97 | def change_user_email(user_id):
98 | """Change a user's email."""
99 | user = User.query.filter_by(id=user_id).first()
100 | if user is None:
101 | abort(404)
102 | form = ChangeUserEmailForm()
103 | if form.validate_on_submit():
104 | user.email = form.email.data
105 | db.session.add(user)
106 | db.session.commit()
107 | flash('Email for user {} successfully changed to {}.'
108 | .format(user.full_name(), user.email),
109 | 'form-success')
110 | return render_template('admin/manage_user.html', user=user, form=form)
111 |
112 |
113 | @admin.route('/user//change-account-type',
114 | methods=['GET', 'POST'])
115 | @login_required
116 | @admin_required
117 | def change_account_type(user_id):
118 | """Change a user's account type."""
119 | if current_user.id == user_id:
120 | flash('You cannot change the type of your own account. Please ask '
121 | 'another administrator to do this.', 'error')
122 | return redirect(url_for('admin.user_info', user_id=user_id))
123 |
124 | user = User.query.get(user_id)
125 | if user is None:
126 | abort(404)
127 | form = ChangeAccountTypeForm()
128 | if form.validate_on_submit():
129 | user.role = form.role.data
130 | db.session.add(user)
131 | db.session.commit()
132 | flash('Role for user {} successfully changed to {}.'
133 | .format(user.full_name(), user.role.name),
134 | 'form-success')
135 | return render_template('admin/manage_user.html', user=user, form=form)
136 |
137 |
138 | @admin.route('/user//delete')
139 | @login_required
140 | @admin_required
141 | def delete_user_request(user_id):
142 | """Request deletion of a user's account."""
143 | user = User.query.filter_by(id=user_id).first()
144 | if user is None:
145 | abort(404)
146 | return render_template('admin/manage_user.html', user=user)
147 |
148 |
149 | @admin.route('/user//_delete')
150 | @login_required
151 | @admin_required
152 | def delete_user(user_id):
153 | """Delete a user's account."""
154 | if current_user.id == user_id:
155 | flash('You cannot delete your own account. Please ask another '
156 | 'administrator to do this.', 'error')
157 | else:
158 | user = User.query.filter_by(id=user_id).first()
159 | db.session.delete(user)
160 | db.session.commit()
161 | flash('Successfully deleted user %s.' % user.full_name(), 'success')
162 | return redirect(url_for('admin.registered_users'))
163 |
164 |
165 | @admin.route('/users/update-donor-level', methods=['POST'])
166 | @login_required
167 | @admin_required
168 | def update_donor_level():
169 | user_id = request.form.get('user_id')
170 | donor_level_name = request.form.get('donor_level')
171 |
172 | user = User.query.get(user_id)
173 | if user is None:
174 | abort(404)
175 | donor_level = DonorLevel.query.filter_by(name=donor_level_name).first()
176 | if donor_level is None:
177 | abort(404)
178 | user.donor_level_id = donor_level.id
179 |
180 | db.session.add(user)
181 | db.session.commit()
182 |
183 | return redirect(url_for('admin.registered_users'))
184 |
--------------------------------------------------------------------------------
/app/resources/views.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, redirect, url_for, flash
2 | from flask.ext.login import login_required, current_user
3 | from . import resources
4 | from .. import db
5 | from ..models import Resource, ZIPCode, Address, ResourceReview,\
6 | ClosedResourceDetail
7 | from .forms import ResourceForm, ReviewForm, ClosedResourceDetailForm
8 | from datetime import datetime
9 |
10 |
11 | @resources.route('/')
12 | @login_required
13 | def index():
14 | return render_template('resources/index.html',
15 | resources=Resource.query.all())
16 |
17 |
18 | @resources.route('/create', methods=['GET', 'POST'])
19 | @login_required
20 | def create_resource():
21 | form = ResourceForm()
22 | if form.validate_on_submit():
23 | # Converting Google Places API names to our model names.
24 | name = form.name.data
25 | street_address = str(form.street_number.data) + ' ' + form.route.data
26 | city = form.locality.data
27 | state = form.administrative_area_level_1.data
28 | zip_code = ZIPCode.create_zip_code(form.postal_code.data)
29 | address = Address.create_address(name,
30 | street_address,
31 | city,
32 | state)
33 | address.zip_code = zip_code
34 | description = form.description.data
35 | website = form.website.data
36 | resource = Resource.create_resource(name,
37 | description,
38 | website)
39 | resource.address = address
40 | resource.user = current_user._get_current_object()
41 | return redirect(url_for('resources.read_resource',
42 | resource_id=resource.id))
43 | return render_template('resources/create_resource.html', form=form)
44 |
45 |
46 | @resources.route('/close/', methods=['GET', 'POST'])
47 | @login_required
48 | def close_resource(resource_id):
49 | resource = Resource.query.get_or_404(resource_id)
50 | closed_details = ClosedResourceDetail.query.filter_by(
51 | resource_id=resource_id).all()
52 | closed_form = ClosedResourceDetailForm()
53 | if closed_form.validate_on_submit():
54 | ClosedResourceDetail.create_closed_resource(
55 | closed_form.explanation.data,
56 | closed_form.connection.data,
57 | resource_id,
58 | current_user.id)
59 | return redirect(url_for('resources.read_resource',
60 | resource_id=resource_id))
61 |
62 | return render_template('resources/read_resource.html',
63 | resource=resource,
64 | reviews=resource.reviews,
65 | current_user_id=current_user.id,
66 | closed_form=closed_form,
67 | closed_details=closed_details,
68 | show_modal=True)
69 |
70 |
71 | @resources.route('/read/')
72 | @login_required
73 | def read_resource(resource_id):
74 | resource = Resource.query.get_or_404(resource_id)
75 | closed_details = ClosedResourceDetail.query.filter_by(
76 | resource_id=resource_id).all()
77 | closed_form = ClosedResourceDetailForm()
78 | return render_template('resources/read_resource.html',
79 | resource=resource,
80 | reviews=resource.reviews,
81 | current_user_id=current_user.id,
82 | closed_form=closed_form,
83 | closed_details=closed_details)
84 |
85 |
86 | @resources.route('/review/create/', methods=['GET', 'POST'])
87 | @login_required
88 | def create_review(resource_id):
89 | resource = Resource.query.get_or_404(resource_id)
90 | form = ReviewForm()
91 | if form.validate_on_submit():
92 | review = ResourceReview(timestamp=datetime.now(),
93 | content=form.content.data,
94 | rating=form.rating.data)
95 | review.resource = resource
96 | review.user = current_user._get_current_object()
97 | db.session.add(review)
98 | db.session.commit()
99 | return redirect(url_for('resources.read_resource',
100 | resource_id=resource.id))
101 | closed_details = ClosedResourceDetail.query.filter_by(
102 | resource_id=resource_id).all()
103 | closed_form = ClosedResourceDetailForm()
104 | return render_template('resources/create_review.html',
105 | resource=resource,
106 | reviews=resource.reviews,
107 | current_user_id=current_user.id,
108 | form=form,
109 | closed_form=closed_form,
110 | closed_details=closed_details)
111 |
112 |
113 | @resources.route('/review/update/', methods=['GET', 'POST'])
114 | @login_required
115 | def update_review(review_id):
116 | review = ResourceReview.query.get_or_404(review_id)
117 | resource = review.resource
118 | if current_user.id != review.user.id:
119 | flash('You cannot edit a review you did not write.', 'error')
120 | return redirect(url_for('resources.read_resource',
121 | resource_id=resource.id))
122 | form = ReviewForm()
123 | if form.validate_on_submit():
124 | review.timestamp = datetime.now()
125 | review.content = form.content.data
126 | review.rating = form.rating.data
127 | db.session.add(review)
128 | db.session.commit()
129 | return redirect(url_for('resources.read_resource',
130 | resource_id=resource.id))
131 | else:
132 | form.content.data = review.content
133 | form.rating.data = review.rating
134 | closed_details = ClosedResourceDetail.query.filter_by(
135 | resource_id=resource.id).all()
136 | closed_form = ClosedResourceDetailForm()
137 | return render_template('resources/create_review.html',
138 | resource=resource,
139 | reviews=resource.reviews,
140 | current_user_id=current_user.id,
141 | form=form,
142 | closed_form=closed_form,
143 | closed_details=closed_details)
144 |
145 |
146 | @resources.route('/review/delete/')
147 | @login_required
148 | def delete_review(review_id):
149 | review = ResourceReview.query.get_or_404(review_id)
150 | resource = review.resource
151 | if current_user.id != review.user.id:
152 | flash('You cannot delete a review you did not write.', 'error')
153 | else:
154 | db.session.delete(review)
155 | db.session.commit()
156 | return redirect(url_for('resources.read_resource',
157 | resource_id=resource.id))
158 |
--------------------------------------------------------------------------------
/app/models/location.py:
--------------------------------------------------------------------------------
1 | from .. import db
2 | from geopy.geocoders import Nominatim
3 |
4 |
5 | class ZIPCode(db.Model):
6 | __tablename__ = 'zip_codes'
7 | id = db.Column(db.Integer, primary_key=True)
8 | zip_code = db.Column(db.String(5), unique=True, index=True)
9 | users = db.relationship('User', backref='zip_code', lazy='dynamic')
10 | addresses = db.relationship('Address', backref='zip_code', lazy='dynamic')
11 | longitude = db.Column(db.Float)
12 | latitude = db.Column(db.Float)
13 |
14 | def __init__(self, zip_code):
15 | """
16 | If possible, the helper methods get_by_zip_code and create_zip_code
17 | should be used instead of explicitly using this constructor.
18 | """
19 | getcoords = Nominatim(country_bias='us')
20 | loc = getcoords.geocode(zip_code)
21 | if loc is None:
22 | raise ValueError('zip code \'%s\' is invalid' % zip_code)
23 | self.longitude = loc.longitude
24 | self.latitude = loc.latitude
25 | self.zip_code = zip_code
26 |
27 | @staticmethod
28 | def get_by_zip_code(zip_code):
29 | """Helper for searching by 5 digit zip codes."""
30 | result = ZIPCode.query.filter_by(zip_code=zip_code).first()
31 | return result
32 |
33 | @staticmethod
34 | def create_zip_code(zip_code):
35 | """
36 | Helper to create a ZIPCode entry. Returns the newly created ZIPCode
37 | or the existing entry if zip_code is already in the table.
38 | """
39 | result = ZIPCode.get_by_zip_code(zip_code)
40 | if result is None:
41 | result = ZIPCode(zip_code)
42 | db.session.add(result)
43 | db.session.commit()
44 | return result
45 |
46 | @staticmethod
47 | def generate_fake():
48 | """
49 | Populate the zip_codes table with arbitrary but real zip codes.
50 | The zip codes generated by the faker library
51 | are not necessarily valid or guaranteed to be located in the US.
52 | """
53 | zip_codes = ['19104', '01810', '02420', '75205', '94305',
54 | '47906', '60521']
55 |
56 | for zip_code in zip_codes:
57 | ZIPCode.create_zip_code(zip_code)
58 |
59 | def __repr__(self):
60 | return '' % self.zip_code
61 |
62 |
63 | class Address(db.Model):
64 | __tablename__ = 'addresses'
65 | id = db.Column(db.Integer, primary_key=True)
66 | name = db.Column(db.Text) # ABC MOVERS
67 | street_address = db.Column(db.Text) # 1500 E MAIN AVE STE 201
68 | city = db.Column(db.Text)
69 | state = db.Column(db.String(2))
70 | zip_code_id = db.Column(db.Integer, db.ForeignKey('zip_codes.id'))
71 | resources = db.relationship('Resource', backref='address', lazy='dynamic')
72 |
73 | def __init__(self, name, street_address, city, state):
74 | """
75 | If possible, the helper methods get_by_address and create_address
76 | should be used instead of explicitly using this constructor.
77 | """
78 | self.name = name
79 | self.street_address = street_address
80 | self.city = city
81 | self.state = state
82 |
83 | @staticmethod
84 | def get_by_address(name, street_address, city, state):
85 | """Helper for searching by all address fields."""
86 | result = Address.query.filter_by(name=name,
87 | street_address=street_address,
88 | city=city,
89 | state=state).first()
90 | return result
91 |
92 | @staticmethod
93 | def create_address(name, street_address, city, state):
94 | """
95 | Helper to create an Address entry. Returns the newly created Address
96 | or the existing entry if all address fields are already in the table.
97 | """
98 | result = Address.get_by_address(name,
99 | street_address,
100 | city,
101 | state)
102 | if result is None:
103 | result = Address(name=name,
104 | street_address=street_address,
105 | city=city,
106 | state=state)
107 | db.session.add(result)
108 | db.session.commit()
109 | return result
110 |
111 | @staticmethod
112 | def generate_fake():
113 | """Generate addresses that actually exist for map testing."""
114 | from faker import Faker
115 |
116 | fake = Faker()
117 |
118 | addresses = [
119 | {
120 | 'street_address': '6511 Candlebrite Dr',
121 | 'city': 'San Antonio',
122 | 'state': 'TX',
123 | 'zip_code': '78244'
124 | }, {
125 | 'street_address': '6111 Lausche Ave',
126 | 'city': 'Cleveland',
127 | 'state': 'OH',
128 | 'zip_code': '44103'
129 | }, {
130 | 'street_address': '11600 Seminole Blvd',
131 | 'city': 'Largo',
132 | 'state': 'FL',
133 | 'zip_code': '33778'
134 | }, {
135 | 'street_address': '6400 Westown Pkwy',
136 | 'city': 'W Des Moines',
137 | 'state': 'IA',
138 | 'zip_code': '50266'
139 | }, {
140 | 'street_address': '3650 Spruce St',
141 | 'city': 'Philadelphia',
142 | 'state': 'PA',
143 | 'zip_code': '19104'
144 | }, {
145 | 'street_address': '3650 Spruce St',
146 | 'city': 'Philadelphia',
147 | 'state': 'PA',
148 | 'zip_code': '19104'
149 | }, {
150 | 'street_address': '6879 N Wildwood Ave',
151 | 'city': 'Chicago',
152 | 'state': 'IL',
153 | 'zip_code': '60646'
154 | }, {
155 | 'street_address': '3809 Maple Ave',
156 | 'city': 'Castalia',
157 | 'state': 'OH',
158 | 'zip_code': '44824'
159 | }, {
160 | 'street_address': '9001 Triple Ridge Rde',
161 | 'city': 'Fairfax Sta',
162 | 'state': 'VA',
163 | 'zip_code': '22039'
164 | }, {
165 | 'street_address': '610 Lake Forbing Dr',
166 | 'city': 'Shreveport',
167 | 'state': 'LA ',
168 | 'zip_code': '71106'
169 | }
170 | ]
171 | for address in addresses:
172 | a = Address.create_address(
173 | name=fake.name(),
174 | street_address=address['street_address'],
175 | city=address['city'],
176 | state=address['state']
177 | )
178 | a.zip_code = ZIPCode.create_zip_code(address['zip_code'])
179 | db.session.add(a)
180 | db.session.commit()
181 |
182 | def __repr__(self):
183 | return '' % self.name
184 |
--------------------------------------------------------------------------------
/app/models/user.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 | from flask.ext.login import UserMixin, AnonymousUserMixin
3 | from werkzeug.security import generate_password_hash, check_password_hash
4 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, \
5 | BadSignature, SignatureExpired
6 | from .. import db, login_manager
7 |
8 |
9 | class Permission:
10 | GENERAL = 0x01
11 | ADMINISTER = 0xff
12 |
13 |
14 | class Role(db.Model):
15 | __tablename__ = 'roles'
16 | id = db.Column(db.Integer, primary_key=True)
17 | name = db.Column(db.String(64), unique=True)
18 | index = db.Column(db.String(64))
19 | default = db.Column(db.Boolean, default=False, index=True)
20 | permissions = db.Column(db.Integer)
21 | users = db.relationship('User', backref='role', lazy='dynamic')
22 |
23 | @staticmethod
24 | def insert_roles():
25 | roles = {
26 | 'User': (
27 | Permission.GENERAL, 'main', True
28 | ),
29 | 'Administrator': (
30 | Permission.ADMINISTER, 'admin', False # grants all permissions
31 | )
32 | }
33 | for r in roles:
34 | role = Role.query.filter_by(name=r).first()
35 | if role is None:
36 | role = Role(name=r)
37 | role.permissions = roles[r][0]
38 | role.index = roles[r][1]
39 | role.default = roles[r][2]
40 | db.session.add(role)
41 | db.session.commit()
42 |
43 | def __repr__(self):
44 | return '' % self.name
45 |
46 |
47 | class UserLinks(db.Model):
48 | __tablename__ = 'user_links'
49 | id = db.Column(db.Integer, primary_key=True)
50 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
51 | facebook_link = db.Column(db.String(100))
52 | linkedin_link = db.Column(db.String(100))
53 | twitter_link = db.Column(db.String(100))
54 | instagram_link = db.Column(db.String(100))
55 |
56 | def __repr__(self):
57 | return '' % self.id
58 |
59 |
60 | class DonorLevel(db.Model):
61 | __tablename__ = 'donor_levels'
62 | id = db.Column(db.Integer, primary_key=True)
63 | name = db.Column(db.String(50))
64 | description = db.Column(db.Text)
65 | users = db.relationship('User', backref='donor_level', lazy='dynamic')
66 |
67 | @staticmethod
68 | def insert_donor_levels():
69 | donor_levels = {
70 | 'Advocate',
71 | 'Supporter',
72 | 'Benefactor'
73 | }
74 | for name in donor_levels:
75 | donor_level = DonorLevel.query.filter_by(name=name).first()
76 | if donor_level is None:
77 | donor_level = DonorLevel(name=name)
78 | db.session.add(donor_level)
79 | db.session.commit()
80 |
81 | def __repr__(self):
82 | return '' % self.name
83 |
84 |
85 | class User(UserMixin, db.Model):
86 | __tablename__ = 'users'
87 | id = db.Column(db.Integer, primary_key=True)
88 | confirmed = db.Column(db.Boolean, default=False)
89 | first_name = db.Column(db.String(64), index=True)
90 | last_name = db.Column(db.String(64), index=True)
91 | email = db.Column(db.String(64), unique=True, index=True)
92 | password_hash = db.Column(db.String(128))
93 | zip_code_id = db.Column(db.Integer, db.ForeignKey('zip_codes.id'))
94 | role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
95 | donor_level_id = db.Column(db.Integer, db.ForeignKey('donor_levels.id'))
96 | bio = db.Column(db.Text)
97 | causes = db.Column(db.Text)
98 | user_links = db.relationship('UserLinks', lazy='joined', uselist=False)
99 | resources = db.relationship('Resource', backref='user', lazy='dynamic')
100 | resource_reviews = db.relationship('ResourceReview', backref='user',
101 | lazy='dynamic')
102 | birthday = db.Column(db.Date, index=True)
103 | closed_resource_details = db.relationship('ClosedResourceDetail',
104 | backref='user')
105 |
106 | def __init__(self, **kwargs):
107 | super(User, self).__init__(**kwargs)
108 | if self.role is None:
109 | if self.email == current_app.config['ADMIN_EMAIL']:
110 | self.role = Role.query.filter_by(
111 | permissions=Permission.ADMINISTER).first()
112 | if self.role is None:
113 | self.role = Role.query.filter_by(default=True).first()
114 |
115 | def full_name(self):
116 | return '%s %s' % (self.first_name, self.last_name)
117 |
118 | def can(self, permissions):
119 | return self.role is not None and \
120 | (self.role.permissions & permissions) == permissions
121 |
122 | def is_admin(self):
123 | return self.can(Permission.ADMINISTER)
124 |
125 | @property
126 | def password(self):
127 | raise AttributeError('`password` is not a readable attribute')
128 |
129 | @password.setter
130 | def password(self, password):
131 | self.password_hash = generate_password_hash(password)
132 |
133 | def verify_password(self, password):
134 | return check_password_hash(self.password_hash, password)
135 |
136 | def generate_confirmation_token(self, expiration=604800):
137 | """Generate a confirmation token to email a new user."""
138 |
139 | s = Serializer(current_app.config['SECRET_KEY'], expiration)
140 | return s.dumps({'confirm': self.id})
141 |
142 | def generate_email_change_token(self, new_email, expiration=3600):
143 | """Generate an email change token to email an existing user."""
144 | s = Serializer(current_app.config['SECRET_KEY'], expiration)
145 | return s.dumps({'change_email': self.id, 'new_email': new_email})
146 |
147 | def generate_password_reset_token(self, expiration=3600):
148 | """
149 | Generate a password reset change token to email to an existing user.
150 | """
151 | s = Serializer(current_app.config['SECRET_KEY'], expiration)
152 | return s.dumps({'reset': self.id})
153 |
154 | def confirm_account(self, token):
155 | """Verify that the provided token is for this user's id."""
156 | s = Serializer(current_app.config['SECRET_KEY'])
157 | try:
158 | data = s.loads(token)
159 | except (BadSignature, SignatureExpired):
160 | return False
161 | if data.get('confirm') != self.id:
162 | return False
163 | self.confirmed = True
164 | db.session.add(self)
165 | return True
166 |
167 | def change_email(self, token):
168 | """Verify the new email for this user."""
169 | s = Serializer(current_app.config['SECRET_KEY'])
170 | try:
171 | data = s.loads(token)
172 | except (BadSignature, SignatureExpired):
173 | return False
174 | if data.get('change_email') != self.id:
175 | return False
176 | new_email = data.get('new_email')
177 | if new_email is None:
178 | return False
179 | if self.query.filter_by(email=new_email).first() is not None:
180 | return False
181 | self.email = new_email
182 | db.session.add(self)
183 | return True
184 |
185 | def reset_password(self, token, new_password):
186 | """Verify the new password for this user."""
187 | s = Serializer(current_app.config['SECRET_KEY'])
188 | try:
189 | data = s.loads(token)
190 | except (BadSignature, SignatureExpired):
191 | return False
192 | if data.get('reset') != self.id:
193 | return False
194 | self.password = new_password
195 | db.session.add(self)
196 | return True
197 |
198 | @staticmethod
199 | def generate_fake(count=100, **kwargs):
200 | """Generate a number of fake users for testing."""
201 | from sqlalchemy.exc import IntegrityError
202 | from random import choice
203 | from faker import Faker
204 |
205 | fake = Faker()
206 | roles = Role.query.all()
207 |
208 | for i in range(count):
209 | u = User(
210 | first_name=fake.first_name(),
211 | last_name=fake.last_name(),
212 | email=fake.email(),
213 | password=fake.password(),
214 | confirmed=True,
215 | role=choice(roles),
216 | **kwargs
217 | )
218 | db.session.add(u)
219 | try:
220 | db.session.commit()
221 | except IntegrityError:
222 | db.session.rollback()
223 |
224 | @staticmethod
225 | def set_random_zip_codes(users, zip_codes):
226 | """Assign a random ZIPCode from zip_codes to each User in users."""
227 | from random import choice
228 |
229 | for user in users:
230 | user.zip_code = choice(zip_codes)
231 | db.session.add(user)
232 | db.session.commit()
233 |
234 | @staticmethod
235 | def set_random_affiliation_tags(users, affiliation_tags):
236 | """
237 | Assign a random AffiliationTag from affiliation_tags to each User in
238 | users.
239 | """
240 | from random import choice, randint
241 |
242 | for user in users:
243 | for i in range(randint(1, 3)):
244 | user.tags.append(choice(affiliation_tags))
245 | db.session.add(user)
246 | db.session.commit()
247 |
248 | @staticmethod
249 | def create_confirmed_admin(first_name, last_name, email, password,
250 | zip_code):
251 | """Create a confirmed admin with the given input properties."""
252 | from sqlalchemy.exc import IntegrityError
253 |
254 | u = User(
255 | first_name=first_name,
256 | last_name=last_name,
257 | email=email,
258 | password=password,
259 | zip_code=zip_code,
260 | confirmed=True,
261 | role=Role.query.filter_by(
262 | permissions=Permission.ADMINISTER).first()
263 | )
264 | db.session.add(u)
265 | try:
266 | db.session.commit()
267 | except IntegrityError:
268 | db.session.rollback()
269 |
270 | def __repr__(self):
271 | return '' % self.full_name()
272 |
273 |
274 | class AnonymousUser(AnonymousUserMixin):
275 | def can(self, _):
276 | return False
277 |
278 | def is_admin(self):
279 | return False
280 |
281 |
282 | login_manager.anonymous_user = AnonymousUser
283 |
284 |
285 | @login_manager.user_loader
286 | def load_user(user_id):
287 | return User.query.get(int(user_id))
288 |
--------------------------------------------------------------------------------
/app/account/views.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, redirect, request, url_for, flash, abort
2 | from flask.ext.login import (
3 | login_required,
4 | login_user,
5 | logout_user,
6 | current_user
7 | )
8 | from . import account
9 | from .. import db
10 | from ..email import send_email
11 | from ..models import User, ZIPCode, AffiliationTag
12 | from .forms import (
13 | LoginForm,
14 | RegistrationForm,
15 | CreatePasswordForm,
16 | ChangePasswordForm,
17 | ChangeEmailForm,
18 | RequestResetPasswordForm,
19 | ResetPasswordForm,
20 | EditProfileForm,
21 | )
22 |
23 |
24 | @account.route('/login', methods=['GET', 'POST'])
25 | def login():
26 | """Log in an existing user."""
27 | form = LoginForm()
28 | if form.validate_on_submit():
29 | user = User.query.filter_by(email=form.email.data).first()
30 | if user is not None and user.verify_password(form.password.data):
31 | login_user(user, form.remember_me.data)
32 | flash('You are now logged in. Welcome back!', 'success')
33 | return redirect(request.args.get('next') or url_for('main.index'))
34 | else:
35 | flash('Invalid email or password.', 'form-error')
36 | return render_template('account/login.html', form=form)
37 |
38 |
39 | @account.route('/register', methods=['GET', 'POST'])
40 | def register():
41 | """Register a new user, and send them a confirmation email."""
42 | form = RegistrationForm()
43 | if form.validate_on_submit():
44 | zip_code = ZIPCode.create_zip_code(form.zip_code.data)
45 | user = User(first_name=form.first_name.data,
46 | last_name=form.last_name.data,
47 | email=form.email.data,
48 | password=form.password.data,
49 | zip_code_id=zip_code.id)
50 | db.session.add(user)
51 | db.session.commit()
52 | token = user.generate_confirmation_token()
53 | send_email(user.email, 'Confirm Your Account',
54 | 'account/email/confirm', user=user, token=token)
55 | flash('A confirmation link has been sent to {}.'.format(user.email),
56 | 'warning')
57 | return redirect(url_for('main.index'))
58 | return render_template('account/register.html', form=form)
59 |
60 |
61 | @account.route('/logout')
62 | @login_required
63 | def logout():
64 | logout_user()
65 | flash('You have been logged out.', 'info')
66 | return redirect(url_for('main.index'))
67 |
68 |
69 | @account.route('/manage', methods=['GET', 'POST'])
70 | @account.route('/manage/info', methods=['GET', 'POST'])
71 | @login_required
72 | def manage():
73 | """Display a user's account information."""
74 | return render_template('account/manage.html', user=current_user, form=None)
75 |
76 |
77 | @account.route('/reset-password', methods=['GET', 'POST'])
78 | def reset_password_request():
79 | """Respond to existing user's request to reset their password."""
80 | if not current_user.is_anonymous():
81 | return redirect(url_for('main.index'))
82 | form = RequestResetPasswordForm()
83 | if form.validate_on_submit():
84 | user = User.query.filter_by(email=form.email.data).first()
85 | if user:
86 | token = user.generate_password_reset_token()
87 | send_email(user.email,
88 | 'Reset Your Password',
89 | 'account/email/reset_password',
90 | user=user,
91 | token=token,
92 | next=request.args.get('next'))
93 | flash('A password reset link has been sent to {}.'
94 | .format(form.email.data),
95 | 'warning')
96 | return redirect(url_for('account.login'))
97 | return render_template('account/reset_password.html', form=form)
98 |
99 |
100 | @account.route('/reset-password/', methods=['GET', 'POST'])
101 | def reset_password(token):
102 | """Reset an existing user's password."""
103 | if not current_user.is_anonymous():
104 | return redirect(url_for('main.index'))
105 | form = ResetPasswordForm()
106 | if form.validate_on_submit():
107 | user = User.query.filter_by(email=form.email.data).first()
108 | if user is None:
109 | flash('Invalid email address.', 'form-error')
110 | return redirect(url_for('main.index'))
111 | if user.reset_password(token, form.new_password.data):
112 | flash('Your password has been updated.', 'form-success')
113 | return redirect(url_for('account.login'))
114 | else:
115 | flash('The password reset link is invalid or has expired.',
116 | 'form-error')
117 | return redirect(url_for('main.index'))
118 | return render_template('account/reset_password.html', form=form)
119 |
120 |
121 | @account.route('/manage/change-password', methods=['GET', 'POST'])
122 | @login_required
123 | def change_password():
124 | """Change an existing user's password."""
125 | form = ChangePasswordForm()
126 | if form.validate_on_submit():
127 | if current_user.verify_password(form.old_password.data):
128 | current_user.password = form.new_password.data
129 | db.session.add(current_user)
130 | db.session.commit()
131 | flash('Your password has been updated.', 'form-success')
132 | return redirect(url_for('main.index'))
133 | else:
134 | flash('Original password is invalid.', 'form-error')
135 | return render_template('account/manage.html', form=form)
136 |
137 |
138 | @account.route('/manage/change-email', methods=['GET', 'POST'])
139 | @login_required
140 | def change_email_request():
141 | """Respond to existing user's request to change their email."""
142 | form = ChangeEmailForm()
143 | if form.validate_on_submit():
144 | if current_user.verify_password(form.password.data):
145 | new_email = form.email.data
146 | token = current_user.generate_email_change_token(new_email)
147 | send_email(new_email,
148 | 'Confirm Your New Email',
149 | 'account/email/change_email',
150 | user=current_user,
151 | token=token)
152 | flash('A confirmation link has been sent to {}.'.format(new_email),
153 | 'warning')
154 | return redirect(url_for('main.index'))
155 | else:
156 | flash('Invalid email or password.', 'form-error')
157 | return render_template('account/manage.html', form=form)
158 |
159 |
160 | @account.route('/manage/change-email/', methods=['GET', 'POST'])
161 | @login_required
162 | def change_email(token):
163 | """Change existing user's email with provided token."""
164 | if current_user.change_email(token):
165 | flash('Your email address has been updated.', 'success')
166 | else:
167 | flash('The confirmation link is invalid or has expired.', 'error')
168 | return redirect(url_for('main.index'))
169 |
170 |
171 | @account.route('/confirm-account')
172 | @login_required
173 | def confirm_request():
174 | """Respond to new user's request to confirm their account."""
175 | token = current_user.generate_confirmation_token()
176 | send_email(current_user.email, 'Confirm Your Account',
177 | 'account/email/confirm', user=current_user, token=token)
178 | flash('A new confirmation link has been sent to {}.'.
179 | format(current_user.email),
180 | 'warning')
181 | return redirect(url_for('main.index'))
182 |
183 |
184 | @account.route('/confirm-account/')
185 | @login_required
186 | def confirm(token):
187 | """Confirm new user's account with provided token."""
188 | if current_user.confirmed:
189 | return redirect(url_for('main.index'))
190 | if current_user.confirm_account(token):
191 | flash('Your account has been confirmed.', 'success')
192 | else:
193 | flash('The confirmation link is invalid or has expired.', 'error')
194 | return redirect(url_for('main.index'))
195 |
196 |
197 | @account.route('/join-from-invite//',
198 | methods=['GET', 'POST'])
199 | def join_from_invite(user_id, token):
200 | """
201 | Confirm new user's account with provided token and prompt them to set
202 | a password.
203 | """
204 | if current_user is not None and current_user.is_authenticated():
205 | flash('You are already logged in.', 'error')
206 | return redirect(url_for('main.index'))
207 |
208 | new_user = User.query.get(user_id)
209 | if new_user is None:
210 | return redirect(404)
211 |
212 | if new_user.password_hash is not None:
213 | flash('You have already joined.', 'error')
214 | return redirect(url_for('main.index'))
215 |
216 | if new_user.confirm_account(token):
217 | form = CreatePasswordForm()
218 | if form.validate_on_submit():
219 | new_user.password = form.password.data
220 | db.session.add(new_user)
221 | db.session.commit()
222 | flash('Your password has been set. After you log in, you can '
223 | 'go to the "Your Account" page to review your account '
224 | 'information and settings.', 'success')
225 | return redirect(url_for('account.login'))
226 | return render_template('account/join_invite.html', form=form)
227 | else:
228 | flash('The confirmation link is invalid or has expired. Another '
229 | 'invite email with a new link has been sent to you.', 'error')
230 | token = new_user.generate_confirmation_token()
231 | send_email(new_user.email,
232 | 'You Are Invited To Join',
233 | 'account/email/invite',
234 | user=new_user,
235 | user_id=new_user.id,
236 | token=token)
237 | return redirect(url_for('main.index'))
238 |
239 |
240 | @account.before_app_request
241 | def before_request():
242 | """Force user to confirm email before accessing login-required routes."""
243 | if current_user.is_authenticated() \
244 | and not current_user.confirmed \
245 | and request.endpoint[:8] != 'account.' \
246 | and request.endpoint != 'static':
247 | return redirect(url_for('account.unconfirmed'))
248 |
249 |
250 | @account.route('/unconfirmed')
251 | def unconfirmed():
252 | """Catch users with unconfirmed emails."""
253 | if current_user.is_anonymous() or current_user.confirmed:
254 | return redirect(url_for('main.index'))
255 | return render_template('account/unconfirmed.html')
256 |
257 |
258 | @account.route('/profile')
259 | @login_required
260 | def profile_current():
261 | """Display the current logged in User's profile."""
262 | return redirect(url_for('account.profile', user_id=current_user.id))
263 |
264 |
265 | @account.route('/profile/')
266 | @login_required
267 | def profile(user_id):
268 | """Display a user's profile."""
269 | user = User.query.get(user_id)
270 | if user is None:
271 | abort(404)
272 | is_current = user.id == current_user.id
273 | return render_template('account/profile.html', user=user,
274 | is_current=is_current)
275 |
276 |
277 | @account.route('/profile/edit', methods=['GET', 'POST'])
278 | @login_required
279 | def edit_profile():
280 | """User can edit their own profile."""
281 | form = EditProfileForm()
282 | if form.validate_on_submit():
283 | current_user.first_name = form.first_name.data
284 | current_user.last_name = form.last_name.data
285 | current_user.birthday = form.birthday.data
286 | current_user.bio = form.bio.data
287 | # Remove current affiliation tags.
288 | current_user.tags = [tag for tag in current_user.tags
289 | if tag.type != "AffiliationTag"]
290 |
291 | # Add new affiliation tags.
292 | for affiliation_tag_id in form.affiliations.data:
293 | affiliation_tag = AffiliationTag.query.get(affiliation_tag_id)
294 | current_user.tags.append(affiliation_tag)
295 |
296 | db.session.add(current_user)
297 | db.session.commit()
298 | flash('Profile updated', 'success')
299 | return redirect(url_for('account.profile', user_id=current_user.id))
300 | else:
301 | # Populating form with current user profile information.
302 | form.first_name.data = current_user.first_name
303 | form.last_name.data = current_user.last_name
304 | form.birthday.data = current_user.birthday
305 | form.bio.data = current_user.bio
306 | for affiliation_tag in current_user.tags:
307 | if affiliation_tag.type == "AffiliationTag":
308 | form.affiliations.default.append(str(affiliation_tag.id))
309 |
310 | return render_template(
311 | 'account/edit_profile.html',
312 | user=current_user,
313 | form=form,
314 | affiliations=AffiliationTag.query.order_by(AffiliationTag.name).all())
315 |
316 |
317 | @account.route('/donate')
318 | @login_required
319 | def donate():
320 | """Display donate page with PayPal link."""
321 | return render_template('account/donate.html')
322 |
--------------------------------------------------------------------------------