Skip to content

Commit

Permalink
upload v3
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulNGilson committed Nov 19, 2024
1 parent daeba17 commit 4ad2f6b
Show file tree
Hide file tree
Showing 27 changed files with 682 additions and 131 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ Giga - the gig booking site that'll be _huge_.
* Create the databases specified in `lib/database_connection.py` - `giga` and `giga_test`
* Seed the production database e.g. `psql -h 127.0.0.1 giga -f seeds/test_gigs.sql -f seeds/test_users.sql -f seeds/test_bookings.sql`
* Run the server with `python app.py`

## Adding Users

Users can be added to the "users" table with values of a username (string) and a
password generated via a hash.

The following example shows steps that can be used to generate a hashed
password, starting with your virtual environment activated:

```python
(giga_venv) % python
>>> from werkzeug.security import generate_password_hash
>>> generate_password_hash("mypassword123")
'scrypt:32768:8:1$IuT0ev03aikfxfuB$724fc6b7123b7eb74b7bc9084b8a0cdd2be087cac401bf93e057191babcd8c5f9b3a93d3e27188bd756b40ac333b026297878d9cc35928099dd454f53f015370'
```

The `scrypt` string can be inserted into the table - see the existing seed file
for reference.
141 changes: 127 additions & 14 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from flask import Flask, request, render_template, redirect, url_for
from lib.database_connection import get_flask_database_connection
from lib.gig_repository import GigRepository
from lib.gig import Gig
from lib.booking_repository import BookingRepository
from flask_login import (
LoginManager,
Expand All @@ -13,6 +14,7 @@
)
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import json

app = Flask(__name__)
app.config.update(
Expand All @@ -34,6 +36,9 @@ def password_valid(self, connection, username, password_attempt):
def get_user_database_id(self, connection, username):
rows = connection.execute('SELECT * FROM users WHERE username = %s', [username])
return rows[0]["id"]
def get_username(self, connection, id):
rows = connection.execute('SELECT * FROM users WHERE id = %s', [id])
return rows[0]["username"]

@login_manager.user_loader
def user_loader(username: str):
Expand All @@ -44,15 +49,34 @@ def user_loader(username: str):
return user_model
return None

def admin_user_required(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if current_user.__dict__.get("id") and current_user.id == "admin":
return func(*args, **kwargs)
else:
return redirect(url_for('get_home'))
return decorated_view

@app.errorhandler(401)
def unauthorised(e):
return render_template('401.html'), 401

@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404



@app.route('/home', methods=['GET'])
def get_home():
return render_template('home.html')
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('home.html', logged_in_as=logged_in_as)

@app.route('/about', methods=['GET'])
def get_about():
return render_template('about.html')
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('about.html', logged_in_as=logged_in_as)

@app.route('/gigs', methods=['GET', 'POST'])
def get_gigs():
Expand All @@ -65,14 +89,15 @@ def get_gigs():
selected_location = "All"
if "location" in request.form.keys():
selected_location = request.form["location"]
date_from = "1900-01-01"
if "date_from" in request.form.keys():
date_from = "2000-01-01"
if "date_from" in request.form.keys() and request.form["date_from"]:
date_from = request.form["date_from"]
date_to = "3000-01-01"
if "date_to" in request.form.keys():
date_to = "2100-01-01"
if "date_to" in request.form.keys() and request.form["date_to"]:
date_to = request.form["date_to"]
gigs = repo.get_by_location_and_dates(selected_location, date_from, date_to)
return render_template('gigs.html', gigs=gigs, locations=locations, selected_location=selected_location, date_from=date_from, date_to=date_to)
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('gigs.html', gigs=gigs, locations=locations, selected_location=selected_location, date_from=date_from, date_to=date_to, logged_in_as=logged_in_as)

@app.route('/gigs/<id>', methods=['GET'])
def get_gig_by_id(id):
Expand All @@ -82,22 +107,32 @@ def get_gig_by_id(id):
logged_in_as = str(current_user.id) if current_user.__dict__.get("id") else None
repo = BookingRepository(connection)
if current_user.__dict__ != {}:
already_booked_gig = gig.id in [booking.gig_id for booking in repo.get_bookings(1)]
user_database_id = User().get_user_database_id(connection, current_user.id)
already_booked_gig = gig.id in [booking.gig_id for booking in repo.get_bookings(user_database_id)]
else:
already_booked_gig = False
return render_template('gig.html', gig=gig, logged_in_as=logged_in_as, already_booked_gig=already_booked_gig)

@app.route("/book_gig/<gig_id>", methods=["POST"])
@app.route('/bands/<band_name>', methods=["GET"])
def get_band_by_name(band_name):
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
gigs = repo.get_by_band_name(band_name)
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('band.html', gigs=gigs, band_name=band_name, logged_in_as=logged_in_as)

@app.route('/book_gig/<gig_id>', methods=["POST"])
@login_required
def post_book_gig(gig_id):
connection = get_flask_database_connection(app)
repo = BookingRepository(connection)
user_database_id = User().get_user_database_id(connection, current_user.id)
repo.make_booking(gig_id, user_database_id, request.form["ticket_count"])
return redirect(url_for('get_account'))

@app.route("/login", methods=["POST"])
@app.route('/login', methods=["POST"])
def post_login():
username = request.form["usernmae"]
username = request.form["username"]
password = request.form["password"]
connection = get_flask_database_connection(app)

Expand All @@ -113,12 +148,14 @@ def post_login():

@app.route('/login', methods=['GET'])
def get_login():
return render_template('login.html')
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('login.html', logged_in_as=logged_in_as)

@app.route('/logout', methods=['GET'])
def get_logout():
logout_user()
return render_template('logout.html')
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('logout.html', logged_in_as=logged_in_as)

@app.route('/account', methods=['GET'])
@login_required
Expand All @@ -136,7 +173,83 @@ def get_account():
"ticket_count": ticket_text,
"gig": repo.get_by_id(booking.gig_id)
})
return render_template('account.html', booking_details=booking_details)
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('account.html', booking_details=booking_details, logged_in_as=logged_in_as)



# Admin routes

@app.route('/admin', methods=["GET"])
@admin_user_required
def get_admin():
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
return render_template('admin.html', logged_in_as=logged_in_as)

@app.route('/admin_add_gig', methods=["POST"])
@admin_user_required
def admin_add_gig():
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
datetime = request.form["datetime"]
band = request.form["band"]
venue = request.form["venue"]
location = request.form["location"]
postcode = request.form["postcode"]
repo.add_gig(Gig(None, datetime, band, venue, location, postcode))
return render_template('admin.html')



# API routes

@app.route('/api')
def api_root():
return "You need to specify a resource such as \"gigs\" via a request like GET /api/&lt;resource&gt;"

@app.route('/api/<resource>')
def api_resource(resource):
match resource:
case "gigs":
if request.method == "GET":
selected_location = "All"
if "location" in request.args.keys():
selected_location = request.args["location"]
date_from = "2000-01-01"
if "date_from" in request.args.keys():
date_from = request.args["date_from"]
date_to = "2100-01-01"
if "date_to" in request.args.keys():
date_to = request.args["date_to"]
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
gigs = repo.get_by_location_and_dates(selected_location, date_from, date_to)
return json.dumps([gig.jsonify() for gig in gigs])
case "bands":
if request.method == "GET":
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
gigs = repo.all()
bands = set([gig.band for gig in gigs])
return json.dumps(list(bands))
case "accounts" | "bookings":
return "You need to specify an Id for this resource via a request like GET /api/&lt;resource&gt;/&lt;Id&gt;"
case _:
return "Unknown API resource: " + resource

@app.route('/api/gigs/<id>')
def api_gig(id):
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
gig = repo.get_by_id(id)
return json.dumps(gig.jsonify())

@app.route('/api/bands/<name>')
def api_band(name):
connection = get_flask_database_connection(app)
repo = GigRepository(connection)
gigs = repo.get_by_band_name(name)
return json.dumps([gig.jsonify() for gig in gigs])

if __name__ == '__main__':
app.run(debug=True, port=int(os.environ.get('PORT', 5001)))
2 changes: 1 addition & 1 deletion lib/database_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def execute(self, query, params=[]):
'in your app.py file (or in your tests)?'

def _check_connection(self):
if self.connection is None:
if not hasattr(self, "connection"):
raise Exception(self.CONNECTION_MESSAGE)

def _database_name(self):
Expand Down
10 changes: 10 additions & 0 deletions lib/gig.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ def __eq__(self, other):

def datetime_pretty(self):
return self.datetime.strftime("%Y-%m-%d %H:%M")

def jsonify(self):
return {
"id": self.id,
"datetime": self.datetime_pretty(),
"band": self.band,
"venue": self.venue,
"location": self.location,
"postcode": self.postcode
}
22 changes: 18 additions & 4 deletions lib/gig_repository.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from lib.gig import Gig
from datetime import date

class GigRepository:
def __init__(self, connection):
self._connection = connection

def all(self):
rows = self._connection.execute('SELECT * FROM gigs')
rows = self._connection.execute('SELECT * FROM gigs ORDER BY datetime')
gigs = []
for row in rows:
gigs.append(Gig(row["id"], row["datetime"], row["band"], row["venue"], row["location"], row["postcode"]))
Expand All @@ -26,14 +27,16 @@ def get_by_location(self, location):
gigs.append(Gig(row["id"], row["datetime"], row["band"], row["venue"], row["location"], row["postcode"]))
return gigs

def get_by_dates(self, date_from="1900-01-01", date_to="3000-01-01"):
rows = self._connection.execute('SELECT * FROM gigs WHERE datetime BETWEEN %s AND %s ORDER BY datetime', [date_from, date_to])
def get_by_dates(self, date_from="2000-01-01", date_to="2100-01-01"):
if date.fromisoformat(date_from) > date.fromisoformat(date_to):
raise Exception("Past cannot be after future")
rows = self._connection.execute('SELECT * FROM gigs WHERE datetime BETWEEN %s AND %s ORDER BY datetime', [date_from + " 00:00", date_to + " 23:59"])
gigs = []
for row in rows:
gigs.append(Gig(row["id"], row["datetime"], row["band"], row["venue"], row["location"], row["postcode"]))
return gigs

def get_by_location_and_dates(self, location, date_from="1900-01-01", date_to="3000-01-01"):
def get_by_location_and_dates(self, location, date_from="2000-01-01", date_to="2100-01-01"):
if location == "All":
gigs_by_location = self.all()
else:
Expand All @@ -44,3 +47,14 @@ def get_by_location_and_dates(self, location, date_from="1900-01-01", date_to="3
if gig in gigs_by_dates:
matches.append(gig)
return matches

def get_by_band_name(self, band_name):
rows = self._connection.execute('SELECT * FROM gigs WHERE band = %s ORDER BY datetime', [band_name])
gigs = []
for row in rows:
gigs.append(Gig(row["id"], row["datetime"], row["band"], row["venue"], row["location"], row["postcode"]))
return gigs

def add_gig(self, gig):
self._connection.execute('INSERT INTO gigs (datetime, band, venue, location, postcode) VALUES (%s, %s, %s, %s, %s)',
[gig.datetime, gig.band, gig.venue, gig.location, gig.postcode])
Binary file added static/images/band.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/front.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified static/images/logo.bmp
Binary file not shown.
Binary file added static/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 43 additions & 5 deletions static/style.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
body {
background-color: #d7d4c9;
}
table {
background-color: #000000;
width: 100%;
#page-content {
margin-left: 20px;
max-width: 850px;
}
table a {
color: #FFFFFF;
h2 {
margin-bottom: 0;
}
#logo {
margin: 15px;
margin-right: 30px;
}
.menu_bar {
background-color: black;
height: 3em;
margin-bottom: 10px; /** spacing below menu, before page content **/
}
.btn-menu{
--bs-btn-color:#fff;
--bs-btn-bg:#c7c4b9;
--bs-btn-border-color:#c7c4b9;
--bs-btn-hover-color:#fff;
--bs-btn-hover-bg:#a7a499;
--bs-btn-hover-border-color:#a7a499;
--bs-btn-active-color:#bfb;
--bs-btn-active-bg:#979489;
--bs-btn-active-border-color:#979489;
--bs-btn-padding-y:0.25rem;
--bs-btn-font-size:0.875rem;
--bs-btn-border-radius:var(--bs-border-radius-sm)
}
.date-picker {
min-width: 160px;
}
.gig {
background-color: #fff;
padding-left: 15px;
padding-top: 15px;
padding-bottom: 1px;
margin-bottom: 20px;
width: 600px;
border-radius: 0.375rem;
}
.login-field {
margin-bottom: 10px;
}
18 changes: 18 additions & 0 deletions templates/401.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>401 Unauthorised</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="/static/style.css" >
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<h2 id="element-fa1b5c0"><img id="logo" src="/static/images/logo.bmp" width="100px" /></h2>
<div id="page-content">
<h1>401 Unauthorised</h1>
<p>You are not allowed to access this page.</p>
<br /><br /><br />
<p>Why don't you try to Log In first...</p>
</div>
</body>
</html>
Loading

0 comments on commit 4ad2f6b

Please sign in to comment.