Mirrored from https://github.com/LeeRoySheep/BookAlchemy.git
Synced Nov 2, 19:30 UTC
Added Bonus 5 and some extras
Lee Roy Stevenson committed
Mar 29, 2025 at 17:02 UTC
8e1151642651ae97066b5e12129f69dcd6dbf80c
13 files changed
+262
-39
.env
new
+1
@@ -0,0 +1 @@
1
+GOOGLE_API_KEY=AIzaSyCW8RMdQbEt_zOLXZhVjk4JAZSQmCRUruE
\ No newline at end of file
ai_request.py
new
+40
@@ -0,0 +1,40 @@
1
+import json
2
+import os
3
+
4
+from dotenv import load_dotenv
5
+from google import genai
6
+
7
+load_dotenv()
8
+
9
+API_KEY = os.getenv("GOOGLE_API_KEY")
10
+client = genai.Client(api_key=API_KEY)
11
+
12
+def ai_request(data_string):
13
+ """
14
+ Function to get an AI request for a book recommendation
15
+ :param data:
16
+ :return:
17
+ """
18
+ response = client.models.generate_content(
19
+ model="gemini-2.0-flash",
20
+ contents="""Can you recommend a book for me based on my ratings of the following dataset,
21
+ with personal ratings form 0 to 10 as float vlaues, please?:
22
+ "dataset":"""+data_string+"""
23
+ Please answer in the json string format , so I can load the request into python.
24
+ Here an example:
25
+ {"book":
26
+ {"title": "some title",
27
+ "author": "some author",
28
+ "year": "some year",
29
+ "isbn": "some isbn with only numbers please",
30
+ "birthday": "author's birthday" as python datetime object with format "YYYY-MM-DD",
31
+ "died": "author's deathday" as pythondatetime object withformat "YYYY-MM-DD" or Null if still alive},
32
+ "reasoning": "your reasoning text"}
33
+ """
34
+ )
35
+
36
+ json_string = response.text.replace("```json", "")
37
+ json_string = json_string.replace("```", "")
38
+
39
+ data = json.loads(json_string)
40
+ return data
app.py
+117
-25
@@ -1,8 +1,9 @@
1
from datetime import datetime
2
from flask import Flask, jsonify,render_template, request
3
-from sqlalchemy import desc
3
4
+from sqlalchemy import desc
5
6
+import ai_request
7
from data_models import db, Author, Book
8
9
# Create Instance of Flask
@@ -11,6 +12,7 @@ app = Flask(__name__)
12
# Configure Database
13
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data/library.sqlite"
14
15
+
16
# Connect Flask to Database
17
db.init_app(app)
18
@@ -28,23 +30,24 @@ def home():
30
sort_by = getattr(Author, 'name',None)
31
elif sort_by == "year":
32
sort_by = getattr(Book, 'publication_year',None)
33
+ elif sort_by == "rating":
34
+ sort_by = getattr(Book, 'rating',None)
35
descending = request.form.get("descending")
36
if descending:
37
books = db.session.query(Book.id,Book.isbn,Book.title,
38
Author.name, Book.author_id,
35
- Book.publication_year).join(Author) \
39
+ Book.publication_year, Book.rating).join(Author) \
40
.order_by(desc(sort_by)).all()
41
else:
42
books = db.session.query(Book.id,Book.isbn,Book.title,
43
Author.name, Book.author_id,
40
- Book.publication_year).join(Author) \
44
+ Book.publication_year, Book.rating).join(Author) \
45
.order_by(sort_by).all()
42
- print(books)
46
return render_template("home.html", books=books)
47
else:
45
- books = db.session.query(Book.id,Book.isbn,Book.title, Author.name,
46
- Book.author_id,Book.publication_year) \
47
- .join(Author).all()
48
+ books = db.session.query(Book.id,Book.isbn,Book.title,
49
+ Author.name, Book.author_id,
50
+ Book.publication_year, Book.rating).join(Author).all()
51
52
return render_template("home.html", books=books)
53
@@ -100,6 +103,7 @@ def add_book():
103
title = request.args.get("title", "")
104
year = request.args.get("year", "")
105
isbn = request.args.get("isbn", "")
106
+ rating = request.args.get("rating", "")
107
if title:
108
books = db.session.query(Book) \
109
.filter(Book.title.contains('%' + title + '%')) \
@@ -107,8 +111,9 @@ def add_book():
111
output = []
112
# Convert query results to Dictionary to return as JSON
113
for book in books:
110
- book = dict({'title': book.title, \
111
- 'year': book.publication_year})
114
+ book = dict({'title': book.title,
115
+ 'year': book.publication_year,
116
+ 'rating': book.rating})
117
output.append(book)
118
return jsonify(output), 200
119
elif year:
@@ -133,6 +138,17 @@ def add_book():
138
'year': book.publication_year})
139
output.append(book)
140
return jsonify(output), 200
141
+ elif rating:
142
+ books = db.session.query(Book) \
143
+ .filter(Book.rating.contains(rating)) \
144
+ .all()
145
+ output = []
146
+ # Convert query results to Dictionary to return as JSON
147
+ for book in books:
148
+ book = dict({'title': book.title, \
149
+ 'year': book.publication_year})
150
+ output.append(book)
151
+ return jsonify(output), 200
152
else:
153
return render_template("add_book.html")
154
### POST ---------------------------------------------------------------
@@ -141,11 +157,13 @@ def add_book():
157
year = request.form["year"]
158
isbn = request.form["isbn"] if request.form["isbn"] else None
159
author = request.form["author"]
160
+ rating = request.form["rating"]
161
try:
162
author_id = db.session.query(Author.id) \
163
.filter(Author.name.contains('%' + author + '%')) \
164
.one()[0]
148
- book = Book(title=title, publication_year=year, isbn=isbn, author_id=author_id)
165
+ book = Book(title=title, publication_year=year,
166
+ isbn=isbn, author_id=author_id , rating=rating)
167
db.session.add(book)
168
db.session.commit()
169
return render_template("add_book.html", success=True)
@@ -153,6 +171,48 @@ def add_book():
171
print(e)
172
return render_template("add_book.html", success=False)
173
174
+# Bonus 5 add recommendation route----------------------------------
175
+@app.route('/add_recommendation', methods=['POST'])
176
+def add_recommendation():
177
+ """
178
+ Route to add AI recommendation via POST
179
+ :return:
180
+ """
181
+ books = db.session.query(Book.id, Book.isbn, Book.title,
182
+ Author.name, Book.author_id, Book.publication_year,
183
+ Book.rating).join(Author).all()
184
+ if request.method != "POST":
185
+ return render_template("add_recommendation.html", books=books)
186
+ author = Author.query.filter(Author.name==request.form["author"]).first()
187
+ if not author:
188
+ birth_date = datetime.strptime(request.form["birthday"], "%Y-%m-%d")
189
+ if request.form["died"]:
190
+ date_of_death = datetime.strptime(request.form["died"], "%Y-%m-%d")
191
+ author = Author(name=request.form["author"],
192
+ birth_date=birth_date,date_of_death=date_of_death)
193
+ db.session.add(author)
194
+ db.session.commit()
195
+
196
+ else:
197
+ author = Author(name=request.form["author"], birth_date=birth_date)
198
+ db.session.add(author)
199
+ db.session.commit()
200
+
201
+ try:
202
+ new_author = Author.query.filter(Author.name==author.name).first()
203
+ book = Book(isbn=request.form["isbn"], title=request.form["title"],
204
+ publication_year=request.form["year"],
205
+ author_id=new_author.id)
206
+ db.session.add(book)
207
+ db.session.commit()
208
+ new_books = db.session.query(Book.id, Book.isbn, Book.title,
209
+ Author.name, Book.author_id, Book.publication_year,
210
+ Book.rating).join(Author).all()
211
+ return render_template("home.html", books=new_books, success=True)
212
+ except Exception as e: # For Debugging and Testing catch all Exceptions
213
+ print("AN ERROR HAS OCCURED: ",e)
214
+ return render_template("home.html", books=books, success=False)
215
+
216
217
@app.route('/search', methods=['POST'])
218
def search():
@@ -171,7 +231,7 @@ def search():
231
return render_template("home.html", error=True)
232
return render_template("home.html", books=books)
233
174
-
234
+# Bonus 2 -----------------------------------------------
235
@app.route('/book/<int:book_id>/delete', methods=['POST'])
236
def delete_book(book_id):
237
"""
@@ -181,8 +241,8 @@ def delete_book(book_id):
241
"""
242
if request.method != "POST":
243
books = db.session.query(Book.id,Book.isbn,Book.title,
184
- Author.name, Book.author_id,
185
- Book.publication_year).join(Author).all()
244
+ Author.name, Book.author_id,
245
+ Book.publication_year, Book.rating).join(Author).all()
246
return render_template("home.html",books=books)
247
book = Book.query.get(book_id)
248
if book:
@@ -195,16 +255,16 @@ def delete_book(book_id):
255
db.session.delete(author)
256
db.session.commit()
257
books = db.session.query(Book.id,Book.isbn,Book.title,
198
- Author.name, Book.author_id,
199
- Book.publication_year).join(Author).all()
258
+ Author.name, Book.author_id,
259
+ Book.publication_year, Book.rating).join(Author).all()
260
return render_template('home.html',books=books,deleted=True)
261
else:
262
books = db.session.query(Book.id,Book.isbn,Book.title,
203
- Author.name, Book.author_id,
204
- Book.publication_year).join(Author).all()
263
+ Author.name, Book.author_id,
264
+ Book.publication_year, Book.rating).join(Author).all()
265
return render_template('home.html',books=books,deleted=False)
266
207
-
267
+# Bonus 2------------------------------------------------
268
@app.route('/author/<int:author_id>/delete', methods=['POST'])
269
def delete_author(author_id):
270
"""
@@ -217,16 +277,16 @@ def delete_author(author_id):
277
db.session.delete(author)
278
db.session.commit()
279
books = db.session.query(Book.id,Book.isbn,Book.title,
220
- Author.name, Book.author_id,
221
- Book.publication_year).join(Author).all()
280
+ Author.name, Book.author_id,
281
+ Book.publication_year, Book.rating).join(Author).all()
282
return render_template('home.html',books=books,auth_deleted=True)
283
else:
284
books = db.session.query(Book.id,Book.isbn,Book.title,
225
- Author.name, Book.author_id,
226
- Book.publication_year).join(Author).all()
285
+ Author.name, Book.author_id,
286
+ Book.publication_year, Book.rating).join(Author).all()
287
return render_template('home.html',books=books,auth_deleted=False)
288
229
-
289
+# BONUS 3------------------------------------------------
290
@app.route('/author/<int:author_id>', methods=['GET'])
291
def author(author_id):
292
"""
@@ -246,11 +306,43 @@ def book(book_id):
306
:return:
307
"""
308
book = db.session.query(Book.isbn,Book.title, \
249
- Book.publication_year,Author.name) \
309
+ Book.publication_year,Author.name, Book.rating) \
310
.join(Author).filter(Book.id == book_id).one()._mapping
251
- print(book)
311
return render_template("details_book.html", book=book)
312
313
+## BONUS 4-------------------------------------------
314
+@app.route('/book/rating/<int:book_id>', methods=['POST'])
315
+def rate_book(book_id):
316
+ """
317
+ Route to rate book with given id
318
+ :param book_id:
319
+ :return:
320
+ """
321
+ rating = request.form['rating']
322
+ book = Book.query.get(book_id)
323
+ book.rating = rating
324
+ db.session.commit()
325
+ books = db.session.query(Book.id,Book.isbn,Book.title,
326
+ Author.name, Book.author_id,
327
+ Book.publication_year, Book.rating).join(Author).all()
328
+ return render_template('home.html',books=books, rated=True)
329
+
330
+
331
+# Bonus 5 ------------------------------------------------
332
+@app.route("/get_ai_recommendation", methods=["GET"])
333
+def get_ai_recommendation():
334
+ """
335
+ Route to get AI recommendation
336
+ :return:
337
+ """
338
+ books = db.session.query( Book.title, Book.publication_year,
339
+ Author.name, Book.rating).join(Author).all()
340
+ dataset = ""
341
+ for book in books:
342
+ dataset += str(book)
343
+ return render_template("ai_recomendation.html",
344
+ recomendation=ai_request.ai_request(dataset))
345
+
346
app.run(debug=True)
347
348
# Only needed to create datatables at the beginning of the project
data_models.py
+11
-1
@@ -9,6 +9,13 @@ class Author(db.Model):
9
birth_date = db.Column(db.Date)
10
date_of_death = db.Column(db.Date)
11
12
+ books = db.relationship(
13
+ 'Book',
14
+ back_populates='author',
15
+ cascade='all, delete-orphan',
16
+ passive_deletes=True
17
+ )
18
+
19
20
def __str__(self):
21
output = f'Name: {self.name}, Birth Date: {self.birth_date}'
@@ -21,7 +28,10 @@ class Book(db.Model):
28
isbn = db.Column(db.String(13))
29
title = db.Column(db.String(100))
30
publication_year = db.Column(db.Integer)
24
- author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False)
31
+ author_id = db.Column(db.Integer, db.ForeignKey('authors.id', ondelete='CASCADE'), nullable=False)
32
+ rating = db.Column(db.Float, default=0.0)
33
+
34
+ author = db.relationship('Author', back_populates='books')
35
36
def __str__(self):
37
output = f'Title: {self.title}, Year: {self.year}'
instance/data/library.sqlite
Binary files a/instance/data/library.sqlite and b/instance/data/library.sqlite differ
instance/data/library2.sqlite
Binary files a/instance/data/library2.sqlite and b/instance/data/library2.sqlite differ
requirements.txt
+1
@@ -2,3 +2,4 @@ flask
2
flask-sqlalchemy
3
jinja2
4
sqlalchemy
5
+google-api-python-client
static/style.css
+18
-3
@@ -67,15 +67,16 @@ input, select {
67
border-radius: 5px;
68
border: 1px solid #ccc;
69
}
70
-input[type="submit"], .delete-btn {
70
+input[type="submit"], .delete-btn, button {
71
background: #e74c3c;
72
color: white;
73
border: none;
74
padding: 8px 12px;
75
cursor: pointer;
76
transition: 0.3s;
77
+ border-radius: 5px;
78
}
78
-input[type="submit"]:hover, .delete-btn:hover {
79
+input[type="submit"]:hover, .delete-btn:hover, button:hover {
80
background: #c0392b;
81
}
82
@@ -94,16 +95,26 @@ input[type="submit"]:hover, .delete-btn:hover {
95
}
96
97
/* Book Cards */
97
-.book-card {
98
+.book-card, .book-card2 {
99
background: white;
100
padding: 15px;
101
border-radius: 8px;
102
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
103
transition: 0.3s;
104
}
105
+.book-card2 {
106
+ margin: 0 auto;
107
+ align-items: center;
108
+ text-align: center;
109
+ display: flex;
110
+ flex-direction: column;
111
+ justify-content: center;
112
+ width: 70%
113
+}
114
.book-card:hover {
115
transform: scale(1.05);
116
}
117
+
118
.book-card h3 {
119
font-size: 1.2rem;
120
color: #2c3e50;
@@ -132,6 +143,10 @@ input[type="submit"]:hover, .delete-btn:hover {
143
color: black;
144
transition: 0.2s;
145
}
146
+img:hover {
147
+ transform: rotate(2160deg);
148
+ transition: 1.6s;
149
+}
150
151
/* Error Messages */
152
.error {
templates/add_book.html
+3
@@ -28,6 +28,9 @@
28
<label for="author">Author:</label>
29
<input type="text" id="author" name="author" required><br><br>
30
31
+ <label for="rating">Rating:</label>
32
+ <input type="number" id="rating" name="rating" step="0.1" min="0.0" max="10.0"><br><br>
33
+
34
<input type="submit" value="Add Book">
35
</form>
36
<a href="/"><button>Home</button></a>
templates/ai_recomendation.html
new
+43
@@ -0,0 +1,43 @@
1
+<!DOCTYPE html>
2
+<html lang="en" xmlns="http://www.w3.org/1999/html">
3
+<head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ recomendation['book']['title'] }}</title>
7
+ <link rel="stylesheet" href="/static/style.css">
8
+</head>
9
+<body>
10
+
11
+ <header class="title-container">
12
+ <h1>🤖 AI Recomendation: <strong>{{ recomendation['book']['title'] }} </strong></h1>
13
+ <h2>📖 Written by: <strong>{{ recomendation['book']['author'] }}</strong></h2>
14
+ <nav>
15
+ <form action="/add_recommendation" method="POST">
16
+ <input type="hidden" name="isbn" value="{{ recomendation['book']['isbn'] }}">
17
+ <input type="hidden" name="title" value="{{ recomendation['book']['title'] }}">
18
+ <input type="hidden" name="author" value="{{ recomendation['book']['author'] }}">
19
+ <input type="hidden" name="year" value="{{ recomendation['book']['year'] }}">
20
+ <input type="hidden" name="birthday" value="{{ recomendation['book']['birthday'] }}">
21
+ {% if recomendation['book']['died'] %}
22
+ <input type="hidden" name="died" value="{{ recomendation['book']['died'] }}">
23
+ {% endif %}
24
+ <button type="submit" class="btn btn-primary">✨ Add to My Library</button>
25
+ </form>
26
+ <a href="/get_ai_recommendation" class="btn btn-primary"><button>🔄 New Recomendation</button></a>
27
+ </nav>
28
+ </header>
29
+ <section class="book-card2">
30
+ <p class="details">🗓 Published: {{ recomendation['book']['year'] }}</p>
31
+ <p class="details">📚 ISBN: {{ recomendation['book']['isbn'] }}</p>
32
+ <a href="https://openlibrary.org/isbn/{{ recomendation['book']['isbn'] }}" target="_blank">
33
+ <img src="https://covers.openlibrary.org/b/isbn/{{ recomendation['book']['isbn'] }}-M.jpg" class="cover" alt="Book Cover">
34
+ </a><br>
35
+ <div class="description">Reasoning: {{ recomendation['reasoning'] }} </div> <br>
36
+
37
+ <footer class="botton-container">
38
+ <a href="/" class="btn btn-secondary btn-back"><button>⬅ Back to Home</button></a>
39
+ </footer>
40
+ </section>
41
+
42
+</body>
43
+</html>
templates/details_author.html
+2
-2
@@ -7,8 +7,8 @@
7
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
8
<style>
9
body {
10
- background-color: #f8f9fa;
11
- padding: 40px;
10
+ background-color: #111111;
11
+ padding: 5%;
12
}
13
.container {
14
max-width: 600px;
templates/details_book.html
+9
-8
@@ -7,13 +7,13 @@
7
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
8
<style>
9
body {
10
- background-color: #f8f9fa;
11
- padding: 40px;
10
+ background-color: #111111;
11
+ padding: 5%;
12
}
13
.container {
14
max-width: 600px;
15
background: white;
16
- padding: 20px;
16
+ padding: 10px;
17
border-radius: 8px;
18
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
19
text-align: center;
@@ -23,16 +23,16 @@
23
}
24
.details {
25
font-size: 18px;
26
- margin-top: 15px;
26
+ margin-top: 10px;
27
}
28
.cover {
29
- margin: 20px 0;
30
- max-width: 100%;
29
+ margin: 15px 0;
30
+ max-width: 95%;
31
border-radius: 8px;
32
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
33
}
34
.btn-back {
35
- margin-top: 20px;
35
+ margin-top: 8px;
36
text-decoration: none;
37
}
38
</style>
@@ -48,8 +48,9 @@
48
{% if book.isbn %}
49
<a href="https://openlibrary.org/isbn/{{ book.isbn }}" target="_blank">
50
<img src="https://covers.openlibrary.org/b/isbn/{{ book.isbn }}-M.jpg" class="cover" alt="Book Cover">
51
- </a><br><br>
51
+ </a><br>
52
{% endif %}
53
+ <p class="details">⭐ Rating: {{ book.rating if book.rating else "N/A" }} / 10</p>
54
55
<a href="/" class="btn btn-secondary btn-back">⬅ Back to Home</a>
56
</div>
templates/home.html
+17
@@ -15,6 +15,7 @@
15
<ul class="nav-links">
16
<li><a href="/add_author">➕ Add Author</a></li>
17
<li><a href="/add_book">📖 Add Book</a></li>
18
+ <li><a href="/get_ai_recommendation">🤖 AI Recommendation</a></li>
19
</ul>
20
<div class="actions">
21
<form action="/" method="POST">
@@ -23,6 +24,7 @@
24
<option value="title">Title</option>
25
<option value="author">Author</option>
26
<option value="year">Year</option>
27
+ <option value="rating">Rating</option>
28
</select>
29
<label for="descending">Descending:</label>
30
<input type="checkbox" id="descending" name="descending" value="descending">
@@ -41,7 +43,15 @@
43
<section class="books">
44
{% if error %}
45
<h2 class="error">⚠️ No Books Found!</h2>
46
+ {% elif success %}
47
+ <h2 class="success">✅ Book added successfully!</h2>
48
+ {% elif success == False %}
49
+ <h2 class="error">❌ Failed to add book!</h2>
50
{% endif %}
51
+ {% if rated %}
52
+ <h2 class="success">✅ Book rated successfully!</h2>
53
+ {% endif %}
54
+
55
{% if deleted %}
56
<h2 class="success">✅ Book deleted successfully!</h2>
57
{% elif deleted == False %}
@@ -66,6 +76,13 @@
76
<img src="https://covers.openlibrary.org/b/isbn/{{ book.isbn }}-M.jpg" alt="Book Cover">
77
</a>
78
{% endif %}
79
+ <footer>
80
+ <form action="/book/rating/{{ book.id }}" method="POST">
81
+ <label for="rating">Rating:</label>
82
+ <input type="number" id="rating" name="rating" step="0.1" min="0.0" max="10.0" value="{{ book.rating }}" required>
83
+ <input type="submit" value="Rate">
84
+ </form>
85
+ </footer>
86
<form action="/book/{{ book.id }}/delete" method="POST">
87
<button class="delete-btn">🗑 Delete Book</button>
88
</form>