diff --git a/app.py b/app.py index ed56020d7..5c8d1e7fb 100644 --- a/app.py +++ b/app.py @@ -1,48 +1,65 @@ -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Imports -#----------------------------------------------------------------------------# - -import json +# ----------------------------------------------------------------------------# +from werkzeug.serving import run_simple +from sqlalchemy.sql import func +import os import dateutil.parser import babel -from flask import Flask, render_template, request, Response, flash, redirect, url_for +from flask import Flask, render_template, request, Response, flash, redirect, url_for, abort from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.exc import SQLAlchemyError import logging from logging import Formatter, FileHandler -from flask_wtf import Form +from flask_wtf import FlaskForm +from flask_wtf.csrf import CSRFProtect +from flask_migrate import Migrate from forms import * -#----------------------------------------------------------------------------# +from datetime import datetime +# ----------------------------------------------------------------------------# # App Config. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# app = Flask(__name__) moment = Moment(app) app.config.from_object('config') db = SQLAlchemy(app) +migrate = Migrate(app, db) -# TODO: connect to a local postgresql database -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Models. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# + class Venue(db.Model): - __tablename__ = 'Venue' + __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)) + name = db.Column(db.String(120), nullable=False) + city = db.Column(db.String(120), nullable=False) + state = db.Column(db.String(120), nullable=False) + address = db.Column(db.String(120), nullable=False) phone = db.Column(db.String(120)) image_link = db.Column(db.String(500)) facebook_link = db.Column(db.String(120)) + website = db.Column(db.String(120)) + seeking_talent = db.Column(db.Boolean, default=False) + seeking_description = db.Column(db.String(500)) + genres = db.Column(db.ARRAY(db.String(50))) + created_at = db.Column(db.DateTime, server_default=func.now()) + updated_at = db.Column( + db.DateTime, + server_default=func.now(), + onupdate=func.now()) + + def __repr__(self): + return f'' - # TODO: implement any missing fields, as a database migration using Flask-Migrate class Artist(db.Model): - __tablename__ = 'Artist' + __tablename__ = 'artists' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) @@ -53,31 +70,63 @@ class Artist(db.Model): 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 + def __repr__(self): + return f'< Artist is {self.name} {self.id}>' + + +class Show(db.Model): + __tablename__ = 'shows' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200)) + start_time = db.Column(db.DateTime, index=True) + venue_id = db.Column(db.Integer, db.ForeignKey('venue.id')) + artist_id = db.Column(db.Integer, db.ForeignKey('artists.id')) + + venue = db.relationship('Venue', backref='shows') + artist = db.relationship('Artist', backref='shows') + + def __repr__(self): + return f'' + + +class Availability(db.Model): + + __tablename__ = 'availability' + + id = db.Column(db.Integer, primary_key=True) + working_period_start = db.Column(db.DateTime) + working_period_end = db.Column(db.DateTime) + artist_id = db.Column(db.Integer, db.ForeignKey('artists.id')) + artist = db.relationship('Artist', backref='avialabilities') + + def __repr__(self): + return f' {self.working_period_end}>' -# TODO Implement Show and Artist models, and complete all model relationships and properties, as a database migration. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Filters. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# def format_datetime(value, 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') + 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') + app.jinja_env.filters['datetime'] = format_datetime -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Controllers. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# + @app.route('/') def index(): - return render_template('pages/home.html') + return render_template('pages/home.html') # Venues @@ -85,342 +134,289 @@ 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); - -@app.route('/venues/search', methods=['POST']) + try: + # pagination may be implemented later + data = Venue.query.order_by(Venue.name).all() + return render_template('pages/venues.html', areas=data) + + except Exception as e: + + print(f"Error retrieving venues: {e}") + return render_template('errors/500.html'), 500 + + +@app.route('/venues/search', methods=['GET', '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, - }] - } - return render_template('pages/search_venues.html', results=response, search_term=request.form.get('search_term', '')) + search_term = request.form.get('search_term', '') + venues = Venue.query.filter( + func.lower( + Venue.name).contains( + func.lower(search_term))).all() + current_time = datetime.now() + + response = { + "count": len(venues), + "data": [{ + "id": venue.id, + "name": venue.name, + "num_upcoming_shows": len([ + show for show in venue.shows + if show.start_time > current_time + ]) + } for venue in venues] + } + print(response) + + return render_template( + 'pages/search_venues.html', + results=response, + 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) + try: + + data = Venue.query.get(venue_id) + return render_template('pages/show_venue.html', venue=data) + except Exception as e: + + print(f"Error retrieving venue with {venue_id} id: {e}") + return render_template('errors/500.html'), 500 + # Create Venue # ---------------------------------------------------------------- @app.route('/venues/create', methods=['GET']) def create_venue_form(): - form = VenueForm() - return render_template('forms/new_venue.html', form=form) + form = VenueForm() + return render_template('forms/new_venue.html', 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 + venue_form = VenueForm() + if venue_form.validate(): + try: + new_venue = Venue( + name=venue_form.name.data, + city=venue_form.city.data, + state=venue_form.state.data, + address=venue_form.address.data, + phone=venue_form.phone.data, + genres=venue_form.genres.data, + facebook_link=venue_form.facebook_link.data, + image_link=venue_form.image_link.data, + website=venue_form.website_link.data + ) + db.session.add(new_venue) + db.session.commit() + flash( + 'Venue ' + + request.form['name'] + + ' was successfully listed!') + except Exception as e: + db.session.rollback() + flash( + 'An error occurred. Venue ' + + request.form['name'] + + ' could not be listed.') + print(e) + finally: + db.session.close() + else: + for field, errors in venue_form.errors.items(): + for error in errors: + flash(f"Error in {field}: {error}") + + return render_template('pages/home.html') + + +@app.route('/venues/', methods=['POST', 'DELETE']) +def delete_venue(venue_id): + try: - # 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') + item_to_delete = db.session.query(Venue).get(venue_id) -@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. + if not item_to_delete: + abort(404) + + db.session.delete(item_to_delete) + db.session.commit() + flash('Venue successfully deleted!') - # 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 + except SQLAlchemyError as e: + db.session.rollback() + flash(f'Error deleting venue: {str(e)}', 'error') + abort(500) + + return redirect(url_for('shows')) # 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) + try: + + data = Artist.query.all() + return render_template('pages/artists.html', artists=data) + + except Exception as e: + + print(f"Error retrieving artists: {e}") + return render_template('errors/500.html'), 500 + @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, - }] - } - return render_template('pages/search_artists.html', results=response, search_term=request.form.get('search_term', '')) + try: + + search_term = request.form['search_term'] + artists = Artist.query.filter( + Artist.namename.ilike(f'%{search_term}%')).all() + return render_template( + 'pages/search_artists.html', + results=artists, + search_term=request.form.get( + 'search_term', + '')) + + except Exception as e: + + print(f'Error occured while searchig for specific artist : {e}') + return render_template('500.html'), 500 + @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) + try: + + data = Show.query.get(artist_id) + return render_template('pages/show_artist.html', artist=data) + + except Exception as e: + + print('Error occured while retrieving artists:{e}') + return render_template('500.html'), 500 # Update # ---------------------------------------------------------------- + + @app.route('/artists//edit', methods=['GET']) def edit_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) + try: + + record = Artist.query.get_or_404(artist_id) + form = ArtistForm(obj=record) + return render_template( + 'forms/edit_artist.html', + form=form, + artist=record) + + except Exception as e: + + print('Error occured when retrieving artists with error" {e}') + return render_template('500.html'), 500 + @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 - return redirect(url_for('show_artist', artist_id=artist_id)) + record = Artist.query.get_or_404(artist_id) + form = ArtistForm(obj=record) + if form.validate(): + try: + form.populate_obj(record) + db.session.commit() + flash(f'Artist {record.name} was updated successfully') + except Exception as e: + db.session.rollback() + flash(f'something went wrong with {record.name}') + finally: + db.session.close() + else: + for field, errors in form.errors.items(): + for error in errors: + flash(f'Error in {field}: {error}') + + return redirect(url_for('show_artist', artist_id=artist_id)) + @app.route('/venues//edit', methods=['GET']) def edit_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) + record = Venue.query.get_or_404(venue_id) + form = VenueForm(obj=record) + + return render_template('forms/edit_venue.html', form=form, venue=record) + @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 - return redirect(url_for('show_venue', venue_id=venue_id)) + venue = Venue.query.get_or_404(venue_id) + form = VenueForm(request.form) + + if form.validate(): + try: + form.populate_obj(venue) + db.session.commit() + flash(f'Venue {venue.id}:{venue.name} was successfully updated!') + except Exception as e: + db.session.rollback() + flash( + f'The error occurred. Venue {venue.name} could not be updated.') + print(e) + finally: + db.session.close() + else: + for field, errors in form.errors.items(): + for error in errors: + flash(f'Error in {field}: {error}') + + return redirect(url_for('show_venue', venue_id=venue_id)) -# Create Artist -# ---------------------------------------------------------------- @app.route('/artists/create', methods=['GET']) def create_artist_form(): - form = ArtistForm() - return render_template('forms/new_artist.html', form=form) + form = ArtistForm() + return render_template('forms/new_artist.html', 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') + form = ArtistForm() + if form.validate_on_submit(): + try: + new_artist = Artist( + name=form.name.data, + city=form.city.data, + state=form.state.data, + phone=form.phone.data, + genres=form.genres.data, + facebook_link=form.facebook_link.data, + image_link=form.image_link.data + ) + db.session.add(new_artist) + db.session.commit() + flash('Artist ' + new_artist.name + ' was successfully listed!') + except Exception as e: + db.session.rollback() + flash( + 'The error occurred. Artist ' + + form.name.data + + ' could not be listed.') + print(f'Error is {e}') + finally: + db.session.close() + else: + for field, errors in form.errors.items(): + for error in errors: + flash(f'Error in {field}: {error}') + + return render_template('pages/home.html') # Shows @@ -428,68 +424,72 @@ def create_artist_submission(): @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) + shows = db.session.query(Show, Venue, Artist).join( + Venue).join(Artist).all() + data = [] + for show, venue, artist in shows: + data.append({ + "venue_id": venue.id, + "venue_name": venue.name, + "artist_id": artist.id, + "artist_name": artist.name, + "artist_image_link": artist.image_link, + "start_time": show.start_time.isoformat() + }) + return render_template('pages/shows.html', 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) + # renders form. do not touch. + form = ShowForm() + + return render_template('forms/new_show.html', 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 + form = ShowForm() + if form.validate_on_submit(): + try: + artist = Artist.query.get(form.artist_id.data) + venue = Venue.query.get(form.venue_id.data) + if not artist or not venue: + flash('Invalid artist or venue ID.') + return render_template('forms/new_show.html', form=form) + + new_show = Show( + artist_id=artist.id, + venue_id=venue.id, + start_time=form.start_time.data + ) + db.session.add(new_show) + db.session.commit() + flash( + f'Show was successfully listed for {artist.name} at {venue.name}!') + return redirect(url_for('shows')) + except SQLAlchemyError as e: + db.session.rollback() + logging.error(f"Database error creating show: {str(e)}") + flash('An error occurred. Show could not be listed due to a database issue.') + except Exception as e: + db.session.rollback() + logging.error(f"Unexpected error creating show: {str(e)}") + flash('An unexpected error occurred. Show could not be listed.') + finally: + db.session.close() + else: + for field, errors in form.errors.items(): + for error in errors: + flash(f"Error in {getattr(form, field).label.text}: {error}") + return render_template('forms/new_show.html', form=form) - # 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') @app.errorhandler(404) def not_found_error(error): return render_template('errors/404.html'), 404 + @app.errorhandler(500) def server_error(error): return render_template('errors/500.html'), 500 @@ -497,25 +497,27 @@ def server_error(error): if not app.debug: file_handler = FileHandler('error.log') - file_handler.setFormatter( - Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]') - ) + file_handler.setFormatter(Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) app.logger.setLevel(logging.INFO) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.info('errors') -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Launch. -#----------------------------------------------------------------------------# +# ----------------------------------------------------------------------------# # Default port: +''' if __name__ == '__main__': - app.run() + run_simple('localhost', 0, app, use_reloader=True) -# Or specify port manually: + + #app.run(debug=True, port=5000) ''' +# Or specify port manually: + if __name__ == '__main__': - port = int(os.environ.get('PORT', 5000)) + port = int(os.environ.get('PORT', 63653)) app.run(host='0.0.0.0', port=port) -''' diff --git a/config.py b/config.py index c91475f47..f4bdf4478 100644 --- a/config.py +++ b/config.py @@ -10,4 +10,4 @@ # TODO IMPLEMENT DATABASE URL -SQLALCHEMY_DATABASE_URI = '' +SQLALCHEMY_DATABASE_URI = 'postgresql://rastsislaupiatrenka@localhost:5432/project' diff --git a/forms.py b/forms.py index ffd553b6e..d99def36f 100644 --- a/forms.py +++ b/forms.py @@ -1,9 +1,9 @@ from datetime import datetime -from flask_wtf import Form +from flask_wtf import FlaskForm from wtforms import StringField, SelectField, SelectMultipleField, DateTimeField, BooleanField from wtforms.validators import DataRequired, AnyOf, URL -class ShowForm(Form): +class ShowForm(FlaskForm): artist_id = StringField( 'artist_id' ) @@ -16,7 +16,7 @@ class ShowForm(Form): default= datetime.today() ) -class VenueForm(Form): +class VenueForm(FlaskForm): name = StringField( 'name', validators=[DataRequired()] ) @@ -128,7 +128,7 @@ class VenueForm(Form): -class ArtistForm(Form): +class ArtistForm(FlaskForm): name = StringField( 'name', validators=[DataRequired()] ) 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/3132f8470e88_updated_table_names_and_relationship.py b/migrations/versions/3132f8470e88_updated_table_names_and_relationship.py new file mode 100644 index 000000000..acf55c3bb --- /dev/null +++ b/migrations/versions/3132f8470e88_updated_table_names_and_relationship.py @@ -0,0 +1,70 @@ +"""updated table names and relationship + +Revision ID: 3132f8470e88 +Revises: +Create Date: 2025-01-31 22:34:42.614489 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3132f8470e88' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('artists', + 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.PrimaryKeyConstraint('id') + ) + op.create_table('venue', + 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('address', sa.String(length=120), nullable=True), + sa.Column('phone', 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.PrimaryKeyConstraint('id') + ) + op.create_table('availability', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('working_period_start', sa.DateTime(), nullable=True), + sa.Column('working_period_end', sa.DateTime(), nullable=True), + sa.Column('artist_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['artist_id'], ['artists.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('shows', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=200), nullable=True), + sa.Column('date', sa.DateTime(), nullable=True), + sa.Column('venue_id', sa.Integer(), nullable=True), + sa.Column('artist_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['artist_id'], ['artists.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('shows') + op.drop_table('availability') + op.drop_table('venue') + op.drop_table('artists') + # ### end Alembic commands ### diff --git a/migrations/versions/7f9ff24be733_updates_show_model_and_changed_date_.py b/migrations/versions/7f9ff24be733_updates_show_model_and_changed_date_.py new file mode 100644 index 000000000..42390bb76 --- /dev/null +++ b/migrations/versions/7f9ff24be733_updates_show_model_and_changed_date_.py @@ -0,0 +1,34 @@ +"""updates show model and changed date column to start_time + +Revision ID: 7f9ff24be733 +Revises: 3132f8470e88 +Create Date: 2025-02-05 17:02:16.528709 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '7f9ff24be733' +down_revision = '3132f8470e88' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('shows', schema=None) as batch_op: + batch_op.add_column(sa.Column('start_time', sa.DateTime(), nullable=True)) + batch_op.drop_column('date') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('shows', schema=None) as batch_op: + batch_op.add_column(sa.Column('date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + batch_op.drop_column('start_time') + + # ### end Alembic commands ### diff --git a/migrations/versions/9e2fbbb87173_updated_venue_model_with_more_forms_.py b/migrations/versions/9e2fbbb87173_updated_venue_model_with_more_forms_.py new file mode 100644 index 000000000..3cfd709fb --- /dev/null +++ b/migrations/versions/9e2fbbb87173_updated_venue_model_with_more_forms_.py @@ -0,0 +1,66 @@ +"""updated venue model with more forms from the site + +Revision ID: 9e2fbbb87173 +Revises: 7f9ff24be733 +Create Date: 2025-02-05 17:19:28.261097 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9e2fbbb87173' +down_revision = '7f9ff24be733' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('venue', schema=None) as batch_op: + batch_op.add_column(sa.Column('website', sa.String(length=120), nullable=True)) + batch_op.add_column(sa.Column('seeking_talent', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('seeking_description', sa.String(length=500), nullable=True)) + batch_op.add_column(sa.Column('genres', sa.ARRAY(sa.String(length=50)), nullable=True)) + batch_op.add_column(sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True)) + batch_op.add_column(sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True)) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(), + nullable=False) + batch_op.alter_column('city', + existing_type=sa.VARCHAR(length=120), + nullable=False) + batch_op.alter_column('state', + existing_type=sa.VARCHAR(length=120), + nullable=False) + batch_op.alter_column('address', + existing_type=sa.VARCHAR(length=120), + nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('venue', schema=None) as batch_op: + batch_op.alter_column('address', + existing_type=sa.VARCHAR(length=120), + nullable=True) + batch_op.alter_column('state', + existing_type=sa.VARCHAR(length=120), + nullable=True) + batch_op.alter_column('city', + existing_type=sa.VARCHAR(length=120), + nullable=True) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(), + nullable=True) + batch_op.drop_column('updated_at') + batch_op.drop_column('created_at') + batch_op.drop_column('genres') + batch_op.drop_column('seeking_description') + batch_op.drop_column('seeking_talent') + batch_op.drop_column('website') + + # ### end Alembic commands ### diff --git a/migrations/versions/a692a597f8c4_added_index_to_starttime_on_the_show_.py b/migrations/versions/a692a597f8c4_added_index_to_starttime_on_the_show_.py new file mode 100644 index 000000000..ad9c99eba --- /dev/null +++ b/migrations/versions/a692a597f8c4_added_index_to_starttime_on_the_show_.py @@ -0,0 +1,32 @@ +"""added index to starttime on the show model + +Revision ID: a692a597f8c4 +Revises: 9e2fbbb87173 +Create Date: 2025-02-08 15:22:00.428638 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a692a597f8c4' +down_revision = '9e2fbbb87173' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('shows', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_shows_start_time'), ['start_time'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('shows', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_shows_start_time')) + + # ### end Alembic commands ### diff --git a/templates/forms/edit_artist.html b/templates/forms/edit_artist.html index 02472fe5d..929a232ad 100644 --- a/templates/forms/edit_artist.html +++ b/templates/forms/edit_artist.html @@ -3,6 +3,7 @@ {% block content %}
+ {{ form.csrf_token }}

Edit artist {{ artist.name }}

diff --git a/templates/forms/edit_venue.html b/templates/forms/edit_venue.html index caeb78d39..972fd0b53 100644 --- a/templates/forms/edit_venue.html +++ b/templates/forms/edit_venue.html @@ -3,6 +3,7 @@ {% block content %}
+ {{ form.csrf_token }}

Edit venue {{ venue.name }}

diff --git a/templates/forms/new_artist.html b/templates/forms/new_artist.html index 067391bd8..ac7174ec9 100644 --- a/templates/forms/new_artist.html +++ b/templates/forms/new_artist.html @@ -3,6 +3,7 @@ {% block content %}
+ {{ form.csrf_token }}

List a new artist

diff --git a/templates/forms/new_show.html b/templates/forms/new_show.html index 454f0e665..7d9e6f553 100644 --- a/templates/forms/new_show.html +++ b/templates/forms/new_show.html @@ -3,6 +3,7 @@ {% block content %}
+ {{ form.csrf_token }}

List a new show

diff --git a/templates/forms/new_venue.html b/templates/forms/new_venue.html index d21977cc0..7860f2031 100644 --- a/templates/forms/new_venue.html +++ b/templates/forms/new_venue.html @@ -3,6 +3,7 @@ {% block content %}
+ {{ form.csrf_token }}

List a new venue

diff --git a/templates/layouts/main.html b/templates/layouts/main.html index 09bbe8b87..355e6155f 100644 --- a/templates/layouts/main.html +++ b/templates/layouts/main.html @@ -57,6 +57,7 @@ (request.endpoint == 'search_venues') or (request.endpoint == 'show_venue') %} + {% for artist in artists %} +
  • diff --git a/templates/pages/search_artists.html b/templates/pages/search_artists.html index 8c0f98c1c..2bdfcda81 100644 --- a/templates/pages/search_artists.html +++ b/templates/pages/search_artists.html @@ -2,6 +2,7 @@ {% block title %}Fyyur | Artists Search{% endblock %} {% block content %}

    Number of search results for "{{ search_term }}": {{ results.count }}

    +