@leroysheep / BookAlchemy Mirror · read-only / commits / 8e11516426
Fork 0

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>