├── 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 |

403

5 |

Forbidden

6 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% block content %} 4 |

404

5 |

Page not found

6 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% block content %} 4 |

500

5 |

Internal Server Error

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 |

Create your password

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 |

Reset your password

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 |

7 | Donate Now to Support Women Veterans Rock! 8 |

9 |
Click the Donate button to make a donation to Women Veterans Rock!
10 |
11 |
12 | 13 | 14 | 15 |
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 |
5 |
6 |

User Map

7 |
8 |
9 |
10 |
11 |
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 |

Your Review

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 |
32 |

FAQ here

33 |
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 |

7 | {% if icon is not none %} 8 | 9 | {% endif %} 10 |
11 | {{ title }} 12 | {% if description is not none %} 13 |
14 | {{ description }} 15 |
16 | {% endif %} 17 |
18 |

19 |
20 |
21 | {% endmacro %} 22 | 23 | {% block content %} 24 |
25 |
26 |

27 | Admin Dashboard 28 |

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 |

Log in

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 |
22 | Forgot password? 23 |
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 |

Create an account

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 | 25 | 26 | 27 |
Full name{{ '%s %s' % (user.first_name, user.last_name) }}
Email address{{ user.email }}
Account type{{ user.role.name }}
28 | {% endmacro %} 29 | 30 | {% block content %} 31 |
32 |
33 |

34 | Account Settings 35 |
Manage your account settings and change your login information.
36 |

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 |

15 | Edit Profile 16 |

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 |

{{ user.full_name() }}

8 |
9 |
10 | {% if is_current %} 11 |
12 | 13 | 14 | 15 |
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 |

15 | Add New User 16 |
Create a new user account
17 |

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 |
5 |
6 |

Resources Map

7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | Leave blank if searching for any type of resource. 19 |
20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
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 [![Circle CI](https://circleci.com/gh/hack4impact/women-veterans-rock.svg?style=svg)](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 | 27 | 28 | 29 |
Full name{{ '%s %s' % (user.first_name, user.last_name) }}
Email address{{ user.email }}
Account type{{ user.role.name }}
30 | {% endmacro %} 31 | 32 | {% block content %} 33 |
34 |
35 | 36 | 37 | Back to all users 38 | 39 |

40 | {{ user.full_name() }} 41 |
View and manage {{ user.first_name }}’s account.
42 |

43 |
44 |
45 |
46 | {{ navigation(endpoints) }} 47 |
48 |
49 | {% if request.endpoint == deletion_endpoint %} 50 |

51 | 52 |
53 | This action is permanent 54 |
55 | Deleting a user account is not a reversible change. Any information associated 56 | with this account will be removed, and cannot be recovered. 57 |
58 |
59 |

60 | 61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | Delete this user 70 | 71 |
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 –
method attribute (default 'POST') 9 | extra_classes – The classes to add to the . 10 | enctype – enctype attribute. If None, will automatically be set to 11 | multipart/form-data if a FileField is present in the form. #} 12 | {% macro render_form(form, method='POST', extra_classes='', enctype=None) %} 13 | {% set flashes = { 14 | 'error': get_flashed_messages(category_filter=['form-error']), 15 | 'warning': get_flashed_messages(category_filter=['form-check-email']), 16 | 'info': get_flashed_messages(category_filter=['form-info']), 17 | 'success': get_flashed_messages(category_filter=['form-success']) 18 | } %} 19 | 20 | {{ begin_form(form, flashes, method=method, extra_classes=extra_classes, enctype=enctype) }} 21 | {% for field in form if not (is_hidden_field(field) or field.type == 'SubmitField') %} 22 | {{ render_form_field(field) }} 23 | {% endfor %} 24 | 25 | {{ form_message(flashes['error'], header='Something went wrong.', class='error') }} 26 | {{ form_message(flashes['warning'], header='Check your email.', class='warning') }} 27 | {{ form_message(flashes['info'], header='Information', class='info') }} 28 | {{ form_message(flashes['success'], header='Success!', class='success') }} 29 | 30 | {% for field in form | selectattr('type', 'equalto', 'SubmitField') %} 31 | {{ render_form_field(field) }} 32 | {% endfor %} 33 | {{ end_form(form) }} 34 | {% endmacro %} 35 | 36 | {# Set up the form, including hidden fields and error states #} 37 | {% macro begin_form(form, flashes, action='', method='POST', extra_classes='', enctype=None) %} 38 | {# Set proper enctype #} 39 | {% if enctype is none and (form | selectattr('type', 'equalto', 'FileField') | list | length > 0) %} 40 | {% set enctype = 'multipart/form-data' %} 41 | {% else %} 42 | {% set enctype = '' %} 43 | {% endif %} 44 | 45 | 51 | {{ form.hidden_tag() }} 52 | {% endmacro %} 53 | 54 | {# Mirrors begin_form #} 55 | {% macro end_form(form) %} 56 |
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 |
{{ header }}
65 | {% endif %} 66 | {% if messages %} 67 | 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 |
83 | {{ render_form_input(field) }} 84 | {% if field.errors %} 85 |
86 | {{ field.errors[0] | safe }} 87 |
88 | {% endif %} 89 |
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 |

Add a resource

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 | {{ config.APP_NAME }}! 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 | 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 | 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 | 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 |

10 | Registered Users 11 |
12 | View and manage currently registered users. 13 |
14 |

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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% for u in users | sort(attribute='last_name') %} 50 | 51 | 52 | 53 | 54 | 66 | 67 | {% endfor %} 68 | 69 |
Full NameEmail addressAccount typeDonor Level
{{ u.full_name() }}{{ u.email }}{{ u.role.name }} 55 | 65 |
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 | 63 | 64 |

Resource

65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Name{{ resource.name }}
Description{{ resource.description }}
Website{{ resource.website }}
Address{{ resource.address.street_address }}
User{{ resource.user.full_name() }}
87 | 88 | 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 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
Rating{{ review.rating }}
Content{{ review.content }}
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 | --------------------------------------------------------------------------------