From dbabc6c8610a4bee3996cacc3b1c3a69137c8096 Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 21:02:25 +0100 Subject: [PATCH 01/13] added connection to db and .gitignore file --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' From f52afb7e1cb88f5862bdac8fbcf210122283b4d6 Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 21:07:47 +0100 Subject: [PATCH 02/13] added two models Shows and Availability --- app.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app.py b/app.py index ed56020d7..3c86355e0 100644 --- a/app.py +++ b/app.py @@ -27,6 +27,21 @@ # Models. #----------------------------------------------------------------------------# +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_periond_end}>' + + + class Venue(db.Model): __tablename__ = 'Venue' @@ -57,6 +72,21 @@ class Artist(db.Model): # TODO Implement Show and Artist models, and complete all model relationships and properties, as a database migration. +class Show(db.Model): + __tablename__='shows' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200)) + date = db.Column(db.DateTime) + venue_id = db.Column(db.Integer, db.ForeignKey('venues.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'' + #----------------------------------------------------------------------------# # Filters. #----------------------------------------------------------------------------# From e0370f0467514fa23b8f361e8b9a4289660e51e1 Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 21:24:53 +0100 Subject: [PATCH 03/13] adding migration --- app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.py b/app.py index 3c86355e0..d44c31251 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,7 @@ import logging from logging import Formatter, FileHandler from flask_wtf import Form +from flask_migrate import Migrate from forms import * #----------------------------------------------------------------------------# # App Config. @@ -20,6 +21,7 @@ moment = Moment(app) app.config.from_object('config') db = SQLAlchemy(app) +migrate = Migrate(app,db) # TODO: connect to a local postgresql database From a89635b8146c33fe36e6521e9673d74a1e11d3bc Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 21:52:08 +0100 Subject: [PATCH 04/13] trying to migrate, so far so bad --- app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index d44c31251..f47be6c67 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ #----------------------------------------------------------------------------# # Imports #----------------------------------------------------------------------------# - +import sys import json import dateutil.parser import babel @@ -16,7 +16,7 @@ #----------------------------------------------------------------------------# # App Config. #----------------------------------------------------------------------------# - +print(sys.path) app = Flask(__name__) moment = Moment(app) app.config.from_object('config') From b8d7b20718bd3d86296ffdf4b7e8a590fe0ad92f Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 22:14:03 +0100 Subject: [PATCH 05/13] added migration folder --- migrations/README | 1 + migrations/alembic.ini | 50 +++++++++++++++++ migrations/env.py | 113 ++++++++++++++++++++++++++++++++++++++ migrations/script.py.mako | 24 ++++++++ 4 files changed, 188 insertions(+) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako 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"} From 58d5dcaf61e1612532e13975a84aa3b0db613f14 Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 22:26:42 +0100 Subject: [PATCH 06/13] struggling with migration --- app.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index f47be6c67..9e4a9af81 100644 --- a/app.py +++ b/app.py @@ -29,23 +29,10 @@ # Models. #----------------------------------------------------------------------------# -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_periond_end}>' - class Venue(db.Model): - __tablename__ = 'Venue' + __tablename__ = 'venue' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) @@ -59,7 +46,7 @@ class Venue(db.Model): # TODO: implement any missing fields, as a database migration using Flask-Migrate class Artist(db.Model): - __tablename__ = 'Artist' + __tablename__ = 'artist' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) @@ -89,6 +76,21 @@ class Show(db.Model): 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}>' + + #----------------------------------------------------------------------------# # Filters. #----------------------------------------------------------------------------# From dd782364be9a83b470be79e25d2251eb1c25ea9c Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Fri, 31 Jan 2025 22:28:50 +0100 Subject: [PATCH 07/13] commented out avaliability for the time being --- app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 9e4a9af81..819127b28 100644 --- a/app.py +++ b/app.py @@ -76,7 +76,7 @@ class Show(db.Model): def __repr__(self): return f'' - +''' class Availability(db.Model): __tablename__ = 'availability' @@ -89,7 +89,7 @@ class Availability(db.Model): def __repr__(self): return f' {self.working_period_end}>' - +''' #----------------------------------------------------------------------------# # Filters. From 881edac61d1eae5fbfa99b5748df6e10636b044d Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Tue, 4 Feb 2025 14:54:02 +0100 Subject: [PATCH 08/13] addeed repr to Artist and Venue and finished migrations --- app.py | 15 ++-- ...88_updated_table_names_and_relationship.py | 70 +++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/3132f8470e88_updated_table_names_and_relationship.py diff --git a/app.py b/app.py index 819127b28..1e1b41164 100644 --- a/app.py +++ b/app.py @@ -43,10 +43,13 @@ class Venue(db.Model): image_link = db.Column(db.String(500)) facebook_link = db.Column(db.String(120)) + + 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) @@ -56,6 +59,10 @@ class Artist(db.Model): genres = db.Column(db.String(120)) image_link = db.Column(db.String(500)) facebook_link = db.Column(db.String(120)) + + def __repr__(self): + return f'< Artist is {self.name} {self.id}>' + # TODO: implement any missing fields, as a database migration using Flask-Migrate @@ -67,7 +74,7 @@ class Show(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200)) date = db.Column(db.DateTime) - venue_id = db.Column(db.Integer, db.ForeignKey('venues.id')) + 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') @@ -76,7 +83,7 @@ class Show(db.Model): def __repr__(self): return f'' -''' + class Availability(db.Model): __tablename__ = 'availability' @@ -89,7 +96,7 @@ class Availability(db.Model): def __repr__(self): return f' {self.working_period_end}>' -''' + #----------------------------------------------------------------------------# # Filters. 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 ### From e5585912a12a22404dc41ed49076b43ce5d43894 Mon Sep 17 00:00:00 2001 From: Rastsislau Piatrenka Date: Wed, 5 Feb 2025 20:34:45 +0100 Subject: [PATCH 09/13] added a lot of modification in one commit, which is against best practices --- app.py | 282 ++++++++++++------ forms.py | 8 +- ...33_updates_show_model_and_changed_date_.py | 34 +++ ...73_updated_venue_model_with_more_forms_.py | 66 ++++ templates/forms/edit_artist.html | 1 + templates/forms/edit_venue.html | 1 + templates/forms/new_artist.html | 1 + templates/forms/new_show.html | 1 + templates/forms/new_venue.html | 1 + templates/pages/search_artists.html | 1 + templates/pages/search_venues.html | 1 + templates/pages/venues.html | 1 + 12 files changed, 311 insertions(+), 87 deletions(-) create mode 100644 migrations/versions/7f9ff24be733_updates_show_model_and_changed_date_.py create mode 100644 migrations/versions/9e2fbbb87173_updated_venue_model_with_more_forms_.py diff --git a/app.py b/app.py index 1e1b41164..43a5e1bdf 100644 --- a/app.py +++ b/app.py @@ -1,28 +1,33 @@ #----------------------------------------------------------------------------# # Imports #----------------------------------------------------------------------------# +from werkzeug.serving import run_simple +from sqlalchemy.sql import func import sys +import os import json 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, jsonify 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 * #----------------------------------------------------------------------------# # App Config. #----------------------------------------------------------------------------# -print(sys.path) + app = Flask(__name__) moment = Moment(app) app.config.from_object('config') db = SQLAlchemy(app) migrate = Migrate(app,db) - +csrf = CSRFProtect(app) # TODO: connect to a local postgresql database #----------------------------------------------------------------------------# @@ -33,15 +38,21 @@ 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)) + 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): @@ -73,7 +84,7 @@ class Show(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200)) - date = db.Column(db.DateTime) + start_time = db.Column(db.DateTime) venue_id = db.Column(db.Integer, db.ForeignKey('venue.id')) artist_id = db.Column(db.Integer, db.ForeignKey('artists.id')) @@ -126,6 +137,9 @@ def index(): @app.route('/venues') def venues(): + + data = Venue.query.all() + ''' # TODO: replace with real venues data. # num_upcoming_shows should be aggregated based on number of upcoming shows per venue. data=[{ @@ -149,27 +163,22 @@ def venues(): "num_upcoming_shows": 0, }] }] + ''' return render_template('pages/venues.html', areas=data); @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, - }] - } - return render_template('pages/search_venues.html', results=response, search_term=request.form.get('search_term', '')) +def search_venues(search): + search_term = request.form['search_term'] + venues = Venue.query.filter(Venue.name.ilike(f'%{search_term}%')).all() + return render_template('pages/search_venues.html', results=venues, search_term=request.form.get('search_term', '')) @app.route('/venues/') def show_venue(venue_id): + data = Venue.query.get(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", @@ -247,7 +256,8 @@ def show_venue(venue_id): "past_shows_count": 1, "upcoming_shows_count": 1, } - data = list(filter(lambda d: d['id'] == venue_id, [data1, data2, data3]))[0] + ''' + #data = list(filter(lambda d: d['id'] == venue_id, [data1, data2, data3]))[0] return render_template('pages/show_venue.html', venue=data) # Create Venue @@ -260,18 +270,40 @@ def create_venue_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') + 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=['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. @@ -283,7 +315,8 @@ def delete_venue(venue_id): # ---------------------------------------------------------------- @app.route('/artists') def artists(): - # TODO: replace with real data returned from querying the database + data = Artist.query.all() + ''' data=[{ "id": 4, "name": "Guns N Petals", @@ -294,28 +327,22 @@ def artists(): "id": 6, "name": "The Wild Sax Band", }] + ''' return render_template('pages/artists.html', artists=data) @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', '')) + 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', '')) @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={ + data = Show.query.get(artist_id) + ''' + data1={ "id": 4, "name": "Guns N Petals", "genres": ["Rock n Roll"], @@ -387,13 +414,17 @@ def show_artist(artist_id): "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) + ''' + return render_template('pages/show_artist.html', artist=data) # Update # ---------------------------------------------------------------- @app.route('/artists//edit', methods=['GET']) def edit_artist(artist_id): - form = ArtistForm() + + record = Artist.query.get_or_404(artist_id) + form = ArtistForm(obj=record) + ''' artist={ "id": 4, "name": "Guns N Petals", @@ -407,19 +438,36 @@ def edit_artist(artist_id): "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) + return render_template('forms/edit_artist.html', form=form, artist=record) @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 - + 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() + record = Venue.query.get_or_404(venue_id) + form = VenueForm(obj=record) + ''' venue={ "id": 1, "name": "The Musical Hop", @@ -435,12 +483,30 @@ def edit_venue(venue_id): "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) + ''' + 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 + 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.name} was successfully updated!') + except Exception as e: + db.session.rollback() + flash(f'An 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 @@ -453,15 +519,33 @@ def create_artist_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('An error occurred. Artist ' + form.name.data + ' could not be listed.') + print(e) # For debugging purposes + 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 @@ -469,8 +553,11 @@ def create_artist_submission(): @app.route('/shows') def shows(): - # displays list of shows at /shows - # TODO: replace with real venues data. + data = Show.query.all() + return render_template('pages/shows.html', shows=data) + + + ''' data=[{ "venue_id": 1, "venue_name": "The Musical Hop", @@ -507,25 +594,50 @@ def shows(): "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) - + ''' + @app.route('/shows/create') def create_shows(): # 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 - - # 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') + 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) @app.errorhandler(404) def not_found_error(error): @@ -551,12 +663,16 @@ def server_error(error): #----------------------------------------------------------------------------# # 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/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/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/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/pages/search_artists.html b/templates/pages/search_artists.html index 8c0f98c1c..11e899a82 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 }}

+{{ form.csrf_token }}