Try the site live| Check out the Wiki | Visit the developers portfolio
6 |
7 | [](https://postimg.cc/z3Rns7w0)
8 |
9 | ## Technologies used in Trashed
10 |
11 | **JavaScript** | **Python** | **SQLAlchemy** | **Flask** | **React** | **Redux**
12 |
13 | **Google Maps API** | **Geocode API** | **HTML** | **CSS** | **Docker** | **Heroku**
14 |
15 | ## Features implemented
16 |
17 | * Users can **log in** or **sign up** to access the site.
18 | * A user has the ability to **report a trashed area** that displays on Google Map.
19 | * A user has the ability to **organize clean up events** for a trashed area.
20 | * The **search** bar can locate using a case insensitive search term and display results to the user.
21 | * A user has the ability to **post tips** to share different ways to reduce waste at home.
22 | * A user can only **edit** an area, event, or tip that they created.
23 | * A user can only **delete** an event or tip that they created.
24 |
25 |
26 | ## Challenges
27 | It was a challenge deciding how to allow users to interact with a map and add markers to the map that didn't require them to know the latitude and longitude of the location they wished to add. After research and looking at documentation, I decided to implement a Geocode API to obtain the latitude and longitude of each created area. Another challenging aspect of this feature was finding a way to use Geocode that would send back the latitude and longitude before the post request was sent to the back end. For my solution, I used two helper functions (one for latitude and one for longitude) that utilized **Geocode.fromAddress()** and passed in an interpolated string that made up the entire address provided by the user. From there I was able to call each helper function and await the results inside of an asynchronous **handleSubmit()** before it was sent to the appropriate thunk.
28 |
29 | [](https://postimg.cc/WhdbWYM1)
30 |
31 | ## Getting started
32 |
33 | 1. Clone the repository
34 |
35 | ```bash
36 | git clone https://github.com/QuintinHull/trashed.git
37 | ```
38 |
39 | 2. Install dependencies
40 |
41 | ```bash
42 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt
43 | ```
44 |
45 | 3. Create a **.env** file based on the example with proper settings for your
46 | development environment
47 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file
48 |
49 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app
50 |
51 | ```bash
52 | pipenv shell
53 | ```
54 |
55 | ```bash
56 | flask db upgrade
57 | ```
58 |
59 | ```bash
60 | flask seed all
61 | ```
62 |
63 | ```bash
64 | flask run
65 | ```
66 |
67 | 6. To run the React App in development, checkout the [README](./react-app/README.md) inside the `react-app` directory.
68 |
69 | ***
70 | *IMPORTANT!*
71 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment.
72 | You can do this by running:
73 |
74 | ```bash
75 | pipenv lock -r > requirements.txt
76 | ```
77 |
78 | *ALSO IMPORTANT!*
79 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux.
80 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for you.
81 | ***
82 |
83 | ## Deploy to Heroku
84 |
85 | 1. Create a new project on Heroku
86 | 2. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres"
87 | 3. Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line)
88 | 4. Run
89 |
90 | ```bash
91 | heroku login
92 | ```
93 |
94 | 5. Login to the heroku container registry
95 |
96 | ```bash
97 | heroku container:login
98 | ```
99 |
100 | 6. Update the `REACT_APP_BASE_URL` variable in the Dockerfile.
101 | This should be the full URL of your Heroku app: i.e. "https://flask-react-aa.herokuapp.com"
102 | 7. Push your docker container to heroku from the root directory of your project.
103 | This will build the dockerfile and push the image to your heroku container registry
104 |
105 | ```bash
106 | heroku container:push web -a {NAME_OF_HEROKU_APP}
107 | ```
108 |
109 | 8. Release your docker container to heroku
110 |
111 | ```bash
112 | heroku container:release web -a {NAME_OF_HEROKU_APP}
113 | ```
114 |
115 | 9. set up your database:
116 |
117 | ```bash
118 | heroku run -a {NAME_OF_HEROKU_APP} flask db upgrade
119 | heroku run -a {NAME_OF_HEROKU_APP} flask seed all
120 | ```
121 |
122 | 10. Under Settings find "Config Vars" and add any additional/secret .env variables.
123 |
124 |
125 | # Thanks for reading my README!
126 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, render_template, request, session, redirect
3 | from flask_cors import CORS
4 | from flask_migrate import Migrate
5 | from flask_wtf.csrf import CSRFProtect, generate_csrf
6 | from flask_login import LoginManager
7 |
8 | from .models import db, User
9 | from .api.user_routes import user_routes
10 | from .api.auth_routes import auth_routes
11 | from .api.area_routes import area_routes
12 | from .api.event_routes import event_routes
13 | from .api.type_routes import type_routes
14 | from .api.item_routes import item_routes
15 |
16 | from .seeds import seed_commands
17 |
18 | from .config import Config
19 |
20 | app = Flask(__name__)
21 |
22 | # Setup login manager
23 | login = LoginManager(app)
24 | login.login_view = 'auth.unauthorized'
25 |
26 |
27 | @login.user_loader
28 | def load_user(id):
29 | return User.query.get(int(id))
30 |
31 |
32 | # Tell flask about our seed commands
33 | app.cli.add_command(seed_commands)
34 |
35 | app.config.from_object(Config)
36 | app.register_blueprint(user_routes, url_prefix='/api/users')
37 | app.register_blueprint(auth_routes, url_prefix='/api/auth')
38 | app.register_blueprint(area_routes, url_prefix='/api/areas')
39 | app.register_blueprint(event_routes, url_prefix='/api/events')
40 | app.register_blueprint(type_routes, url_prefix='/api/types')
41 | app.register_blueprint(item_routes, url_prefix='/api/items')
42 |
43 | db.init_app(app)
44 | Migrate(app, db)
45 |
46 | # Application Security
47 | CORS(app)
48 |
49 | # Since we are deploying with Docker and Flask,
50 | # we won't be using a buildpack when we deploy to Heroku.
51 | # Therefore, we need to make sure that in production any
52 | # request made over http is redirected to https.
53 | # Well.........
54 |
55 | @app.before_request
56 | def https_redirect():
57 | if os.environ.get('FLASK_ENV') == 'production':
58 | if request.headers.get('X-Forwarded-Proto') == 'http':
59 | url = request.url.replace('http://', 'https://', 1)
60 | code = 301
61 | return redirect(url, code=code)
62 |
63 |
64 | @app.after_request
65 | def inject_csrf_token(response):
66 | response.set_cookie('csrf_token',
67 | generate_csrf(),
68 | secure=True if os.environ.get(
69 | 'FLASK_ENV') == 'production' else False,
70 | samesite='Strict' if os.environ.get(
71 | 'FLASK_ENV') == 'production' else None,
72 | httponly=True)
73 | return response
74 |
75 |
76 | @app.route('/', defaults={'path': ''})
77 | @app.route('/')
78 | def react_root(path):
79 | print("path", path)
80 | if path == 'favicon.ico':
81 | return app.send_static_file('favicon.ico')
82 | return app.send_static_file('index.html')
83 |
--------------------------------------------------------------------------------
/app/api/area_routes.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from flask import Blueprint, jsonify, request, Response
3 | from flask_login import current_user
4 | from app.models import db, Area
5 | from app.forms import AreaForm
6 | from sqlalchemy import and_
7 |
8 | area_routes = Blueprint("areas", __name__)
9 |
10 | @area_routes.route("/")
11 | def all_areas():
12 | areas = Area.query.all()
13 | # areas = Area.query.order_by(Area.id)
14 | # areas = Area.query.all().order_by(Area.id)
15 | return {"all_areas": {area.id: area.to_dict() for area in areas}}
16 |
17 | @area_routes.route("/")
18 | def single_area(id):
19 | area = Area.query.get(id)
20 | return {"area": area.to_dict()}
21 |
22 | @area_routes.route("/", methods=["POST"])
23 | def create_area():
24 | form = AreaForm()
25 | form['csrf_token'].data = request.cookies['csrf_token']
26 | if form.validate_on_submit():
27 | area = Area(
28 | address=form.data["address"],
29 | city=form.data['city'],
30 | state=form.data['state'],
31 | zipcode=form.data['zipcode'],
32 | description=form.data['description'],
33 | clean=False,
34 | latitude=form.data['latitude'],
35 | longitude=form.data['longitude'],
36 | created_at=datetime.datetime.now(),
37 | user_id=current_user.id,
38 | )
39 | db.session.add(area)
40 | db.session.commit()
41 | return {"area": area.to_dict()}
42 | return {"errors": "error with area form / post route"}
43 |
44 | @area_routes.route("//edit", methods=["PUT"])
45 | def edit_location(id):
46 | area = Area.query.get(id)
47 |
48 | new_area = request.get_json()
49 |
50 | area.address = new_area["address"]
51 | area.city = new_area["city"]
52 | area.state = new_area["state"]
53 | area.zipcode = new_area["zipcode"]
54 | area.description = new_area["description"]
55 | area.latitude = new_area["latitude"]
56 | area.longitude = new_area["longitude"]
57 |
58 | db.session.commit()
59 | return {"area": area.to_dict()}
60 |
61 |
62 | @area_routes.route('/delete/', methods=["DELETE"])
63 | def delete_area(id):
64 | area = Area.query.get(id)
65 | db.session.delete(area)
66 | db.session.commit()
67 | return {'area': area.to_dict()}
68 |
69 |
70 | @area_routes.route('/search/')
71 | def search_areas(city):
72 | areas = Area.query.filter(Area.city.ilike(f'%{city}%'))
73 | return {"searched_areas": [area.to_dict() for area in areas]}
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/api/auth_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, session, request
2 | from app.models import User, db
3 | from app.forms import LoginForm
4 | from app.forms import SignUpForm
5 | from flask_login import current_user, login_user, logout_user, login_required
6 |
7 | auth_routes = Blueprint('auth', __name__)
8 |
9 |
10 | def validation_errors_to_error_messages(validation_errors):
11 | """
12 | Simple function that turns the WTForms validation errors into a simple list
13 | """
14 | errorMessages = []
15 | for field in validation_errors:
16 | for error in validation_errors[field]:
17 | errorMessages.append(f"{field} : {error}")
18 | return errorMessages
19 |
20 |
21 | @auth_routes.route('/')
22 | def authenticate():
23 | """
24 | Authenticates a user.
25 | """
26 | if current_user.is_authenticated:
27 | return current_user.to_dict()
28 | return {'errors': ['Unauthorized']}, 401
29 |
30 |
31 | @auth_routes.route('/login', methods=['POST'])
32 | def login():
33 | """
34 | Logs a user in
35 | """
36 | form = LoginForm()
37 | print(request.get_json())
38 | # Get the csrf_token from the request cookie and put it into the
39 | # form manually to validate_on_submit can be used
40 | form['csrf_token'].data = request.cookies['csrf_token']
41 | if form.validate_on_submit():
42 | # Add the user to the session, we are logged in!
43 | user = User.query.filter(User.email == form.data['email']).first()
44 | login_user(user)
45 | return user.to_dict()
46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
47 |
48 |
49 | @auth_routes.route('/logout')
50 | def logout():
51 | """
52 | Logs a user out
53 | """
54 | logout_user()
55 | return {'message': 'User logged out'}
56 |
57 |
58 | @auth_routes.route('/signup', methods=['POST'])
59 | def sign_up():
60 | """
61 | Creates a new user and logs them in
62 | """
63 | form = SignUpForm()
64 | form['csrf_token'].data = request.cookies['csrf_token']
65 | if form.validate_on_submit():
66 | user = User(
67 | first_name=form.data['first_name'],
68 | last_name=form.data['last_name'],
69 | email=form.data['email'],
70 | password=form.data['password']
71 | )
72 | db.session.add(user)
73 | db.session.commit()
74 | login_user(user)
75 | return user.to_dict()
76 | return {'errors': validation_errors_to_error_messages(form.errors)}
77 |
78 |
79 | @auth_routes.route('/unauthorized')
80 | def unauthorized():
81 | """
82 | Returns unauthorized JSON when flask-login authentication fails
83 | """
84 | return {'errors': ['Unauthorized']}, 401
85 |
--------------------------------------------------------------------------------
/app/api/event_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request, Response
2 | from flask_login import current_user
3 | from app.models import db, Event
4 | from app.forms import EventForm
5 | from sqlalchemy import and_
6 |
7 | event_routes = Blueprint("events", __name__)
8 |
9 |
10 | @event_routes.route("/")
11 | def all_events():
12 | events = Event.query.all()
13 | return {"all_events": {event.id: event.to_dict() for event in events}}
14 |
15 | @event_routes.route("/area/")
16 | def events_for_area(id):
17 | events = Event.query.filter(Event.area_id == id).all()
18 | return {"all_area_events": {event.id: event.to_dict() for event in events}}
19 |
20 |
21 | @event_routes.route("/")
22 | def single_event(id):
23 | event = Event.query.get(id)
24 | return {"event": event.to_dict()}
25 |
26 |
27 | @event_routes.route("/", methods=["POST"])
28 | def create_event(areaId):
29 | form = EventForm()
30 | print(request.data)
31 |
32 | print("---------------form datetime------->", form.data['date_time'])
33 |
34 | form['csrf_token'].data = request.cookies['csrf_token']
35 | if form.validate_on_submit():
36 | event = Event(
37 | title=form.data['title'],
38 | date_time=form.data['date_time'],
39 | description=form.data['description'],
40 | area_id=areaId,
41 | user_id=current_user.id,
42 | )
43 | print("---event route, event--->", event)
44 | db.session.add(event)
45 | db.session.commit()
46 | return {"event": event.to_dict()}
47 | return {"errors": "error with event form validation"}
48 |
49 |
50 | @event_routes.route("//edit", methods=["PUT"])
51 | def edit_location(id):
52 | event = Event.query.get(id)
53 |
54 | new_event = request.get_json()
55 |
56 | event.title = new_event["title"]
57 | event.date_time = new_event["date_time"]
58 | event.description = new_event["description"]
59 |
60 | db.session.commit()
61 | return {"event": event.to_dict()}
62 |
63 |
64 | @event_routes.route('/delete/', methods=["DELETE"])
65 | def delete_event(id):
66 | event = Event.query.get(id)
67 | db.session.delete(event)
68 | db.session.commit()
69 | return {'event': event.to_dict()}
--------------------------------------------------------------------------------
/app/api/item_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request, Response
2 | from flask_login import current_user
3 | from app.models import db, Item
4 | from app.forms import ItemForm
5 |
6 | item_routes = Blueprint("items", __name__)
7 |
8 | @item_routes.route("/")
9 | def all_items():
10 | items = Item.query.all()
11 | return {"all_items": {item.id: item.to_dict() for item in items}}
12 |
13 | @item_routes.route("/type/")
14 | def items_for_type(id):
15 | items = Item.query.filter(Item.type_id == id).all()
16 | return {"all_type_items": {item.id: item.to_dict() for item in items}}
17 |
18 | @item_routes.route("/")
19 | def single_item(id):
20 | item = Item.query.get(id)
21 | return {"item": item.to_dict()}
22 |
23 |
24 | @item_routes.route("/", methods=["POST"])
25 | def create_item(typeId):
26 | form = ItemForm()
27 | form['csrf_token'].data = request.cookies['csrf_token']
28 | if form.validate_on_submit():
29 | item = Item(
30 | name=form.data['name'],
31 | description=form.data['description'],
32 | user_id=current_user.id,
33 | type_id=typeId,
34 | )
35 | db.session.add(item)
36 | db.session.commit()
37 | return {"item": item.to_dict()}
38 | return {"errors": "error with item form / post route"}
39 |
40 |
41 | @item_routes.route("//edit", methods=["PUT"])
42 | def edit_item(id):
43 | item = Item.query.get(id)
44 |
45 | new_item = request.get_json()
46 |
47 | item.name = new_item["name"]
48 | item.description = new_item["description"]
49 |
50 | db.session.commit()
51 | return {"item": item.to_dict()}
52 |
53 |
54 | @item_routes.route('/delete/', methods=["DELETE"])
55 | def delete_item(id):
56 | item = Item.query.get(id)
57 | db.session.delete(item)
58 | db.session.commit()
59 | return {'item': item.to_dict()}
60 |
--------------------------------------------------------------------------------
/app/api/type_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request, Response
2 | from app.models import db, Type
3 |
4 | type_routes = Blueprint("types", __name__)
5 |
6 | @type_routes.route("/")
7 | def all_types():
8 | types = Type.query.all()
9 | return {"all_types": {one_type.id: one_type.to_dict() for one_type in types}}
10 |
11 |
12 | @type_routes.route("/")
13 | def single_type(id):
14 | one_type = Type.query.get(id)
15 | return {"one_type": one_type.to_dict()}
--------------------------------------------------------------------------------
/app/api/user_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 | from flask_login import login_required
3 | from app.models import User
4 |
5 | user_routes = Blueprint('users', __name__)
6 |
7 |
8 | @user_routes.route('/')
9 | @login_required
10 | def users():
11 | users = User.query.all()
12 | return {"users": [user.to_dict() for user in users]}
13 |
14 |
15 | @user_routes.route('/')
16 | @login_required
17 | def user(id):
18 | user = User.query.get(id)
19 | return user.to_dict()
20 |
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | class Config:
4 | SECRET_KEY=os.environ.get('SECRET_KEY')
5 | SQLALCHEMY_TRACK_MODIFICATIONS=False
6 | SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL')
7 | SQLALCHEMY_ECHO=True
--------------------------------------------------------------------------------
/app/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .login_form import LoginForm
2 | from .signup_form import SignUpForm
3 | from .area_form import AreaForm
4 | from .event_form import EventForm
5 | from .item_form import ItemForm
--------------------------------------------------------------------------------
/app/forms/area_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, TextAreaField, IntegerField, SelectField, FloatField, BooleanField, DateTimeField
3 | from wtforms.validators import DataRequired
4 | from app.models import Area
5 |
6 | state = [("HI"), ("AL"), ("AK"), ("AZ"), ("AR"), ("CA"), ("CO"), ("CT"), ("DE"), ("FL"), ("GA"), ("ID"), ("IL"), ("IN"), ("IA"), ("KS"), ("KY"), ("LA"), ("ME"), ("MD"), ("MA"), ("MI"), ("MN"), ("MS"),
7 | ("MO"), ("MT"), ("NE"), ("NV"), ("NH"), ("NJ"), ("NM"), ("NY"), ("NC"), ("ND"), ("OH"), ("OK"), ("OR"), ("PA"), ("RI"), ("SC"), ("SD"), ("TN"), ("TX"), ("UT"), ("VT"), ("VA"), ("WA"), ("WV"), ("WI"), ("WY")]
8 |
9 |
10 | class AreaForm(FlaskForm):
11 | address = StringField("address", validators=[DataRequired()])
12 | city = StringField("city", validators=[DataRequired()])
13 | state = SelectField("state", choices=state, validators=[DataRequired()])
14 | zipcode = IntegerField("zipcode", validators=[DataRequired()])
15 | description = TextAreaField("description", validators=[DataRequired()])
16 | latitude = FloatField("latitude")
17 | longitude = FloatField("longitude")
--------------------------------------------------------------------------------
/app/forms/event_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, TextAreaField
3 | from wtforms.fields.html5 import DateTimeLocalField
4 | from wtforms.validators import DataRequired
5 | from app.models import Event
6 |
7 | class EventForm(FlaskForm):
8 | title = StringField("title", validators=[DataRequired()])
9 | date_time = DateTimeLocalField("date_time", validators=[DataRequired()], format="%Y-%m-%dT%H:%M")
10 | description = TextAreaField("description", validators=[DataRequired()])
--------------------------------------------------------------------------------
/app/forms/item_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, TextAreaField
3 | from wtforms.validators import DataRequired
4 | from app.models import Item
5 |
6 | class ItemForm(FlaskForm):
7 | name = StringField("name", validators=[DataRequired()])
8 | description = TextAreaField("description", validators=[DataRequired()])
9 |
10 |
--------------------------------------------------------------------------------
/app/forms/login_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exists", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if not user:
12 | raise ValidationError("Email provided not found.")
13 |
14 |
15 | def password_matches(form, field):
16 | print("Checking if password matches")
17 | password = field.data
18 | email = form.data['email']
19 | user = User.query.filter(User.email == email).first()
20 | if not user:
21 | raise ValidationError("No such user exists.")
22 | if not user.check_password(password):
23 | raise ValidationError("Password was incorrect.")
24 |
25 |
26 | class LoginForm(FlaskForm):
27 | email = StringField('email', validators=[DataRequired(), user_exists])
28 | password = StringField('password', validators=[
29 | DataRequired(), password_matches])
30 |
--------------------------------------------------------------------------------
/app/forms/signup_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exits", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if user:
12 | raise ValidationError("User is already registered.")
13 |
14 |
15 | class SignUpForm(FlaskForm):
16 | first_name = StringField('first_name', validators=[DataRequired()])
17 | last_name = StringField('last_name', validators=[DataRequired()])
18 | email = StringField('email', validators=[DataRequired(), user_exists])
19 | password = StringField('password', validators=[DataRequired()])
20 |
--------------------------------------------------------------------------------
/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from .user import User
3 | from .area import Area
4 | from .event import Event
5 | from .item import Item
6 | from .type import Type
--------------------------------------------------------------------------------
/app/models/area.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Area(db.Model):
5 | __tablename__ = 'areas'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | address = db.Column(db.String(50), nullable=False)
9 | city = db.Column(db.String(50), nullable=False)
10 | state = db.Column(db.String(2), nullable=False)
11 | zipcode = db.Column(db.Integer, nullable=False)
12 | description = db.Column(db.String(250), nullable=False)
13 | clean = db.Column(db.Boolean, nullable=False)
14 | latitude = db.Column(db.Float)
15 | longitude = db.Column(db.Float)
16 | created_at = db.Column(db.DateTime, nullable=False)
17 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
18 |
19 | area_user = db.relationship("User", back_populates="user_area")
20 | area_event = db.relationship("Event", cascade="all, delete-orphan", back_populates="event_area")
21 |
22 | def to_dict(self):
23 | return {
24 | "id": self.id,
25 | "address": self.address,
26 | "city": self.city,
27 | "state": self.state,
28 | "zipcode": self.zipcode,
29 | "description": self.description,
30 | "clean": self.clean,
31 | "latitude": self.latitude,
32 | "longitude": self.longitude,
33 | "created_at": self.created_at,
34 | "user_id": self.user_id,
35 | "first_name": self.area_user.first_name,
36 | "last_name": self.area_user.last_name
37 | }
--------------------------------------------------------------------------------
/app/models/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | db = SQLAlchemy()
3 |
--------------------------------------------------------------------------------
/app/models/event.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Event(db.Model):
5 | __tablename__ = 'events'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | title = db.Column(db.String(50), nullable=False)
9 | date_time = db.Column(db.DateTime, nullable=False)
10 | description = db.Column(db.String(250), nullable=False)
11 | area_id = db.Column(db.Integer, db.ForeignKey("areas.id"), nullable=False)
12 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
13 |
14 | event_area = db.relationship("Area", back_populates="area_event")
15 | event_user = db.relationship("User", back_populates="user_event")
16 |
17 | def to_dict(self):
18 | return {
19 | "id": self.id,
20 | "title": self.title,
21 | "date_time": self.date_time,
22 | "description": self.description,
23 | "area_id": self.area_id,
24 | "user_id": self.user_id,
25 | "area_address": self.event_area.address,
26 | "area_state": self.event_area.state,
27 | "first_name": self.event_user.first_name,
28 | "last_name": self.event_user.last_name
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/models/item.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 | class Item(db.Model):
4 | __tablename__ = 'items'
5 |
6 | id = db.Column(db.Integer, primary_key=True)
7 | name = db.Column(db.String(50), nullable=False)
8 | description = db.Column(db.String(250), nullable=False)
9 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
10 | type_id = db.Column(db.Integer, db.ForeignKey("types.id"), nullable=False)
11 |
12 | item_user = db.relationship("User", back_populates="user_item")
13 | item_type = db.relationship("Type", back_populates="type_item")
14 |
15 | def to_dict(self):
16 | return {
17 | "id": self.id,
18 | "name": self.name,
19 | "description": self.description,
20 | "user_id": self.user_id,
21 | "type_id": self.type_id,
22 | "type": self.item_type.name,
23 | "first_name": self.item_user.first_name,
24 | "last_name": self.item_user.last_name,
25 | }
26 |
--------------------------------------------------------------------------------
/app/models/type.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Type(db.Model):
5 | __tablename__ = "types"
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | name = db.Column(db.String(50), nullable=False)
9 |
10 | type_item = db.relationship("Item", back_populates="item_type")
11 |
12 | def to_dict(self):
13 | return {
14 | "id": self.id,
15 | "name": self.name
16 | }
--------------------------------------------------------------------------------
/app/models/user.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 | from flask_login import UserMixin
4 |
5 | class User(db.Model, UserMixin):
6 | __tablename__ = 'users'
7 |
8 | id = db.Column(db.Integer, primary_key = True)
9 | first_name = db.Column(db.String(50), nullable=False)
10 | last_name = db.Column(db.String(50), nullable=False)
11 | email = db.Column(db.String(255), nullable=False, unique=True)
12 | hashed_password = db.Column(db.String(255), nullable=False)
13 |
14 | # relationships here
15 | user_area = db.relationship("Area", back_populates="area_user")
16 | user_event = db.relationship("Event", back_populates="event_user")
17 | user_item = db.relationship("Item", back_populates="item_user")
18 |
19 | @property
20 | def password(self):
21 | return self.hashed_password
22 |
23 |
24 | @password.setter
25 | def password(self, password):
26 | self.hashed_password = generate_password_hash(password)
27 |
28 |
29 | def check_password(self, password):
30 | return check_password_hash(self.password, password)
31 |
32 |
33 | def to_dict(self):
34 | return {
35 | "id": self.id,
36 | "first_name": self.first_name,
37 | "last_name": self.last_name,
38 | "email": self.email
39 | }
40 |
--------------------------------------------------------------------------------
/app/seeds/__init__.py:
--------------------------------------------------------------------------------
1 | from flask.cli import AppGroup
2 | from .users import seed_users, undo_users
3 | from .areas import seed_areas, undo_areas
4 | from .events import seed_events, undo_events
5 | from .types import seed_types, undo_types
6 | from .items import seed_items, undo_items
7 |
8 | # Creates a seed group to hold our commands
9 | # So we can type `flask seed --help`
10 | seed_commands = AppGroup('seed')
11 |
12 | # Creates the `flask seed all` command
13 | @seed_commands.command('all')
14 | def seed():
15 | seed_users()
16 | seed_areas()
17 | seed_events()
18 | seed_types()
19 | seed_items()
20 | # Add other seed functions here
21 |
22 | # Creates the `flask seed undo` command
23 | @seed_commands.command('undo')
24 | def undo():
25 | undo_users()
26 | undo_areas()
27 | undo_events()
28 | undo_types()
29 | undo_items()
30 | # Add other undo functions here
31 |
--------------------------------------------------------------------------------
/app/seeds/areas.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Area
2 |
3 | def seed_areas():
4 |
5 | area_1 = Area(address="White Plains", city="Ewa Beach", state="HI", zipcode="96706", description="White Plains Beach", clean=False, latitude=21.3035,
6 | longitude=-158.0452, created_at="2021-03-03 04:05:06", user_id="1")
7 | area_2 = Area(address="91-1450 Renton Rd", city="Ewa Beach", state="HI", zipcode="96706", description="Asing Community Park", clean=False, latitude=21.3508,
8 | longitude=-158.0253, created_at="2021-03-01 10:15:04", user_id="2")
9 | area_3 = Area(address="2199 Kalia Rd", city="Honolulu", state="HI", zipcode="96815", description="The stretch of Kalia road by Lewers Lounge", clean=False, latitude=21.2779,
10 | longitude=-157.8323, created_at="2021-02-26 12:00:00", user_id="4")
11 | area_4 = Area(address="Lower Saki Mana Rd", city="Waimea", state="HI", zipcode="96796", description="Polihale State Park", clean=False, latitude=22.0795,
12 | longitude=-159.7648, created_at="2021-04-01 07:03:12", user_id="1")
13 | area_5 = Area(address="200 W. Kawili St", city="Hilo", state="HI", zipcode="96720", description="Campus of University of Hawaii at Hilo", clean=False, latitude=19.7018462,
14 | longitude=-155.0791607, created_at="2021-04-01 07:27:44", user_id="3")
15 | area_6 = Area(address="Kahekili Hwy", city="Wailuku", state="HI", zipcode="96793", description="Waihee Ridge Trail", clean=False, latitude=20.9529,
16 | longitude=-156.5316, created_at="2021-04-12 13:45:29", user_id="4")
17 | area_7 = Area(address="6600 Makena Alanui", city="Kihei", state="HI", zipcode="96753", description="Makena Beach", clean=False, latitude=20.6316,
18 | longitude=-156.4448, created_at="2021-04-25 10:31:00", user_id="1")
19 | area_8 = Area(address="Kaahumanu Pl", city="Kailua-Kona", state="HI", zipcode="96740", description="Kailua Pier", clean=False, latitude=19.6400,
20 | longitude=-155.9969, created_at="2021-04-25 14:12:01", user_id="2")
21 | area_9 = Area(address="Mauna Kea Summit Rd", city="Hilo", state="HI", zipcode="96720", description="Lake Waiau Parking Lot", clean=False, latitude=19.810409,
22 | longitude=-155.46768, created_at="2021-04-27 08:28:55", user_id="5")
23 |
24 | db.session.add(area_1)
25 | db.session.add(area_2)
26 | db.session.add(area_3)
27 | db.session.add(area_4)
28 | db.session.add(area_5)
29 | db.session.add(area_6)
30 | db.session.add(area_7)
31 | db.session.add(area_8)
32 | db.session.add(area_9)
33 |
34 | db.session.commit()
35 |
36 | def undo_areas():
37 | Area.query.delete()
38 | db.session.commit()
--------------------------------------------------------------------------------
/app/seeds/events.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Event
2 |
3 | def seed_events():
4 |
5 | event_1 = Event(title="Ka Makana Swim Club", date_time="2021-04-03 08:00:00", description="The Ka Makana Swim Club is getting together to clean up the neighboring beach the first weekend in April",
6 | area_id=1, user_id=2)
7 |
8 | event_2 = Event(title="Family of Five", date_time="2021-04-04 12:00:00", description="My family will be spending the first Sunday in April picking up litter on the beach. Come join us!",
9 | area_id=1, user_id=1)
10 |
11 | event_3 = Event(title="Ewa Makai Middle School", date_time="2021-04-07 09:00:00", description="Our Ewa Makai eighth graders are spending a day in April cleaning up White Plains Beach.",
12 | area_id=1, user_id=5)
13 |
14 | event_4 = Event(title="Fanciscan Vistas Apartments", date_time="2021-04-12 07:15:00", description="Some of us at Franciscan Vistas Ewa Apartments are going to meet up before work and pick up trash at Asing Park. Always need help!",
15 | area_id=2, user_id=3)
16 |
17 | event_5 = Event(title="Service Hours", date_time="2021-04-15 12:00:00", description="I need to get more service hours for school so I'll be picking up trash at the park over lunch.",
18 | area_id=2, user_id=1)
19 |
20 | event_6 = Event(title="Lewers Lounge Staff", date_time="2021-04-27 08:30:00", description="The staff at Lewers Lounge is going to be taking the morning off to clean up our surrounding area. If anyone is interested don't hesitate to join us!",
21 | area_id=3, user_id=1)
22 |
23 | event_7 = Event(title="808 Cleanups", date_time="2021-05-01 09:00:00", description="808 Cleanups is going to be focusing on the Waikiki area for the month and we are going to start with the Int. Market Place area.",
24 | area_id=3, user_id=3)
25 |
26 | event_8 = Event(title="Neighborhood Cleanup", date_time="2021-04-07 08:30:00", description="Our neighborhood is going to devote time to picking up Polihale State Park. We all visit the beach there regularly and take ownership in keeping it clean.",
27 | area_id=4, user_id=4)
28 |
29 | event_9 = Event(title="Parks & Rec", date_time="2021-04-10 10:00:00", description="Kauai Parks & Recreation will be sending a team over to Polihale State Park to remove whatever trash and litter remains. We'd love the help and support of the community!",
30 | area_id=4, user_id=1)
31 |
32 | event_10 = Event(title="Hale 'Alahonua", date_time="2021-04-15 17:00:00", description="Our dorm is going to spend the afternoon picking up trash throughout campus. Anyone is welcome!",
33 | area_id=5, user_id=4)
34 |
35 | event_11 = Event(title="'92 Alumni", date_time="2021-04-20 12:20:00", description="The UH Hilo class of '92 is meeting on campus to take part in litter clean up. Lets go Vulcans!",
36 | area_id=5, user_id=1)
37 |
38 | event_12 = Event(title="Family Clean Up", date_time="2021-04-17 10:00:00", description="Our entire family is coming together to clean up trash along Waihee Ridge Trail. We all frequent the trail and noticed the litter accumulating.",
39 | area_id=6, user_id=1)
40 |
41 | event_13 = Event(title="Just Me", date_time="2021-04-20 15:30:00", description="I walk Waihee Ridge Trail every weekend and I can't stand to see it get covered with litter. I'll be going out to pick up what I can and anyone who wants to join me is welcome.",
42 | area_id=6, user_id=5)
43 |
44 | event_14 = Event(title="Makena Golf & Beach Club", date_time="2021-05-05 13:15:00", description="We noticed a decent amount of trash washed up on shore of Makena Beach and our staff and members are going to spend the afternoon cleaning it up. Come join!",
45 | area_id=7, user_id=2)
46 |
47 | event_15 = Event(title="Marine Ecology", date_time="2021-05-02 12:00:00", description="Our marine ecology class is taking a field trip to the pier to pick up trash in the area and keep it out of the ocean.",
48 | area_id=8, user_id=4)
49 |
50 | event_16 = Event(title="O'okala Fellowship", date_time="2021-04-28 08:00:00", description="Our church members are going to take the initiative to clean up the mess that was left at the Lake Waiau Parking Lot.",
51 | area_id=9, user_id=2)
52 |
53 |
54 | db.session.add(event_1)
55 | db.session.add(event_2)
56 | db.session.add(event_3)
57 | db.session.add(event_4)
58 | db.session.add(event_5)
59 | db.session.add(event_6)
60 | db.session.add(event_7)
61 | db.session.add(event_8)
62 | db.session.add(event_9)
63 | db.session.add(event_10)
64 | db.session.add(event_11)
65 | db.session.add(event_12)
66 | db.session.add(event_13)
67 | db.session.add(event_14)
68 | db.session.add(event_15)
69 | db.session.add(event_16)
70 |
71 | db.session.commit()
72 |
73 | def undo_events():
74 | Event.query.delete()
75 | db.session.commit()
--------------------------------------------------------------------------------
/app/seeds/items.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Item
2 |
3 | def seed_items():
4 | #Bathroom
5 | item_1 = Item(name="Toothpaste", description="Instead of buying a tube of toothpaste every month or so you can shop for plastic free toothpaste as a better alternative. Bite is the brand I use and it gets the job done!",
6 | user_id="4", type_id="3")
7 | item_2 = Item(name="Soap", description="I recently switched back to using soap bars instead of the liquid soap that comes in plastic bottles. Schmidt's is the brand that I currently use and it smells great!",
8 | user_id="1", type_id="3")
9 | item_3 = Item(name="Face pads", description="I finally switched from my single use face pads I use to wash my face and purchased washable face pads that feel great and work just as well! Seek Bamboo is the brand I use.",
10 | user_id="2", type_id="3")
11 | item_4 = Item(name="Q-Tips", description="If you haven't heard...toss the q-tips! They only push the wax further in to your ear and are a needless use of plastic.",
12 | user_id="3", type_id="3")
13 | #Kitchen
14 | item_5 = Item(name="Food Covers", description="I stopped buying ziplock bags because I only use stretchy silicone food lids. They come in many sizes and in square and circlular shapes. Just stretch over your casserole dish or salad bowl and you're set.",
15 | user_id="5", type_id="1")
16 | item_6 = Item(name="Multi-Surface Cleaner", description="I bought the multi-surface starter kit from Blueland and they sent me a reusable spray bottle that I fill on my own and drop in one of their provided tablets.",
17 | user_id="4", type_id="1")
18 | item_7 = Item(name="Reusable Bags", description="Buy reusable grocery bags as well as reusable produce bags for when you go to the grocery store. This may seem simple but its a great place to start!",
19 | user_id="1", type_id="1")
20 | item_8 = Item(name="Milk", description="Oat and/or Soy milk uses significantly less land and water to produce than dairy milk.",
21 | user_id="3", type_id="1")
22 | #Office
23 | item_9 = Item(name="Highlighters", description="You can now find eco friendly wooden highlighters pretty much anywhere online.",
24 | user_id="1", type_id="2")
25 | item_10 = Item(name="Electronic Recycling", description="Electronic stores like Best Buy will actually take your old electronics and recycle them. They typically have drop off boxes at the store front.",
26 | user_id="2", type_id="2")
27 | item_11 = Item(name="Go Digital", description="I noticed a huge decrease in my paper usage after I made a conscious effort to switch to digital files. Google Drive is what I use but there are other options.",
28 | user_id="1", type_id="2")
29 | item_12 = Item(name="Pens", description="You can bring your old pens, highlighters, and mechanical pencils to most office supply stores to recycle them. I take mine to Staples.",
30 | user_id="4", type_id="2")
31 | #Closet
32 | item_13 = Item(name="Old Clothes", description="Some retail stores will offer you credit if you send them old clothes. Marine Layer will give you $5 credit per tee that you send in (up to $25). Of course you can send them more than 5 tee's if you want.",
33 | user_id="3", type_id="4")
34 | item_14 = Item(name="Jeans", description="Save water and stop washing your jeans. You'd be surprised how long you can go before you actually need to wash your jeans. If you're just wearing them occassionally you could go up to 6 months or more before needing them washed.",
35 | user_id="2", type_id="4")
36 | item_15 = Item(name="Micro Plastics", description="Fabrics that release microplastics: Polyester, Nylon, Acrylic, Fleece, Spandex, Acetate. Fabrics that dont: Cotton, Flax, Hemp, Jute, Linen, Ramie, Sisal, Kenaf.",
37 | user_id="1", type_id="4")
38 | item_16 = Item(name="Hangers", description="Recycled fiber board hangers are a nice substitute for plastic hangers, but before you buy them, ask yourself if all the clothes currently on hangers actually need hangers. Ditto Hangers are the brand I use.",
39 | user_id="5", type_id="4")
40 | #Laundry
41 | item_17 = Item(name="Soap Nuts", description="Instead of buying laundry detergent in plastic containers you can use soap nuts (from Soapberry trees) instead. You can reuse soap nuts up to 6 loads of laundry and they are compostable.",
42 | user_id="1", type_id="5")
43 | item_18 = Item(name="Lemons", description="I recently discovered that you can avoid using bleach by soaking stained clothes overnight in one gallon of hot water and a half cup of lemon juice. It gets the stains out, and you can avoid the harsh chemicals.",
44 | user_id="2", type_id="5")
45 | item_19 = Item(name="Dryer Balls", description="Dryer balls retain heat, help clothes stay separated, and reduce drying time by 25%. Wool dryer balls, which are easy to find, can last up to 1,000 loads.",
46 | user_id="3", type_id="5")
47 | item_20 = Item(name="Hang Dry", description="If you dont need youre clothes dry right away, consider hanging them out to dry. Your dryer is more than likely one of the top energy-hungry appliances in your house...and your clothes will last longer!",
48 | user_id="4", type_id="5")
49 | #Other
50 | item_21 = Item(name="Broom", description="When your broom begings to fray and it doesnt sweep like it used to, you can extend its life by simply trimming the frayed ends with a pair of scissors.",
51 | user_id="5", type_id="6")
52 | item_22 = Item(name="Composting", description="Divert waste from the landfill and start composting items like food scraps, coffee grinds, and grass clippings.",
53 | user_id="1", type_id="6")
54 | item_23 = Item(name="Bicycle", description="If can swing it, start commuting on your bike. You'd be surprised the effect it can have on emmissions and fuel consumption if we all rode our bikes. Not to mention you'll be in better shape and save on gas!",
55 | user_id="2", type_id="6")
56 | item_24 = Item(name="Paperless Billing", description="This one might seem simple, but save some trees and enroll in paperless billing.",
57 | user_id="3", type_id="6")
58 |
59 | db.session.add(item_1)
60 | db.session.add(item_2)
61 | db.session.add(item_3)
62 | db.session.add(item_4)
63 | db.session.add(item_5)
64 | db.session.add(item_6)
65 | db.session.add(item_7)
66 | db.session.add(item_8)
67 | db.session.add(item_9)
68 | db.session.add(item_10)
69 | db.session.add(item_11)
70 | db.session.add(item_12)
71 | db.session.add(item_13)
72 | db.session.add(item_14)
73 | db.session.add(item_15)
74 | db.session.add(item_16)
75 | db.session.add(item_17)
76 | db.session.add(item_18)
77 | db.session.add(item_19)
78 | db.session.add(item_20)
79 | db.session.add(item_21)
80 | db.session.add(item_22)
81 | db.session.add(item_23)
82 | db.session.add(item_24)
83 |
84 | db.session.commit()
85 |
86 | def undo_items():
87 | Item.query.delete()
88 | db.session.commit()
89 |
--------------------------------------------------------------------------------
/app/seeds/types.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Type
2 |
3 | def seed_types():
4 |
5 | type_1 = Type(name="Kitchen")
6 | type_2 = Type(name="Office")
7 | type_3 = Type(name="Bathroom")
8 | type_4 = Type(name="Closet")
9 | type_5 = Type(name="Laundry")
10 | type_6 = Type(name="Miscellaneous")
11 |
12 | db.session.add(type_1)
13 | db.session.add(type_2)
14 | db.session.add(type_3)
15 | db.session.add(type_4)
16 | db.session.add(type_5)
17 | db.session.add(type_6)
18 |
19 | db.session.commit()
20 |
21 | def undo_types():
22 | Type.query.delete()
23 | db.session.commit()
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/seeds/users.py:
--------------------------------------------------------------------------------
1 | from werkzeug.security import generate_password_hash
2 | from app.models import db, User
3 |
4 | # Adds a demo user, you can add other users here if you want
5 | def seed_users():
6 |
7 | demo = User(first_name="Demo", last_name="User",
8 | email='demo@aa.io', password='password')
9 |
10 | quintin = User(first_name="Quintin", last_name="Hull",
11 | email="quintinhull92@gmail.com", password='password')
12 |
13 | olivia = User(first_name="Olivia", last_name="Jones",
14 | email="olivia21@gmail.com", password='passOJ21')
15 |
16 | tito = User(first_name="Tito", last_name="Makani",
17 | email="tito@shoreshack.com", password="alohatothat")
18 |
19 | reggie = User(first_name="Reggie", last_name="Smoove",
20 | email="smoovereg@gmail.com", password="smoove24")
21 |
22 | db.session.add(demo)
23 | db.session.add(quintin)
24 | db.session.add(olivia)
25 | db.session.add(tito)
26 | db.session.add(reggie)
27 |
28 | db.session.commit()
29 |
30 | # Uses a raw SQL query to TRUNCATE the users table.
31 | # SQLAlchemy doesn't have a built in function to do this
32 | # TRUNCATE Removes all the data from the table, and resets
33 | # the auto incrementing primary key
34 | def undo_users():
35 | db.session.execute('TRUNCATE users;')
36 | db.session.commit()
37 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary==2.8.6
2 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger('alembic.env')
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 | config.set_main_option(
26 | 'sqlalchemy.url',
27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
28 | target_metadata = current_app.extensions['migrate'].db.metadata
29 |
30 | # other values from the config, defined by the needs of env.py,
31 | # can be acquired:
32 | # my_important_option = config.get_main_option("my_important_option")
33 | # ... etc.
34 |
35 |
36 | def run_migrations_offline():
37 | """Run migrations in 'offline' mode.
38 |
39 | This configures the context with just a URL
40 | and not an Engine, though an Engine is acceptable
41 | here as well. By skipping the Engine creation
42 | we don't even need a DBAPI to be available.
43 |
44 | Calls to context.execute() here emit the given string to the
45 | script output.
46 |
47 | """
48 | url = config.get_main_option("sqlalchemy.url")
49 | context.configure(
50 | url=url, target_metadata=target_metadata, literal_binds=True
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, 'autogenerate', False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info('No changes in schema detected.')
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix='sqlalchemy.',
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | process_revision_directives=process_revision_directives,
86 | **current_app.extensions['migrate'].configure_args
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/migrations/versions/20201120_150602_create_users_table.py:
--------------------------------------------------------------------------------
1 | """create_users_table
2 |
3 | Revision ID: ffdc0a98111c
4 | Revises:
5 | Create Date: 2020-11-20 15:06:02.230689
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'ffdc0a98111c'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('users',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('first_name', sa.String(length=50), nullable=False),
24 | sa.Column('last_name', sa.String(length=50), nullable=False),
25 | sa.Column('email', sa.String(length=255), nullable=False),
26 | sa.Column('hashed_password', sa.String(length=255), nullable=False),
27 | sa.PrimaryKeyConstraint('id'),
28 | sa.UniqueConstraint('email'),
29 | )
30 | # ### end Alembic commands ###qqqqqqqqq
31 |
32 |
33 | def downgrade():
34 | # ### commands auto generated by Alembic - please adjust! ###
35 | op.drop_table('users')
36 | # ### end Alembic commands ###
37 |
--------------------------------------------------------------------------------
/migrations/versions/20210305_134652_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 42421609039e
4 | Revises: ffdc0a98111c
5 | Create Date: 2021-03-05 13:46:52.382327
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '42421609039e'
14 | down_revision = 'ffdc0a98111c'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('types',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('name', sa.String(length=50), nullable=False),
24 | sa.PrimaryKeyConstraint('id')
25 | )
26 | op.create_table('areas',
27 | sa.Column('id', sa.Integer(), nullable=False),
28 | sa.Column('address', sa.String(length=50), nullable=False),
29 | sa.Column('city', sa.String(length=50), nullable=False),
30 | sa.Column('state', sa.String(length=2), nullable=False),
31 | sa.Column('zipcode', sa.Integer(), nullable=False),
32 | sa.Column('description', sa.String(length=250), nullable=False),
33 | sa.Column('clean', sa.Boolean(), nullable=False),
34 | sa.Column('latitude', sa.Float(), nullable=True),
35 | sa.Column('longitude', sa.Float(), nullable=True),
36 | sa.Column('created_at', sa.DateTime(), nullable=False),
37 | sa.Column('user_id', sa.Integer(), nullable=False),
38 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
39 | sa.PrimaryKeyConstraint('id')
40 | )
41 | op.create_table('items',
42 | sa.Column('id', sa.Integer(), nullable=False),
43 | sa.Column('name', sa.String(length=50), nullable=False),
44 | sa.Column('description', sa.String(length=250), nullable=False),
45 | sa.Column('user_id', sa.Integer(), nullable=False),
46 | sa.Column('type_id', sa.Integer(), nullable=False),
47 | sa.ForeignKeyConstraint(['type_id'], ['types.id'], ),
48 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
49 | sa.PrimaryKeyConstraint('id')
50 | )
51 | op.create_table('events',
52 | sa.Column('id', sa.Integer(), nullable=False),
53 | sa.Column('title', sa.String(length=50), nullable=False),
54 | sa.Column('date_time', sa.DateTime(), nullable=False),
55 | sa.Column('description', sa.String(length=250), nullable=False),
56 | sa.Column('area_id', sa.Integer(), nullable=False),
57 | sa.Column('user_id', sa.Integer(), nullable=False),
58 | sa.ForeignKeyConstraint(['area_id'], ['areas.id'], ),
59 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
60 | sa.PrimaryKeyConstraint('id')
61 | )
62 | # ### end Alembic commands ###
63 |
64 |
65 | def downgrade():
66 | # ### commands auto generated by Alembic - please adjust! ###
67 | op.drop_table('events')
68 | op.drop_table('items')
69 | op.drop_table('areas')
70 | op.drop_table('types')
71 | # ### end Alembic commands ###
72 |
--------------------------------------------------------------------------------
/react-app/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL=http://localhost:5000
2 | REACT_APP_GOOGLE_KEY=
3 |
--------------------------------------------------------------------------------
/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/react-app/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | Your React App will live here. While is development, run this application from this location using `npm start`.
4 |
5 |
6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku!
7 |
8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works.
9 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "date-fns": "^2.19.0",
10 | "dotenv": "^8.2.0",
11 | "http-proxy-middleware": "^1.0.5",
12 | "query-string": "^6.14.1",
13 | "react": "^17.0.0",
14 | "react-dom": "^17.0.0",
15 | "react-geocode": "^0.2.3",
16 | "react-google-maps": "^9.4.5",
17 | "react-nice-dates": "^3.0.7",
18 | "react-redux": "^7.2.2",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "3.4.3",
21 | "redux": "^4.0.5",
22 | "redux-logger": "^3.0.6",
23 | "redux-thunk": "^2.3.0"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": "react-app"
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "proxy": "http://localhost:5000"
47 | }
48 |
--------------------------------------------------------------------------------
/react-app/public/beachLitter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/beachLitter.jpg
--------------------------------------------------------------------------------
/react-app/public/carousel_4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/react-app/public/gitHubMark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/gitHubMark.png
--------------------------------------------------------------------------------
/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
10 | TRASHED
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/react-app/public/linkedIn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/linkedIn.png
--------------------------------------------------------------------------------
/react-app/public/logoWithName.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/logoWithName.png
--------------------------------------------------------------------------------
/react-app/public/simpleLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/simpleLogo.png
--------------------------------------------------------------------------------
/react-app/public/trashBin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/trashBin.jpg
--------------------------------------------------------------------------------
/react-app/public/trashed_home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintinHull/trashed/bde412e1f2da17e53f99ba47f6b4d68e9c663ea8/react-app/public/trashed_home.png
--------------------------------------------------------------------------------
/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { BrowserRouter, Route, Switch } from "react-router-dom";
3 | import { Provider as ReduxProvider } from "react-redux";
4 | import LoginForm from "./components/auth/LoginForm";
5 | import SignUpForm from "./components/auth/SignUpForm";
6 | import NavBar from "./components/NavBar";
7 | import ProtectedRoute from "./components/auth/ProtectedRoute";
8 | import UsersList from "./components/UsersList";
9 | import User from "./components/User";
10 | import { authenticate } from "./services/auth";
11 | import configureStore from "./store";
12 |
13 | import HomePage from "./components/HomePage";
14 | import AreaView from "./components/AreaView";
15 | import EventView from "./components/EventView";
16 | import SearchResult from "./components/SearchResult";
17 | import ItemView from "./components/ItemView";
18 | import TypeView from "./components/TypeView";
19 | import SingleItemView from "./components/SingleItemView";
20 | import Footer from "./components/Footer";
21 | import About from "./components/About";
22 | import { ModalProvider } from "./context/Modal"
23 |
24 | const store = configureStore();
25 |
26 | function App() {
27 | const [authenticated, setAuthenticated] = useState(false);
28 | const [loaded, setLoaded] = useState(false);
29 | const [showLogin, setShowLogin] = useState(false);
30 | const [showSignUp, setShowSignUp] = useState(false);
31 |
32 | useEffect(() => {
33 | (async () => {
34 | const user = await authenticate();
35 | if (!user.errors) {
36 | setAuthenticated(true);
37 | }
38 | setLoaded(true);
39 | })();
40 | }, []);
41 |
42 | if (!loaded) {
43 | return null;
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
75 |
76 |
77 |
82 |
83 |
84 |
85 |
91 |
92 |
98 |
99 |
100 |
106 |
107 |
108 |
113 |
114 |
115 |
121 |
122 |
123 |
129 |
130 |
131 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | );
145 | }
146 |
147 | export default App;
148 |
--------------------------------------------------------------------------------
/react-app/src/components/About/About.css:
--------------------------------------------------------------------------------
1 | .about_container {
2 | margin-top: 10px;
3 | }
4 |
5 | .about_row1 {
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: center;
9 | }
10 |
11 | .about_col1 {
12 | background-color: white;
13 | border-radius: 2px;
14 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
15 | padding: 5px;
16 | margin-right: 5px;
17 | }
18 |
19 | .about_col2 {
20 | display: flex;
21 | background-color: white;
22 | border-radius: 2px;
23 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
24 | font-family: "Poppins", sans-serif;
25 | font-size: 4rem;
26 | }
27 |
28 | .about_col2 div {
29 | align-self: center;
30 | margin: 10px;
31 | }
32 |
33 | .about_image {
34 | height: 200px;
35 | padding-top: 3px;
36 | border-radius: 20px;
37 | }
38 |
39 | .about_row2 {
40 | font-size: 1.2rem;
41 | margin: auto;
42 | margin-top: 5px;
43 | display: flex;
44 | /* justify-content: center; */
45 | background-color: white;
46 | border-radius: 2px;
47 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
48 | width: 818px;
49 | padding: 10px;
50 | font-family: "Scope One", serif;
51 | }
52 |
53 | .about_row2 div {
54 | margin-left: 10px;
55 | }
56 |
57 | .about_row2 span {
58 | font-family: "Viga", sans-serif;
59 | font-size: 1.1rem;
60 | }
61 |
62 | .about_row3 {
63 | margin:auto;
64 | margin-top: 5px;
65 | width: 838px;
66 | display: flex;
67 | flex-direction: row;
68 | justify-content: space-between;
69 | }
70 |
71 | .about_row3_col1 {
72 | padding-top: 20px;
73 | font-family: "Poppins", sans-serif;
74 | display: flex;
75 | justify-content: center;
76 | font-size: 1.3rem;
77 | height: 50px;
78 | background-color: white;
79 | border-radius: 2px;
80 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
81 | width: 32.9%;
82 | color: #7fc149;
83 | }
84 |
85 | .about_row3_col2 {
86 | padding-top: 20px;
87 | font-family: "Poppins", sans-serif;
88 | display: flex;
89 | justify-content: center;
90 | font-size: 1.3rem;
91 | height: 50px;
92 | background-color: white;
93 | border-radius: 2px;
94 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
95 | width: 32.9%;
96 | color: #7fc149
97 | }
98 | .about_row3_col3 {
99 | padding-top: 20px;
100 | font-family: "Poppins", sans-serif;
101 | display: flex;
102 | justify-content: center;
103 | font-size: 1.3rem;
104 | height: 50px;
105 | background-color: white;
106 | border-radius: 2px;
107 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
108 | width: 32.9%;
109 | color: #7fc149
110 | }
111 |
112 | .about_row4 {
113 | font-style: italic;
114 | height: 30px;
115 | padding-top: 10px;
116 | font-family: "Scope One", serif;
117 | display: flex;
118 | justify-content: center;
119 | margin: auto;
120 | margin-top: 5px;
121 | width: 838px;
122 | background-color: white;
123 | border-radius: 2px;
124 | box-shadow: rgb(0 0 0 / 30%) 0px 1px 4px -1px;
125 | }
126 |
127 | .aa_span {
128 | color: red;
129 | /* font-family: "Poppins", sans-serif; */
130 | font-size: 1rem;
131 | font-weight: bolder;
132 | }
--------------------------------------------------------------------------------
/react-app/src/components/About/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import "./About.css"
4 |
5 | const About = () => {
6 | const imagePath = process.env.NODE_ENV === "production" ? "/static" : "";
7 |
8 | return (
9 |
10 |
11 |
12 | {}
13 |
14 |
15 |
welcome to trashed
16 |
17 |
18 |
19 |
report waste in your area
20 |
organize clean up events
21 |
reduce waste from home
22 |
23 |
24 |
TRASHED is an application designed for users
25 | interested in keeping their communities clean and beautiful!
26 | Whether you want to get out and get your hands dirty, or
27 | create change from the comfort of your home, TRASHED
28 | can be a helpful tool for anyone.
29 |
30 |
Built and designed by Quintin Hull as his capstone
31 | at App Academy.