Skip to content

Commit 2aa2335

Browse files
committed
release v4
1 parent 5139b95 commit 2aa2335

22 files changed

Lines changed: 437 additions & 151 deletions

app.py

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ def password_valid(self, connection, username, password_attempt):
3333
if not self.signed_up(connection, username):
3434
return False
3535
rows = connection.execute('SELECT * FROM users WHERE username = %s', [username])
36-
return check_password_hash(rows[0]["hashed_password"], password_attempt) != username
36+
return check_password_hash(rows[0]["hashed_password"], password_attempt)
3737
def get_user_database_id(self, connection, username):
3838
rows = connection.execute('SELECT * FROM users WHERE username = %s', [username])
3939
return rows[0]["id"]
4040
def get_username(self, connection, id):
4141
rows = connection.execute('SELECT * FROM users WHERE id = %s', [id])
4242
return rows[0]["username"]
43+
def sign_up(self, connection, username, password):
44+
hashed_password = generate_password_hash(password)
45+
connection.execute('INSERT INTO users (username, hashed_password) VALUES (%s, %s)', [username, hashed_password])
4346

4447
@login_manager.user_loader
4548
def user_loader(username: str):
@@ -69,6 +72,10 @@ def page_not_found(e):
6972

7073

7174

75+
@app.route('/', methods=['GET'])
76+
def get_root():
77+
return redirect(url_for('get_home'))
78+
7279
@app.route('/home', methods=['GET'])
7380
def get_home():
7481
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
@@ -83,10 +90,11 @@ def get_about():
8390
def get_gigs():
8491
connection = get_flask_database_connection(app)
8592
repo = GigRepository(connection)
86-
locations = ["All"]
93+
venue_locations = [("", "All")]
8794
for gig in repo.all():
88-
if gig.location not in locations:
89-
locations.append(gig.location)
95+
if (gig.venue, gig.location) not in venue_locations:
96+
venue_locations.append((gig.venue, gig.location))
97+
locations = [vl[1] for vl in venue_locations]
9098
selected_location = "All"
9199
if "location" in request.form.keys():
92100
selected_location = request.form["location"]
@@ -126,16 +134,28 @@ def get_band_by_name(band_name):
126134
@app.route('/book_gig/<gig_id>', methods=["POST"])
127135
@login_required
128136
def post_book_gig(gig_id):
137+
try:
138+
int(request.form["ticket_count"])
139+
except:
140+
return render_template('400.html', reason="Non-integer ticket number requested"), 400
129141
if int(request.form["ticket_count"]) < 1:
130-
return "Ticket number must be at least 1"
142+
return render_template('400.html', reason="Ticket number must be at least 1"), 400
131143
if int(request.form["ticket_count"]) > 8:
132-
return "A user can't book more than 8 tickets for one gig"
144+
return render_template('400.html', reason="A user can't book more than 8 tickets for one gig"), 400
133145
connection = get_flask_database_connection(app)
134146
repo = BookingRepository(connection)
135147
user_database_id = User().get_user_database_id(connection, current_user.id)
136148
repo.make_booking(gig_id, user_database_id, request.form["ticket_count"])
137149
return redirect(url_for('get_account'))
138150

151+
@app.route('/cancel_booking/<booking_id>', methods=["GET"])
152+
@login_required
153+
def get_cancel_booking(booking_id):
154+
connection = get_flask_database_connection(app)
155+
repo = BookingRepository(connection)
156+
repo.cancel_booking(booking_id)
157+
return redirect(url_for('get_account'))
158+
139159
@app.route('/login', methods=["POST"])
140160
def post_login():
141161
username = request.form["username"]
@@ -149,19 +169,50 @@ def post_login():
149169
login_user(user_model)
150170
return redirect(url_for('get_home'))
151171
else:
152-
return "Wrong credentials"
153-
return "Unknown user"
172+
return render_template('401.html', reason="Wrong credentials"), 401
173+
return render_template('401.html', reason="Unknown user"), 401
154174

155175
@app.route('/login', methods=['GET'])
156176
def get_login():
177+
signup_message = None
178+
if "signup_message" in request.args.keys():
179+
signup_message = request.args["signup_message"]
157180
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
158-
return render_template('login.html', logged_in_as=logged_in_as)
181+
return render_template('login.html', logged_in_as=logged_in_as, signup_message=signup_message)
182+
183+
def password_complexity(password):
184+
return len(password) > 7 and any(char in password for char in "!@$%&")
185+
186+
@app.route('/signup', methods=['GET', 'POST'])
187+
def get_signup():
188+
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
189+
if logged_in_as:
190+
return redirect(url_for('get_home'))
191+
username_error = None
192+
password_error = None
193+
username = ""
194+
if request.method == "POST":
195+
username = request.form["username"]
196+
password = request.form["password"]
197+
confirm_password = request.form["confirm_password"]
198+
if password != confirm_password:
199+
password_error = "Passwords did not match"
200+
elif not password_complexity(password):
201+
password_error = "Password complexity requirements not met"
202+
elif username == "":
203+
username_error = "Username cannot be blank"
204+
elif username.lower() == "admin":
205+
username_error = "Username cannot be 'admin' (or similar)"
206+
if not username_error and not password_error:
207+
connection = get_flask_database_connection(app)
208+
User().sign_up(connection, username, password)
209+
return redirect(url_for('get_login', signup_message=[True]))
210+
return render_template('signup.html', username_error=username_error, password_error=password_error, username=username)
159211

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

166217
@app.route('/account', methods=['GET'])
167218
@login_required
@@ -177,7 +228,8 @@ def get_account():
177228
ticket_text = f"{booking.ticket_count} tickets" if booking.ticket_count > 1 else f"{booking.ticket_count} ticket"
178229
booking_details.append({
179230
"ticket_count": ticket_text,
180-
"gig": repo.get_by_id(booking.gig_id)
231+
"gig": repo.get_by_id(booking.gig_id),
232+
"id": booking.id
181233
})
182234
logged_in_as = ", " + str(current_user.id) if current_user.__dict__.get("id") else None
183235
return render_template('account.html', booking_details=booking_details, logged_in_as=logged_in_as)
@@ -211,7 +263,7 @@ def admin_add_gig():
211263

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

216268
@app.route('/api/<resource>')
217269
def api_resource(resource):
@@ -239,16 +291,19 @@ def api_resource(resource):
239291
bands = set([gig.band for gig in gigs])
240292
return json.dumps(list(bands))
241293
case "accounts" | "bookings":
242-
return "You need to specify an Id for this resource via a request like GET /api/&lt;resource&gt;/&lt;Id&gt;"
294+
return "Specify an Id for this resource via a request like GET /api/&lt;resource&gt;/&lt;Id&gt;"
243295
case _:
244-
return "Unknown API resource: " + resource
296+
return "Unknown API resource: " + resource, 404
245297

246298
@app.route('/api/gigs/<id>')
247299
def api_gig(id):
248300
connection = get_flask_database_connection(app)
249301
repo = GigRepository(connection)
250302
gig = repo.get_by_id(id)
251-
return json.dumps(gig.jsonify())
303+
try:
304+
return json.dumps(gig.jsonify())
305+
except:
306+
return json.dumps({}), 404
252307

253308
@app.route('/api/bands/<name>')
254309
def api_band(name):
@@ -257,5 +312,53 @@ def api_band(name):
257312
gigs = repo.get_by_band_name(name)
258313
return json.dumps([gig.jsonify() for gig in gigs])
259314

315+
@app.route('/api/accounts/<id>')
316+
@login_required
317+
def api_account(id):
318+
connection = get_flask_database_connection(app)
319+
repo = BookingRepository(connection)
320+
bookings = repo.get_bookings(id)
321+
try:
322+
username = User().get_username(connection, id)
323+
return json.dumps({
324+
"bookings": [booking.jsonify() for booking in bookings],
325+
"username": username
326+
})
327+
except:
328+
return json.dumps({}), 404
329+
330+
@app.route('/api/bookings/<id>', methods=["GET"])
331+
@admin_user_required
332+
def api_booking(id):
333+
connection = get_flask_database_connection(app)
334+
repo = BookingRepository(connection)
335+
booking = repo.get_by_id(id)
336+
try:
337+
return json.dumps(booking.jsonify())
338+
except:
339+
return json.dumps({}), 404
340+
341+
@app.route('/api/bookings', methods=["POST"])
342+
@admin_user_required
343+
def api_post_booking():
344+
connection = get_flask_database_connection(app)
345+
repo = BookingRepository(connection)
346+
try:
347+
gig_id = request.form["gig_id"]
348+
user_id = request.form["user_id"]
349+
ticket_count = request.form["ticket_count"]
350+
repo.make_booking(gig_id, user_id, ticket_count)
351+
return "", 200
352+
except:
353+
return "POST failed", 400
354+
355+
@app.route('/api/bookings/<id>', methods=["DELETE"])
356+
@admin_user_required
357+
def api_delete_booking(id):
358+
connection = get_flask_database_connection(app)
359+
repo = BookingRepository(connection)
360+
booking = repo.cancel_booking(id)
361+
return "", 200
362+
260363
if __name__ == '__main__':
261364
app.run(debug=True, port=int(os.environ.get('PORT', 5001)))

lib/booking.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,15 @@ def __init__(self, id, dt, gig_id, user_id, ticket_count):
1818

1919
def __eq__(self, other):
2020
return self.__dict__ == other.__dict__
21+
22+
def datetime_pretty(self):
23+
return self.datetime.strftime("%Y-%m-%d %H:%M")
24+
25+
def jsonify(self):
26+
return {
27+
"id": self.id,
28+
"datetime": self.datetime_pretty(),
29+
"gig_id": self.gig_id,
30+
"user_id": self.user_id,
31+
"ticket_count": self.ticket_count
32+
}

lib/booking_repository.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ def get_bookings(self, user_id):
1212
bookings.append(Booking(row["id"], row["datetime"], row["gig_id"], row["user_id"], row["ticket_count"]))
1313
return bookings
1414

15+
def get_by_id(self, booking_id):
16+
rows = self._connection.execute('SELECT * FROM bookings WHERE id = %s', [booking_id])
17+
if rows == []:
18+
return None
19+
else:
20+
row = rows[0]
21+
return Booking(row["id"], row["datetime"], row["gig_id"], row["user_id"], row["ticket_count"])
22+
1523
def make_booking(self, gig_id, user_id, ticket_count):
1624
booking_datetime_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
1725
self._connection.execute('INSERT INTO bookings (datetime, gig_id, user_id, ticket_count) VALUES (%s, %s, %s, %s)', [booking_datetime_str, gig_id, user_id, ticket_count])
26+
27+
def cancel_booking(self, booking_id):
28+
self._connection.execute('DELETE FROM bookings WHERE id = %s', [booking_id])

lib/gig_repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def get_by_location_and_dates(self, location, date_from="2000-01-01", date_to="2
4949
return matches
5050

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

static/style.css

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,29 @@ body {
55
margin-left: 20px;
66
max-width: 850px;
77
}
8-
h2 {
8+
h1 {
9+
font-size: 2em !important;
910
margin-bottom: 0;
1011
}
1112
#logo {
1213
margin: 15px;
1314
margin-right: 30px;
1415
}
1516
.menu_bar {
16-
background-color: black;
17+
background-color: rgb(38, 68, 73);
1718
height: 3em;
1819
margin-bottom: 10px; /** spacing below menu, before page content **/
1920
}
2021
.btn-menu{
2122
--bs-btn-color:#fff;
22-
--bs-btn-bg:#c7c4b9;
23-
--bs-btn-border-color:#c7c4b9;
23+
--bs-btn-bg:#777469;
24+
--bs-btn-border-color:#777469;
2425
--bs-btn-hover-color:#fff;
25-
--bs-btn-hover-bg:#a7a499;
26-
--bs-btn-hover-border-color:#a7a499;
27-
--bs-btn-active-color:#bfb;
28-
--bs-btn-active-bg:#979489;
29-
--bs-btn-active-border-color:#979489;
26+
--bs-btn-hover-bg:#575449;
27+
--bs-btn-hover-border-color:#575449;
28+
--bs-btn-active-color:#8ac2d4;
29+
--bs-btn-active-bg:#474439;
30+
--bs-btn-active-border-color:#474439;
3031
--bs-btn-padding-y:0.25rem;
3132
--bs-btn-font-size:0.875rem;
3233
--bs-btn-border-radius:var(--bs-border-radius-sm)
@@ -45,4 +46,11 @@ h2 {
4546
}
4647
.login-field {
4748
margin-bottom: 10px;
49+
}
50+
a {
51+
color: #1d7d9c;
52+
text-decoration: underline;
53+
}
54+
* {
55+
font-family: 'Gill Sans';
4856
}

templates/400.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>400 Client Error</title>
5+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
6+
<link rel="stylesheet" href="/static/style.css" >
7+
</head>
8+
<body>
9+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
10+
<h1>{% include 'logo_link.html' %}</h1>
11+
<div id="page-content">
12+
<h2>400 Client Error</h2>
13+
{% if reason %}<p>Reason: {{ reason }}</p>{% endif %}
14+
<br /><br /><br />
15+
<p>Go <a href="javascript:window.history.back();">Back</a> and try again!</p>
16+
</div>
17+
</body>
18+
</html>

templates/401.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
<!DOCTYPE html>
2-
<html>
2+
<html lang="en">
33
<head>
44
<title>401 Unauthorised</title>
55
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
66
<link rel="stylesheet" href="/static/style.css" >
77
</head>
88
<body>
99
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
10-
<h2 id="element-fa1b5c0"><img id="logo" src="/static/images/logo.bmp" width="100px" /></h2>
10+
<h1>{% include 'logo_link.html' %}</h1>
1111
<div id="page-content">
12-
<h1>401 Unauthorised</h1>
12+
<h2>401 Unauthorised</h2>
1313
<p>You are not allowed to access this page.</p>
14+
{% if reason %}<p>Reason: {{ reason }}</p>{% endif %}
1415
<br /><br /><br />
15-
<p>Why don't you try to Log In first...</p>
16+
<p>Why don't you try to <a href="/login">Log In</a> first...</p>
1617
</div>
1718
</body>
1819
</html>

templates/404.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<!DOCTYPE html>
2-
<html>
2+
<html lang="en">
33
<head>
44
<title>404 Not Found</title>
55
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
66
<link rel="stylesheet" href="/static/style.css" >
77
</head>
88
<body>
99
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
10-
<h2 id="element-234ce119"><img id="logo" src="/static/images/logo.bmp" width="100px" /></h2>
10+
<h1>{% include 'logo_link.html' %}</h1>
1111
<div id="page-content">
1212
<h1>404 Not Found</h1>
1313
<p>I'm sorry to inform you that you have sailed off the edge of the world.</p>

0 commit comments

Comments
 (0)