diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b372415e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +__pycache__ +env + +node_modules \ No newline at end of file diff --git a/app.py b/app.py index ed56020d7..de8dc597c 100644 --- a/app.py +++ b/app.py @@ -2,72 +2,38 @@ # Imports #----------------------------------------------------------------------------# -import json +import sys import dateutil.parser import babel -from flask import Flask, render_template, request, Response, flash, redirect, url_for +from flask import Flask, render_template, request, flash, redirect, url_for from flask_moment import Moment -from flask_sqlalchemy import SQLAlchemy import logging from logging import Formatter, FileHandler -from flask_wtf import Form -from forms import * +from forms import VenueForm, ArtistForm, ShowForm +from datetime import datetime +from models import Venue, Artist, Show, setup_db +from constants import HOME_PAGE_TEMPLATE, NEW_VENUE_TEMPLATE, ARTISTS_PAGE_TEMPLATE, \ + SEARCH_ARTISTS_TEMPLATE, SHOW_ARTIST_TEMPLATE, SHOWS_TEMPLATE, SHOW_VENUE_TEMPLATE, ERROR_404, \ + ERROR_500, VENUES_TEMPLATE, SEARCH_VENUES_TEMPLATE, EDIT_VENUE_TEMPLATE, EDIT_ARTIST_TEMPLATE, NEW_ARTIST_TEMPLATE, NEW_SHOW_TEMPLATE #----------------------------------------------------------------------------# # App Config. #----------------------------------------------------------------------------# app = Flask(__name__) moment = Moment(app) -app.config.from_object('config') -db = SQLAlchemy(app) - -# TODO: connect to a local postgresql database - -#----------------------------------------------------------------------------# -# Models. -#----------------------------------------------------------------------------# - -class Venue(db.Model): - __tablename__ = 'Venue' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String) - city = db.Column(db.String(120)) - state = db.Column(db.String(120)) - address = db.Column(db.String(120)) - phone = db.Column(db.String(120)) - image_link = db.Column(db.String(500)) - facebook_link = db.Column(db.String(120)) - - # TODO: implement any missing fields, as a database migration using Flask-Migrate - -class Artist(db.Model): - __tablename__ = 'Artist' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String) - city = db.Column(db.String(120)) - state = db.Column(db.String(120)) - phone = db.Column(db.String(120)) - genres = db.Column(db.String(120)) - image_link = db.Column(db.String(500)) - facebook_link = db.Column(db.String(120)) - - # TODO: implement any missing fields, as a database migration using Flask-Migrate - -# TODO Implement Show and Artist models, and complete all model relationships and properties, as a database migration. +db = setup_db(app) #----------------------------------------------------------------------------# # Filters. #----------------------------------------------------------------------------# -def format_datetime(value, format='medium'): +def format_datetime(value, date_format='medium'): date = dateutil.parser.parse(value) - if format == 'full': - format="EEEE MMMM, d, y 'at' h:mma" - elif format == 'medium': - format="EE MM, dd, y h:mma" - return babel.dates.format_datetime(date, format, locale='en') + if date_format == 'full': + date_format="EEEE MMMM, d, y 'at' h:mma" + elif date_format == 'medium': + date_format="EE MM, dd, y h:mma" + return babel.dates.format_datetime(date, date_format, locale='en') app.jinja_env.filters['datetime'] = format_datetime @@ -77,7 +43,7 @@ def format_datetime(value, format='medium'): @app.route('/') def index(): - return render_template('pages/home.html') + return render_template(HOME_PAGE_TEMPLATE) # Venues @@ -85,129 +51,107 @@ def index(): @app.route('/venues') def venues(): - # TODO: replace with real venues data. - # num_upcoming_shows should be aggregated based on number of upcoming shows per venue. - data=[{ - "city": "San Francisco", - "state": "CA", - "venues": [{ - "id": 1, - "name": "The Musical Hop", - "num_upcoming_shows": 0, - }, { - "id": 3, - "name": "Park Square Live Music & Coffee", - "num_upcoming_shows": 1, - }] - }, { - "city": "New York", - "state": "NY", - "venues": [{ - "id": 2, - "name": "The Dueling Pianos Bar", - "num_upcoming_shows": 0, - }] - }] - return render_template('pages/venues.html', areas=data); + data_areas = [] + locations = Venue.query.with_entities(Venue.city, Venue.state)\ + .group_by(Venue.city, Venue.state).all() + + for location in locations: + venue_items = [] + venues = Venue.query.filter_by(city=location.city).filter_by(state=location.state).all() + + for venue in venues: + upcoming_shows = db.session \ + .query(Show) \ + .filter(Show.venue_id == venue.id, Show.start_time > datetime.now()) \ + .all() + + venue_items.append({ + 'id': venue.id, + 'name': venue.name, + 'num_upcoming_shows': upcoming_shows.count + }) + + data_areas.append({ + 'city': location.city, + 'state': location.state, + 'venues': venue_items + }) + + return render_template(VENUES_TEMPLATE, areas=data_areas); @app.route('/venues/search', methods=['POST']) def search_venues(): - # TODO: implement search on artists with partial string search. Ensure it is case-insensitive. - # seach for Hop should return "The Musical Hop". - # search for "Music" should return "The Musical Hop" and "Park Square Live Music & Coffee" - response={ - "count": 1, - "data": [{ - "id": 2, - "name": "The Dueling Pianos Bar", - "num_upcoming_shows": 0, - }] + search_term = request.form.get('search_term', '') + venues = Venue.query.with_entities(Venue.id, Venue.name)\ + .filter(Venue.name.ilike(f'%{search_term}%')) + + data_venues = [] + for venue in venues: + upcoming_shows = db.session \ + .query(Show) \ + .filter(Show.venue_id == venue.id) \ + .filter(Show.start_time > datetime.now()) \ + .all() + + data_venues.append({ + 'id': venue.id, + 'name': venue.name, + 'num_upcoming_shows': upcoming_shows.count + }) + + results = { + "count": venues.count(), + "data": data_venues } - return render_template('pages/search_venues.html', results=response, search_term=request.form.get('search_term', '')) + return render_template(SEARCH_VENUES_TEMPLATE, results=results, search_term=search_term) @app.route('/venues/') def show_venue(venue_id): - # shows the venue page with the given venue_id - # TODO: replace with real venue data from the venues table, using venue_id - data1={ - "id": 1, - "name": "The Musical Hop", - "genres": ["Jazz", "Reggae", "Swing", "Classical", "Folk"], - "address": "1015 Folsom Street", - "city": "San Francisco", - "state": "CA", - "phone": "123-123-1234", - "website": "https://www.themusicalhop.com", - "facebook_link": "https://www.facebook.com/TheMusicalHop", - "seeking_talent": True, - "seeking_description": "We are on the lookout for a local artist to play every two weeks. Please call us.", - "image_link": "https://images.unsplash.com/photo-1543900694-133f37abaaa5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=60", - "past_shows": [{ - "artist_id": 4, - "artist_name": "Guns N Petals", - "artist_image_link": "https://images.unsplash.com/photo-1549213783-8284d0336c4f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", - "start_time": "2019-05-21T21:30:00.000Z" - }], - "upcoming_shows": [], - "past_shows_count": 1, - "upcoming_shows_count": 0, - } - data2={ - "id": 2, - "name": "The Dueling Pianos Bar", - "genres": ["Classical", "R&B", "Hip-Hop"], - "address": "335 Delancey Street", - "city": "New York", - "state": "NY", - "phone": "914-003-1132", - "website": "https://www.theduelingpianos.com", - "facebook_link": "https://www.facebook.com/theduelingpianos", - "seeking_talent": False, - "image_link": "https://images.unsplash.com/photo-1497032205916-ac775f0649ae?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80", - "past_shows": [], - "upcoming_shows": [], - "past_shows_count": 0, - "upcoming_shows_count": 0, - } - data3={ - "id": 3, - "name": "Park Square Live Music & Coffee", - "genres": ["Rock n Roll", "Jazz", "Classical", "Folk"], - "address": "34 Whiskey Moore Ave", - "city": "San Francisco", - "state": "CA", - "phone": "415-000-1234", - "website": "https://www.parksquarelivemusicandcoffee.com", - "facebook_link": "https://www.facebook.com/ParkSquareLiveMusicAndCoffee", - "seeking_talent": False, - "image_link": "https://images.unsplash.com/photo-1485686531765-ba63b07845a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=747&q=80", - "past_shows": [{ - "artist_id": 5, - "artist_name": "Matt Quevedo", - "artist_image_link": "https://images.unsplash.com/photo-1495223153807-b916f75de8c5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80", - "start_time": "2019-06-15T23:00:00.000Z" - }], - "upcoming_shows": [{ - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-01T20:00:00.000Z" - }, { - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-08T20:00:00.000Z" - }, { - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-15T20:00:00.000Z" - }], - "past_shows_count": 1, - "upcoming_shows_count": 1, - } - data = list(filter(lambda d: d['id'] == venue_id, [data1, data2, data3]))[0] - return render_template('pages/show_venue.html', venue=data) + data_venue = db.session.get(Venue, venue_id) + + upcoming_shows = Show.query \ + .filter(Show.venue_id == venue_id) \ + .filter(Show.start_time > datetime.now()) \ + .all() + + if len(upcoming_shows) > 0: + data_upcoming_shows = [] + + for upcoming_show in upcoming_shows: + artist = db.session.get(Artist, upcoming_show.artist_id) + + data_upcoming_shows.append({ + 'artist_id': artist.id, + 'artist_name': artist.name, + 'artist_image_link': artist.image_link, + 'start_time': str(upcoming_show.start_time), + }) + + data_venue.upcoming_shows = data_upcoming_shows + data_venue.upcoming_shows_count = data_upcoming_shows.count + + past_shows = Show.query \ + .filter(Show.venue_id == venue_id, Show.start_time < datetime.now()) \ + .all() + + if len(past_shows) > 0: + data_past_shows = [] + + for past_show in past_shows: + artist = db.session.get(Artist, past_show.artist_id) + + data_past_shows.append({ + 'artist_id': artist.id, + 'artist_name': artist.name, + 'artist_image_link': artist.image_link, + 'start_time': str(past_show.start_time), + }) + + # Add shows data + data_venue.past_shows = data_past_shows + data_venue.past_shows_count = data_past_shows.count + + return render_template(SHOW_VENUE_TEMPLATE, venue=data_venue) # Create Venue # ---------------------------------------------------------------- @@ -215,191 +159,250 @@ def show_venue(venue_id): @app.route('/venues/create', methods=['GET']) def create_venue_form(): form = VenueForm() - return render_template('forms/new_venue.html', form=form) + return render_template(NEW_VENUE_TEMPLATE, form=form) @app.route('/venues/create', methods=['POST']) def create_venue_submission(): - # TODO: insert form data as a new Venue record in the db, instead - # TODO: modify data to be the data object returned from db insertion - - # on successful db insert, flash success - flash('Venue ' + request.form['name'] + ' was successfully listed!') - # TODO: on unsuccessful db insert, flash an error instead. - # e.g., flash('An error occurred. Venue ' + data.name + ' could not be listed.') - # see: http://flask.pocoo.org/docs/1.0/patterns/flashing/ - return render_template('pages/home.html') + error = False + try: + venue = Venue(name = request.form['name'], + city = request.form['city'], + state = request.form['state'], + address = request.form['address'], + phone = request.form['phone'], + genres = request.form.getlist('genres'), + image_link = request.form['image_link'], + facebook_link = request.form['facebook_link'], + website_link = request.form['website_link'], + seeking_talent = True if 'seeking_talent' in request.form else False, + seeking_description = request.form['seeking_description']) + + db.session.add(venue) + db.session.commit() + except Exception: + error = True + db.session.rollback() + print(sys.exc_info()) + finally: + db.session.close() + + if error: + flash('An error occurred. Venue ' + request.form['name']+ ' could not be listed.', 'danger ') + else: + flash('Venue ' + request.form['name'] + ' was successfully listed!', 'success') + return render_template(HOME_PAGE_TEMPLATE) @app.route('/venues/', methods=['DELETE']) def delete_venue(venue_id): - # TODO: Complete this endpoint for taking a venue_id, and using - # SQLAlchemy ORM to delete a record. Handle cases where the session commit could fail. - - # BONUS CHALLENGE: Implement a button to delete a Venue on a Venue Page, have it so that - # clicking that button delete it from the db then redirect the user to the homepage - return None + error = False + try: + Venue.query.filter_by(id=venue_id).delete() + db.session.commit() + except Exception as e: + db.session.rollback() + print(sys.exc_info()) + print(e) + return render_template(ERROR_500, error=str(e)) + finally: + db.session.close() + + if error: + flash('An error occurred. Venue could not be deleted.', 'danger') + if not error: + flash('Venue was successfully deleted!', 'success') + + return redirect(url_for('home')) # Artists # ---------------------------------------------------------------- @app.route('/artists') def artists(): - # TODO: replace with real data returned from querying the database - data=[{ - "id": 4, - "name": "Guns N Petals", - }, { - "id": 5, - "name": "Matt Quevedo", - }, { - "id": 6, - "name": "The Wild Sax Band", - }] - return render_template('pages/artists.html', artists=data) + data_artists = [] + + artists = Artist.query \ + .with_entities(Artist.id, Artist.name) \ + .order_by('id') \ + .all() + + for artist in artists: + data_artists.append({ + 'id': artist.id, + 'name': artist.name, + }) + + return render_template(ARTISTS_PAGE_TEMPLATE, artists=data_artists) @app.route('/artists/search', methods=['POST']) def search_artists(): - # TODO: implement search on artists with partial string search. Ensure it is case-insensitive. - # seach for "A" should return "Guns N Petals", "Matt Quevado", and "The Wild Sax Band". - # search for "band" should return "The Wild Sax Band". - response={ - "count": 1, - "data": [{ - "id": 4, - "name": "Guns N Petals", - "num_upcoming_shows": 0, - }] + search_term = request.form.get('search_term', '') + + artists = Artist.query.with_entities(Artist.id, Artist.name)\ + .filter(Artist.name.ilike(f'%{search_term}%')) + + data_artists = [] + for artist in artists: + upcoming_shows = db.session.query(Show)\ + .filter(Show.artist_id == artist.id, Show.start_time > datetime.now())\ + .all() + + data_artists.append({ + 'id': artist.id, + 'name': artist.name, + 'num_upcoming_shows': upcoming_shows.count + }) + + response = { + "count": artists.count(), + "data": data_artists } - return render_template('pages/search_artists.html', results=response, search_term=request.form.get('search_term', '')) + return render_template(SEARCH_ARTISTS_TEMPLATE, results=response, search_term=search_term) @app.route('/artists/') def show_artist(artist_id): - # shows the artist page with the given artist_id - # TODO: replace with real artist data from the artist table, using artist_id - data1={ - "id": 4, - "name": "Guns N Petals", - "genres": ["Rock n Roll"], - "city": "San Francisco", - "state": "CA", - "phone": "326-123-5000", - "website": "https://www.gunsnpetalsband.com", - "facebook_link": "https://www.facebook.com/GunsNPetals", - "seeking_venue": True, - "seeking_description": "Looking for shows to perform at in the San Francisco Bay Area!", - "image_link": "https://images.unsplash.com/photo-1549213783-8284d0336c4f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", - "past_shows": [{ - "venue_id": 1, - "venue_name": "The Musical Hop", - "venue_image_link": "https://images.unsplash.com/photo-1543900694-133f37abaaa5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=60", - "start_time": "2019-05-21T21:30:00.000Z" - }], - "upcoming_shows": [], - "past_shows_count": 1, - "upcoming_shows_count": 0, - } - data2={ - "id": 5, - "name": "Matt Quevedo", - "genres": ["Jazz"], - "city": "New York", - "state": "NY", - "phone": "300-400-5000", - "facebook_link": "https://www.facebook.com/mattquevedo923251523", - "seeking_venue": False, - "image_link": "https://images.unsplash.com/photo-1495223153807-b916f75de8c5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80", - "past_shows": [{ - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "venue_image_link": "https://images.unsplash.com/photo-1485686531765-ba63b07845a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=747&q=80", - "start_time": "2019-06-15T23:00:00.000Z" - }], - "upcoming_shows": [], - "past_shows_count": 1, - "upcoming_shows_count": 0, - } - data3={ - "id": 6, - "name": "The Wild Sax Band", - "genres": ["Jazz", "Classical"], - "city": "San Francisco", - "state": "CA", - "phone": "432-325-5432", - "seeking_venue": False, - "image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "past_shows": [], - "upcoming_shows": [{ - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "venue_image_link": "https://images.unsplash.com/photo-1485686531765-ba63b07845a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=747&q=80", - "start_time": "2035-04-01T20:00:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "venue_image_link": "https://images.unsplash.com/photo-1485686531765-ba63b07845a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=747&q=80", - "start_time": "2035-04-08T20:00:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "venue_image_link": "https://images.unsplash.com/photo-1485686531765-ba63b07845a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=747&q=80", - "start_time": "2035-04-15T20:00:00.000Z" - }], - "past_shows_count": 0, - "upcoming_shows_count": 3, - } - data = list(filter(lambda d: d['id'] == artist_id, [data1, data2, data3]))[0] - return render_template('pages/show_artist.html', artist=data) + data_artist = db.session.get(Artist, artist_id) + + upcoming_shows = Show.query \ + .filter(Show.artist_id == artist_id, Show.start_time > datetime.now()) \ + .all() + + if len(upcoming_shows) > 0: + data_upcoming_shows = [] + + for upcoming_show in upcoming_shows: + venue = db.session.get(Venue, upcoming_show.venue_id) + + data_upcoming_shows.append({ + 'venue_id': venue.id, + 'venue_name': venue.name, + 'venue_image_link': venue.image_link, + 'start_time': str(upcoming_show.start_time), + }) + + data_artist.upcoming_shows = data_upcoming_shows + data_artist.upcoming_shows_count = data_upcoming_shows.count + + past_shows = Show.query \ + .filter(Show.artist_id == artist_id) \ + .filter(Show.start_time < datetime.now()) \ + .all() + + if len(past_shows) > 0: + data_past_shows = [] + + for past_show in past_shows: + venue = db.session.get(Venue, past_show.venue_id) + + data_past_shows.append({ + 'venue_id': venue.id, + 'venue_name': venue.name, + 'venue_image_link': venue.image_link, + 'start_time': str(past_show.start_time), + }) + + data_artist.past_shows = data_past_shows + data_artist.past_shows_count = data_past_shows.count + + return render_template(SHOW_ARTIST_TEMPLATE, artist=data_artist) # Update # ---------------------------------------------------------------- @app.route('/artists//edit', methods=['GET']) def edit_artist(artist_id): + artist = db.session.get(Artist, artist_id) form = ArtistForm() - artist={ - "id": 4, - "name": "Guns N Petals", - "genres": ["Rock n Roll"], - "city": "San Francisco", - "state": "CA", - "phone": "326-123-5000", - "website": "https://www.gunsnpetalsband.com", - "facebook_link": "https://www.facebook.com/GunsNPetals", - "seeking_venue": True, - "seeking_description": "Looking for shows to perform at in the San Francisco Bay Area!", - "image_link": "https://images.unsplash.com/photo-1549213783-8284d0336c4f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80" - } - # TODO: populate form with fields from artist with ID - return render_template('forms/edit_artist.html', form=form, artist=artist) + form.name.data = artist.name + form.city.data = artist.city + form.state.data = artist.state + form.phone.data = artist.phone + form.genres.data = artist.genres + form.image_link.data = artist.image_link + form.facebook_link.data = artist.facebook_link + form.website_link.data = artist.website_link + form.seeking_venue.data = artist.seeking_venue + form.seeking_description.data = artist.seeking_description + + return render_template(EDIT_ARTIST_TEMPLATE, form=form, artist=artist) @app.route('/artists//edit', methods=['POST']) def edit_artist_submission(artist_id): - # TODO: take values from the form submitted, and update existing - # artist record with ID using the new attributes + try: + artist = db.session.get(Artist, artist_id) + artist.name = request.form['name'] + artist.city = request.form['city'] + artist.state = request.form['state'] + artist.phone = request.form['phone'] + artist.genres = request.form.getlist('genres') + artist.image_link = request.form['image_link'] + artist.facebook_link = request.form['facebook_link'] + artist.website_link = request.form['website_link'] + artist.seeking_venue = True if 'seeking_venue' in request.form else False + artist.seeking_description = request.form['seeking_description'] + + db.session.commit() + except Exception: + db.session.rollback() + print(sys.exc_info()) + finally: + db.session.close() return redirect(url_for('show_artist', artist_id=artist_id)) @app.route('/venues//edit', methods=['GET']) def edit_venue(venue_id): + venue = db.session.get(Venue, venue_id) + form = VenueForm() - venue={ - "id": 1, - "name": "The Musical Hop", - "genres": ["Jazz", "Reggae", "Swing", "Classical", "Folk"], - "address": "1015 Folsom Street", - "city": "San Francisco", - "state": "CA", - "phone": "123-123-1234", - "website": "https://www.themusicalhop.com", - "facebook_link": "https://www.facebook.com/TheMusicalHop", - "seeking_talent": True, - "seeking_description": "We are on the lookout for a local artist to play every two weeks. Please call us.", - "image_link": "https://images.unsplash.com/photo-1543900694-133f37abaaa5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=60" - } - # TODO: populate form with values from venue with ID - return render_template('forms/edit_venue.html', form=form, venue=venue) + form.name.data = venue.name + form.city.data = venue.city + form.state.data = venue.state + form.address.data = venue.address + form.phone.data = venue.phone + form.image_link.data = venue.image_link + form.genres.data = venue.genres + form.facebook_link.data = venue.facebook_link + form.website_link.data = venue.website_link + form.seeking_talent.data = venue.seeking_talent + form.seeking_description.data = venue.seeking_description + + return render_template(EDIT_VENUE_TEMPLATE, form=form, venue=venue) @app.route('/venues//edit', methods=['POST']) def edit_venue_submission(venue_id): - # TODO: take values from the form submitted, and update existing - # venue record with ID using the new attributes + error = False + try: + venue = db.session.get(Venue, venue_id) + + venue.name = request.form['name'] + venue.city = request.form['city'] + venue.state = request.form['state'] + venue.address = request.form['address'] + venue.phone = request.form['phone'] + venue.genres = request.form.getlist('genres') + venue.image_link = request.form['image_link'] + venue.facebook_link = request.form['facebook_link'] + venue.website_link = request.form['website_link'] + venue.seeking_talent = True if 'seeking_talent' in request.form else False + venue.seeking_description = request.form['seeking_description'] + + db.session.commit() + except Exception: + error = True + db.session.rollback() + print(sys.exc_info()) + finally: + db.session.close() + + if error: + flash( + 'An error occurred. Venue ' + request.form['name'] + ' could not be updated.', + 'danger' + ) + if not error: + flash( + 'Venue ' + request.form['name'] + ' was successfully updated!', + 'success' + ) + return redirect(url_for('show_venue', venue_id=venue_id)) # Create Artist @@ -408,91 +411,107 @@ def edit_venue_submission(venue_id): @app.route('/artists/create', methods=['GET']) def create_artist_form(): form = ArtistForm() - return render_template('forms/new_artist.html', form=form) + return render_template(NEW_ARTIST_TEMPLATE, form=form) @app.route('/artists/create', methods=['POST']) def create_artist_submission(): - # called upon submitting the new artist listing form - # TODO: insert form data as a new Venue record in the db, instead - # TODO: modify data to be the data object returned from db insertion - - # on successful db insert, flash success - flash('Artist ' + request.form['name'] + ' was successfully listed!') - # TODO: on unsuccessful db insert, flash an error instead. - # e.g., flash('An error occurred. Artist ' + data.name + ' could not be listed.') - return render_template('pages/home.html') + error = False + try: + artist = Artist(name = request.form['name'], + city = request.form['city'], + state = request.form['state'], + phone = request.form['phone'], + genres = request.form.getlist('genres'), + image_link = request.form['image_link'], + facebook_link = request.form['facebook_link'], + website_link = request.form['website_link'], + seeking_venue = True if 'seeking_venue' in request.form else False , + seeking_description = request.form['seeking_description']) + + db.session.add(artist) + db.session.commit() + except Exception: + error = True + db.session.rollback() + print(sys.exc_info()) + finally: + db.session.close() + + if error: + flash('An error occurred. Artist ' + request.form['name'] + ' could not be listed.') + else: + flash('Artist ' + request.form['name'] + ' was successfully listed!') + + return render_template(HOME_PAGE_TEMPLATE) +# ---------------------------------------------------------------- # Shows # ---------------------------------------------------------------- @app.route('/shows') def shows(): - # displays list of shows at /shows - # TODO: replace with real venues data. - data=[{ - "venue_id": 1, - "venue_name": "The Musical Hop", - "artist_id": 4, - "artist_name": "Guns N Petals", - "artist_image_link": "https://images.unsplash.com/photo-1549213783-8284d0336c4f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=300&q=80", - "start_time": "2019-05-21T21:30:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "artist_id": 5, - "artist_name": "Matt Quevedo", - "artist_image_link": "https://images.unsplash.com/photo-1495223153807-b916f75de8c5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80", - "start_time": "2019-06-15T23:00:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-01T20:00:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-08T20:00:00.000Z" - }, { - "venue_id": 3, - "venue_name": "Park Square Live Music & Coffee", - "artist_id": 6, - "artist_name": "The Wild Sax Band", - "artist_image_link": "https://images.unsplash.com/photo-1558369981-f9ca78462e61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=794&q=80", - "start_time": "2035-04-15T20:00:00.000Z" - }] - return render_template('pages/shows.html', shows=data) + data = [] + + shows = db.session \ + .query( + Venue.name, + Artist.name, + Artist.image_link, + Show.venue_id, + Show.artist_id, + Show.start_time + ) \ + .filter(Venue.id == Show.venue_id, Artist.id == Show.artist_id) + + for show in shows: + data.append({ + 'venue_name': show[0], + 'artist_name': show[1], + 'artist_image_link': show[2], + 'venue_id': show[3], + 'artist_id': show[4], + 'start_time': str(show[5]) + }) + + return render_template(SHOWS_TEMPLATE, shows=data) @app.route('/shows/create') def create_shows(): - # renders form. do not touch. form = ShowForm() - return render_template('forms/new_show.html', form=form) + return render_template(NEW_SHOW_TEMPLATE, form=form) @app.route('/shows/create', methods=['POST']) def create_show_submission(): - # called to create new shows in the db, upon submitting new show listing form - # TODO: insert form data as a new Show record in the db, instead - - # on successful db insert, flash success - flash('Show was successfully listed!') - # TODO: on unsuccessful db insert, flash an error instead. - # e.g., flash('An error occurred. Show could not be listed.') - # see: http://flask.pocoo.org/docs/1.0/patterns/flashing/ - return render_template('pages/home.html') + error = False + try: + show = Show(start_time=request.form['start_time'], + artist_id=request.form['artist_id'], + venue_id=request.form['venue_id']) + + db.session.add(show) + db.session.commit() + except Exception: + error = True + db.session.rollback() + print(sys.exc_info()) + finally: + db.session.close() + + if error: + flash('An error occurred. Show could not be listed.', 'danger') + else: + flash('Show was successfully listed!', 'success') + + return render_template(HOME_PAGE_TEMPLATE) @app.errorhandler(404) def not_found_error(error): - return render_template('errors/404.html'), 404 + return render_template(ERROR_404), 404 @app.errorhandler(500) def server_error(error): - return render_template('errors/500.html'), 500 + return render_template(ERROR_500), 500 if not app.debug: diff --git a/config.py b/config.py index c91475f47..8a3381405 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,5 @@ DEBUG = True # Connect to the database - - -# TODO IMPLEMENT DATABASE URL -SQLALCHEMY_DATABASE_URI = '' +database_url = 'postgresql://postgres:123456@localhost:5433/Fyyur' +SQLALCHEMY_DATABASE_URI = database_url diff --git a/constants.py b/constants.py new file mode 100644 index 000000000..579c0dd40 --- /dev/null +++ b/constants.py @@ -0,0 +1,15 @@ +HOME_PAGE_TEMPLATE = 'pages/home.html' +NEW_VENUE_TEMPLATE = 'forms/new_venue.html' +ARTISTS_PAGE_TEMPLATE = 'pages/artists.html' +SEARCH_ARTISTS_TEMPLATE = 'pages/search_artists.html' +SHOW_ARTIST_TEMPLATE = 'pages/show_artist.html' +SHOWS_TEMPLATE = 'pages/shows.html' +SHOW_VENUE_TEMPLATE = 'pages/show_venue.html' +ERROR_404 = 'errors/404.html' +ERROR_500 = 'errors/500.html' +VENUES_TEMPLATE = 'pages/venues.html' +SEARCH_VENUES_TEMPLATE = 'pages/search_venues.html' +EDIT_VENUE_TEMPLATE = 'forms/edit_venue.html' +EDIT_ARTIST_TEMPLATE = 'forms/edit_artist.html' +NEW_ARTIST_TEMPLATE = 'forms/new_artist.html' +NEW_SHOW_TEMPLATE = 'forms/new_show.html' diff --git a/enums.py b/enums.py new file mode 100644 index 000000000..4e2aeee1d --- /dev/null +++ b/enums.py @@ -0,0 +1,83 @@ +from enum import Enum + +class Genres(Enum): + Alternative = 'Alternative' + Blues = 'Blues' + Classical = 'Classical' + Country = 'Country' + Electronic = 'Electronic' + Folk = 'Folk' + Funk = 'Funk' + HipHop = 'Hip-Hop' + HeavyMetal = 'Heavy Metal' + Instrumental = 'Instrumental' + Jazz = 'Jazz' + MusicalTheatre = 'Musical Theatre' + Pop = 'Pop' + Punk = 'Punk' + RnB = 'R&B' + Reggae = 'Reggae' + RocknRoll = 'Rock n Roll' + Soul = 'Soul' + Other = 'Other' + + @classmethod + def items(cls): + return [(item.name, item.value) for item in cls] + +class States(Enum): + AL = 'AL' + AK = 'AK' + AZ = 'AZ' + AR = 'AR' + CA = 'CA' + CO = 'CO' + CT = 'CT' + DE = 'DE' + DC = 'DC' + FL = 'FL' + GA = 'GA' + HI = 'HI' + ID = 'ID' + IL = 'IL' + IN = 'IN' + IA = 'IA' + KS = 'KS' + KY = 'KY' + LA = 'LA' + ME = 'ME' + MT = 'MT' + NE = 'NE' + NV = 'NV' + NH = 'NH' + NJ = 'NJ' + NM = 'NM' + NY = 'NY' + NC = 'NC' + ND = 'ND' + OH = 'OH' + OK = 'OK' + OR = 'OR' + MD = 'MD' + MA = 'MA' + MI = 'MI' + MN = 'MN' + MS = 'MS' + MO = 'MO' + PA = 'PA' + RI = 'RI' + SC = 'SC' + SD = 'SD' + TN = 'TN' + TX = 'TX' + UT = 'UT' + VT = 'VT' + VA = 'VA' + WA = 'WA' + WV = 'WV' + WI = 'WI' + WY = 'WY' + + @classmethod + def items(cls): + return [(item.name, item.value) for item in cls] \ No newline at end of file diff --git a/fabfile.py b/fabfile.py index 33a5dee0d..f4628310a 100644 --- a/fabfile.py +++ b/fabfile.py @@ -14,7 +14,7 @@ def test(): def commit(): - message = raw_input("Enter a git commit message: ") + message = input("Enter a git commit message: ") local("git add . && git commit -am '{}'".format(message)) diff --git a/forms.py b/forms.py index ffd553b6e..441d814c6 100644 --- a/forms.py +++ b/forms.py @@ -1,7 +1,8 @@ from datetime import datetime from flask_wtf import Form -from wtforms import StringField, SelectField, SelectMultipleField, DateTimeField, BooleanField -from wtforms.validators import DataRequired, AnyOf, URL +from wtforms import StringField, SelectField, SelectMultipleField, DateTimeField, BooleanField, IntegerField +from wtforms.validators import DataRequired, URL, ValidationError, Regexp, AnyOf +from enums import States, Genres class ShowForm(Form): artist_id = StringField( @@ -16,6 +17,12 @@ class ShowForm(Form): default= datetime.today() ) +def is_facebook_url(form, field): + if "facebook.com" not in field.data: + raise ValidationError("URL must be a Facebook link.") + +phone_regex = r'^\+?1?\d{9,15}$' + class VenueForm(Form): name = StringField( 'name', validators=[DataRequired()] @@ -24,97 +31,25 @@ class VenueForm(Form): 'city', validators=[DataRequired()] ) state = SelectField( - 'state', validators=[DataRequired()], - choices=[ - ('AL', 'AL'), - ('AK', 'AK'), - ('AZ', 'AZ'), - ('AR', 'AR'), - ('CA', 'CA'), - ('CO', 'CO'), - ('CT', 'CT'), - ('DE', 'DE'), - ('DC', 'DC'), - ('FL', 'FL'), - ('GA', 'GA'), - ('HI', 'HI'), - ('ID', 'ID'), - ('IL', 'IL'), - ('IN', 'IN'), - ('IA', 'IA'), - ('KS', 'KS'), - ('KY', 'KY'), - ('LA', 'LA'), - ('ME', 'ME'), - ('MT', 'MT'), - ('NE', 'NE'), - ('NV', 'NV'), - ('NH', 'NH'), - ('NJ', 'NJ'), - ('NM', 'NM'), - ('NY', 'NY'), - ('NC', 'NC'), - ('ND', 'ND'), - ('OH', 'OH'), - ('OK', 'OK'), - ('OR', 'OR'), - ('MD', 'MD'), - ('MA', 'MA'), - ('MI', 'MI'), - ('MN', 'MN'), - ('MS', 'MS'), - ('MO', 'MO'), - ('PA', 'PA'), - ('RI', 'RI'), - ('SC', 'SC'), - ('SD', 'SD'), - ('TN', 'TN'), - ('TX', 'TX'), - ('UT', 'UT'), - ('VT', 'VT'), - ('VA', 'VA'), - ('WA', 'WA'), - ('WV', 'WV'), - ('WI', 'WI'), - ('WY', 'WY'), - ] + 'state', validators=[DataRequired(), + AnyOf([item.value for item in States])], + choices=States.items() ) address = StringField( 'address', validators=[DataRequired()] ) phone = StringField( - 'phone' + 'phone', validators=[Regexp(phone_regex, 0, message="Invalid phone number format.")] ) image_link = StringField( 'image_link' ) genres = SelectMultipleField( - # TODO implement enum restriction - 'genres', validators=[DataRequired()], - choices=[ - ('Alternative', 'Alternative'), - ('Blues', 'Blues'), - ('Classical', 'Classical'), - ('Country', 'Country'), - ('Electronic', 'Electronic'), - ('Folk', 'Folk'), - ('Funk', 'Funk'), - ('Hip-Hop', 'Hip-Hop'), - ('Heavy Metal', 'Heavy Metal'), - ('Instrumental', 'Instrumental'), - ('Jazz', 'Jazz'), - ('Musical Theatre', 'Musical Theatre'), - ('Pop', 'Pop'), - ('Punk', 'Punk'), - ('R&B', 'R&B'), - ('Reggae', 'Reggae'), - ('Rock n Roll', 'Rock n Roll'), - ('Soul', 'Soul'), - ('Other', 'Other'), - ] + 'genres', validators=[DataRequired(), AnyOf([item.value for item in Genres])], + choices=Genres.items() ) facebook_link = StringField( - 'facebook_link', validators=[URL()] + 'facebook_link', validators=[URL(), is_facebook_url] ) website_link = StringField( 'website_link' @@ -126,8 +61,6 @@ class VenueForm(Form): 'seeking_description' ) - - class ArtistForm(Form): name = StringField( 'name', validators=[DataRequired()] @@ -136,95 +69,21 @@ class ArtistForm(Form): 'city', validators=[DataRequired()] ) state = SelectField( - 'state', validators=[DataRequired()], - choices=[ - ('AL', 'AL'), - ('AK', 'AK'), - ('AZ', 'AZ'), - ('AR', 'AR'), - ('CA', 'CA'), - ('CO', 'CO'), - ('CT', 'CT'), - ('DE', 'DE'), - ('DC', 'DC'), - ('FL', 'FL'), - ('GA', 'GA'), - ('HI', 'HI'), - ('ID', 'ID'), - ('IL', 'IL'), - ('IN', 'IN'), - ('IA', 'IA'), - ('KS', 'KS'), - ('KY', 'KY'), - ('LA', 'LA'), - ('ME', 'ME'), - ('MT', 'MT'), - ('NE', 'NE'), - ('NV', 'NV'), - ('NH', 'NH'), - ('NJ', 'NJ'), - ('NM', 'NM'), - ('NY', 'NY'), - ('NC', 'NC'), - ('ND', 'ND'), - ('OH', 'OH'), - ('OK', 'OK'), - ('OR', 'OR'), - ('MD', 'MD'), - ('MA', 'MA'), - ('MI', 'MI'), - ('MN', 'MN'), - ('MS', 'MS'), - ('MO', 'MO'), - ('PA', 'PA'), - ('RI', 'RI'), - ('SC', 'SC'), - ('SD', 'SD'), - ('TN', 'TN'), - ('TX', 'TX'), - ('UT', 'UT'), - ('VT', 'VT'), - ('VA', 'VA'), - ('WA', 'WA'), - ('WV', 'WV'), - ('WI', 'WI'), - ('WY', 'WY'), - ] + 'state', validators=[DataRequired(), AnyOf([item.value for item in States])], + choices=States.items() ) phone = StringField( - # TODO implement validation logic for state - 'phone' + 'phone', validators=[Regexp(phone_regex, 0, message="Invalid phone number format.")] ) image_link = StringField( 'image_link' ) genres = SelectMultipleField( - 'genres', validators=[DataRequired()], - choices=[ - ('Alternative', 'Alternative'), - ('Blues', 'Blues'), - ('Classical', 'Classical'), - ('Country', 'Country'), - ('Electronic', 'Electronic'), - ('Folk', 'Folk'), - ('Funk', 'Funk'), - ('Hip-Hop', 'Hip-Hop'), - ('Heavy Metal', 'Heavy Metal'), - ('Instrumental', 'Instrumental'), - ('Jazz', 'Jazz'), - ('Musical Theatre', 'Musical Theatre'), - ('Pop', 'Pop'), - ('Punk', 'Punk'), - ('R&B', 'R&B'), - ('Reggae', 'Reggae'), - ('Rock n Roll', 'Rock n Roll'), - ('Soul', 'Soul'), - ('Other', 'Other'), - ] - ) + 'genres', validators=[DataRequired(), AnyOf([item.value for item in Genres])], + choices=Genres.items() + ) facebook_link = StringField( - # TODO implement enum restriction - 'facebook_link', validators=[URL()] + 'facebook_link', validators=[URL(), is_facebook_url] ) website_link = StringField( @@ -237,3 +96,13 @@ class ArtistForm(Form): 'seeking_description' ) +class ShowForm(Form): + start_time = DateTimeField( + 'start_time', validators=[DataRequired()], default=datetime.today() + ) + artist_id = IntegerField( + 'artist_id' + ) + venue_id = IntegerField( + 'venue_id' + ) \ No newline at end of file diff --git a/images/databases/database_local.png b/images/databases/database_local.png new file mode 100644 index 000000000..23e418af1 Binary files /dev/null and b/images/databases/database_local.png differ diff --git a/images/databases/select_artist.png b/images/databases/select_artist.png new file mode 100644 index 000000000..18da0df9f Binary files /dev/null and b/images/databases/select_artist.png differ diff --git a/images/databases/select_migrate_version.png b/images/databases/select_migrate_version.png new file mode 100644 index 000000000..198968f27 Binary files /dev/null and b/images/databases/select_migrate_version.png differ diff --git a/images/databases/select_show.png b/images/databases/select_show.png new file mode 100644 index 000000000..9bf0d6b60 Binary files /dev/null and b/images/databases/select_show.png differ diff --git a/images/databases/select_venue.png b/images/databases/select_venue.png new file mode 100644 index 000000000..9734add1d Binary files /dev/null and b/images/databases/select_venue.png differ diff --git a/images/website/create_artist.png b/images/website/create_artist.png new file mode 100644 index 000000000..fe2939629 Binary files /dev/null and b/images/website/create_artist.png differ diff --git a/images/website/create_show.png b/images/website/create_show.png new file mode 100644 index 000000000..6c67d687c Binary files /dev/null and b/images/website/create_show.png differ diff --git a/images/website/create_venue.png b/images/website/create_venue.png new file mode 100644 index 000000000..cae1accf0 Binary files /dev/null and b/images/website/create_venue.png differ diff --git a/images/website/edit_artist.png b/images/website/edit_artist.png new file mode 100644 index 000000000..51cce08c4 Binary files /dev/null and b/images/website/edit_artist.png differ diff --git a/images/website/edit_venue.png b/images/website/edit_venue.png new file mode 100644 index 000000000..61ba14bdf Binary files /dev/null and b/images/website/edit_venue.png differ diff --git a/images/website/search_artist.png b/images/website/search_artist.png new file mode 100644 index 000000000..c81c71402 Binary files /dev/null and b/images/website/search_artist.png differ diff --git a/images/website/search_venues.png b/images/website/search_venues.png new file mode 100644 index 000000000..e9b4bff08 Binary files /dev/null and b/images/website/search_venues.png differ diff --git a/images/website/view_artist.png b/images/website/view_artist.png new file mode 100644 index 000000000..1406fb648 Binary files /dev/null and b/images/website/view_artist.png differ diff --git a/images/website/view_shows.png b/images/website/view_shows.png new file mode 100644 index 000000000..55666e681 Binary files /dev/null and b/images/website/view_shows.png differ diff --git a/images/website/view_venue.png b/images/website/view_venue.png new file mode 100644 index 000000000..300dee342 Binary files /dev/null and b/images/website/view_venue.png differ diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/e1291f27b8a9_init.py b/migrations/versions/e1291f27b8a9_init.py new file mode 100644 index 000000000..3e984f47c --- /dev/null +++ b/migrations/versions/e1291f27b8a9_init.py @@ -0,0 +1,67 @@ +"""init + +Revision ID: e1291f27b8a9 +Revises: +Create Date: 2024-02-09 15:25:54.136718 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e1291f27b8a9' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('Artist', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('city', sa.String(length=120), nullable=True), + sa.Column('state', sa.String(length=120), nullable=True), + sa.Column('phone', sa.String(length=120), nullable=True), + sa.Column('genres', sa.String(length=120), nullable=True), + sa.Column('image_link', sa.String(length=500), nullable=True), + sa.Column('facebook_link', sa.String(length=120), nullable=True), + sa.Column('website_link', sa.String(length=120), nullable=True), + sa.Column('seeking_venue', sa.Boolean(), nullable=True), + sa.Column('seeking_description', sa.String(length=500), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('Venue', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('genres', sa.ARRAY(sa.String()), nullable=True), + sa.Column('address', sa.String(length=120), nullable=True), + sa.Column('city', sa.String(length=120), nullable=True), + sa.Column('state', sa.String(length=120), nullable=True), + sa.Column('phone', sa.String(length=120), nullable=True), + sa.Column('website_link', sa.String(length=120), nullable=True), + sa.Column('facebook_link', sa.String(length=120), nullable=True), + sa.Column('seeking_talent', sa.Boolean(), nullable=True), + sa.Column('seeking_description', sa.String(length=500), nullable=True), + sa.Column('image_link', sa.String(length=500), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('Show', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('artist_id', sa.Integer(), nullable=False), + sa.Column('venue_id', sa.Integer(), nullable=False), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['artist_id'], ['Artist.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['Venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('Show') + op.drop_table('Venue') + op.drop_table('Artist') + # ### end Alembic commands ### diff --git a/models.py b/models.py new file mode 100644 index 000000000..a00e8f466 --- /dev/null +++ b/models.py @@ -0,0 +1,55 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() + +def setup_db(app): + app.config.from_object('config') + db.app = app + migrate = Migrate(app, db) + db.init_app(app) + return db +#----------------------------------------------------------------------------# +# Models. +#----------------------------------------------------------------------------# + +class Venue(db.Model): + __tablename__ = 'Venue' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + genres = db.Column(db.ARRAY(db.String())) + address = db.Column(db.String(120)) + city = db.Column(db.String(120)) + state = db.Column(db.String(120)) + phone = db.Column(db.String(120)) + website_link = db.Column(db.String(120)) + facebook_link = db.Column(db.String(120)) + seeking_talent = db.Column(db.Boolean) + seeking_description = db.Column(db.String(500)) + image_link = db.Column(db.String(500)) + shows = db.relationship('Show', backref='Venue', lazy=True) + +class Artist(db.Model): + __tablename__ = 'Artist' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + city = db.Column(db.String(120)) + state = db.Column(db.String(120)) + phone = db.Column(db.String(120)) + genres = db.Column(db.String(120)) + image_link = db.Column(db.String(500)) + facebook_link = db.Column(db.String(120)) + website_link = db.Column(db.String(120)) + seeking_venue = db.Column(db.Boolean) + seeking_description = db.Column(db.String(500)) + shows = db.relationship('Show', backref='Artist', lazy=True) + +class Show(db.Model): + __tablename__ = 'Show' + + id = db.Column(db.Integer, primary_key=True) + artist_id = db.Column(db.Integer, db.ForeignKey('Artist.id'), nullable=False) + venue_id = db.Column(db.Integer, db.ForeignKey('Venue.id'), nullable=False) + start_time = db.Column(db.DateTime, nullable=False) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..e433728f2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "cd0046-sql-and-data-modeling-for-the-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cd0046-sql-and-data-modeling-for-the-web", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bootstrap": "^3.4.1" + } + }, + "node_modules/bootstrap": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..41aba5773 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "cd0046-sql-and-data-modeling-for-the-web", + "version": "1.0.0", + "description": "Fyyur -----", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bootstrap": "^3.4.1" + } +} + diff --git a/requirements.txt b/requirements.txt index 16d8ace89..41f284132 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ babel==2.9.0 python-dateutil==2.6.0 -flask-moment==0.11.0 -flask-wtf==0.14.3 -flask_sqlalchemy==2.4.4 +flask-moment==1.0.5 +flask-wtf==1.2.1 +flask_sqlalchemy==3.1.1 diff --git a/templates/pages/show_artist.html b/templates/pages/show_artist.html index 2c33e52df..c97600443 100644 --- a/templates/pages/show_artist.html +++ b/templates/pages/show_artist.html @@ -21,7 +21,7 @@

{% if artist.phone %}{{ artist.phone }}{% else %}No Phone{% endif %}

- {% if artist.website %}{{ artist.website }}{% else %}No Website{% endif %} + {% if artist.website_link %}{{ artist.website_link }}{% else %}No Website{% endif %}

{% if artist.facebook_link %}{{ artist.facebook_link }}{% else %}No Facebook Link{% endif %} diff --git a/templates/pages/show_venue.html b/templates/pages/show_venue.html index 39d562b06..625026504 100644 --- a/templates/pages/show_venue.html +++ b/templates/pages/show_venue.html @@ -24,7 +24,7 @@

{% if venue.phone %}{{ venue.phone }}{% else %}No Phone{% endif %}

- {% if venue.website %}{{ venue.website }}{% else %}No Website{% endif %} + {% if venue._link %}{{ venue.website_link }}{% else %}No Website{% endif %}

{% if venue.facebook_link %}{{ venue.facebook_link }}{% else %}No Facebook Link{% endif %}