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 %}