@leroysheep / MovieWebApp / commits / f8f69eda67

got all buttons working

Lee Roy Stevenson committed May 20, 2025 at 00:05 UTC f8f69eda670272149298a784459f35d3e8a69286
12 files changed +297 -125
app.py
+73 -15
@@ -1,5 +1,4 @@
1 -
2 -
1 +from django.contrib.messages import success
2 from flask import Flask, render_template, request
3 from datamanager.sqlite_data_manager import SQliteDataManager
4 from data_models import User, Movie, UserMovie
@@ -33,16 +32,59 @@ def user_movies(user_id):
32 if movie is None:
33 try:
34 movie = data_manager.set_movie(movie_title)
36 - session.add(movie)
37 - data_manager.set_user_movies(user_id=user_id, movie_id=movie.id, rating=movie.rating)
38 - session.commit()
35 + if movie is None:
36 + raise Exception("Movie not found")
37 + data_manager.set_user_movies(user_id=user_id, movie_id=movie.id)
38 + user_movies_list = data_manager.get_user_movies(user_id)
39 + return render_template("user_movies.html",
40 + user_movies=user_movies_list,
41 + user=chosen_user, success=True)
42 + except Exception as e:
43 + session.rollback()
44 + return render_template("user_movies.html",
45 + user_movies=user_movies_list,
46 + user=chosen_user, success=False)
47 + else:
48 + if data_manager.get_user_movie(user_id, movie.id):
49 + return render_template("user_movies.html",
50 + user_movies=data_manager.get_user_movies(user_id),
51 + user=chosen_user, success=False)
52 + try:
53 + data_manager.set_user_movies(user_id=user_id, movie_id=movie.id)
54 user_movies_list = data_manager.get_user_movies(user_id)
40 - return render_template("user_movies.html", user_movies=user_movies_list,
41 - user=chosen_user, success=True)
55 + return render_template("user_movies.html",
56 + user_movies=user_movies_list,
57 + user=chosen_user, success=True)
58 except Exception as e:
59 session.rollback()
44 - return render_template("user_movies.html", user_movies=user_movies_list,
45 - user=chosen_user, success=False)
60 + return render_template("user_movies.html",
61 + user_movies=user_movies_list,
62 + user=chosen_user, success=False)
63 +
64 +
65 +@app.route('/users/<user_id>/<movie_id>', methods=["GET", "POST"])
66 +def update_user_movie(user_id, movie_id):
67 + if request.method == "GET":
68 + chosen_user = data_manager.get_user(user_id)
69 + movie = data_manager.get_movie(movie_id)
70 + user_movie = data_manager.get_user_movie(user_id, movie_id)
71 + return render_template("user_movie.html", user_movie=user_movie,
72 + user=chosen_user, movie=movie)
73 + elif request.method == "POST":
74 + chosen_user = data_manager.get_user(user_id)
75 + movie = data_manager.get_movie(movie_id)
76 + user_movie = data_manager.get_user_movie(user_id, movie_id)
77 + user_rating = request.form["user_rating"]
78 + try:
79 + data_manager.update_user_movie(user_id=user_id, movie_id=movie_id,
80 + update_data={"user_rating": user_rating})
81 + new_user_movie = data_manager.get_user_movie(user_id, movie_id)
82 + return render_template("user_movie.html", user_movie=new_user_movie,
83 + user=chosen_user, movie=movie, success=True)
84 + except Exception as e:
85 + return render_template("user_movie.html", user_movie=user_movie,
86 + user=chosen_user, movie=movie, success=False)
87 +
88
89
90 @app.route('/users/new', methods=["GET", "POST"])
@@ -70,10 +112,10 @@ def new_movie():
112 title = request.form["name"]
113 movie = data_manager.set_movie(title)
114 if movie:
73 - success = True
115 + movie_added = True
116 else:
75 - success = False
76 - return render_template("new_movie.html", movie=movie, success=success)
117 + movie_added = False
118 + return render_template("new_movie.html", movie=movie, success=movie_added)
119
120 @app.route('/movies', methods=["GET", "POST"])
121 def list_movies():
@@ -82,11 +124,27 @@ def list_movies():
124 return render_template("movies.html",movies=movies)
125 elif request.method == "POST":
126 movie_id = request.form["movie_id"]
85 - data_manager.delete_movie(movie_id)
127 + deleted = data_manager.delete_movie(int(movie_id))
128 movies = data_manager.movies
87 - return render_template("movies.html",movies=movies)
129 + return render_template("movies.html", movies=movies, success=deleted)
130 +
131 +@app.route('/users/<user_id>/delete/<movie_id>', methods=["GET", "POST"])
132 +def delete_user_movie(user_id, movie_id):
133 + if request.method == "GET":
134 + chosen_user = data_manager.get_user(user_id)
135 + movie = data_manager.get_movie(movie_id)
136 + user_movie = data_manager.get_user_movie(user_id, movie_id)
137 + return render_template("user_movie.html", user_movie=user_movie,
138 + user=chosen_user, movie=movie)
139 + elif request.method == "POST":
140 + deleted = data_manager.delete_user_movie(user_id, movie_id)
141 + chosen_user = data_manager.get_user(user_id)
142 + movie = data_manager.get_movie(movie_id)
143 + user_movies_list = data_manager.get_user_movies(user_id)
144 + return render_template("user_movies.html", user_movies=user_movies_list,
145 + user=chosen_user, movie=movie, movie_deleted=deleted)
146
147 if __name__ == "__main__":
148
91 - app.run(debug=True, host="127.0.0.1",port=5000)
149 + app.run(debug=True,port=5000)
150
datamanager/sqlite_data_manager.py
+81 -36
@@ -1,6 +1,6 @@
1
2 from sqlalchemy import create_engine
3 -from sqlalchemy.orm import sessionmaker, joinedload, Session
3 +from sqlalchemy.orm import sessionmaker
4 from contextlib import contextmanager
5 from typing import List, Dict, Any, Type
6
@@ -21,7 +21,7 @@ class SQliteDataManager(DataManagerInterface):
21 Initialize the data manager with a database URL.
22 """
23 self.engine = create_engine(db_url)
24 - self.SessionFactory = sessionmaker(bind=self.engine)
24 + self.SessionFactory = sessionmaker(bind=self.engine, expire_on_commit=False)
25 Base.metadata.create_all(self.engine)
26
27 @contextmanager
@@ -74,11 +74,11 @@ class SQliteDataManager(DataManagerInterface):
74 Getter for movies.
75 Returns: a list of Movie objects
76 """
77 - with self.SessionFactory() as db:
78 - return db.query(Movie).options(joinedload(Movie.users)).all()
77 + with self.SessionFactory() as session:
78 + return session.query(Movie).all()
79
80 def set_user_movies(self, user_id: int, movie_id: int, user_rating: float = 0.0)\
81 - -> None:
81 + -> str | None:
82 """
83 Set (add) a movie to a user's list with a rating,
84 either create a new association or update the rating if it exists (self-contained session).
@@ -87,13 +87,11 @@ class SQliteDataManager(DataManagerInterface):
87 with self.SessionFactory() as session:
88 user = session.query(User).filter_by(id=user_id).first()
89 movie = session.query(Movie).filter_by(id=movie_id).first()
90 - print(f"user:{user}, movie:{movie}")
90
92 - if user is not None and movie is not None:
91 + if user and movie:
92 existing_associations = session.query(UserMovie).filter_by(
93 user_id=user_id, movie_id=movie_id
95 - ).all() #check if the association already exists
96 - print(f"existing_associations: {existing_associations}")
94 + ).first() #check if the association already exists
95 if existing_associations:
96 # existing association update
97 session.query(UserMovie).filter_by(user_id=user_id, movie_id=movie_id).update(
@@ -103,18 +101,16 @@ class SQliteDataManager(DataManagerInterface):
101 # create new association
102 association = UserMovie(user_id=user_id, movie_id=movie_id,
103 user_rating=user_rating)
106 - print(f"association: {association}")
104 session.add(association)
108 - print(f"Added movie {movie_id} to user {user_id} with rating {user_rating}")
105
106 session.commit() # Commit within the function
107
108
113 - elif user is not None and movie is None:
114 - print("Failed to add movie!")
109 + elif movie is None:
110 + return "Failed to add movie, check ID and try again!"
111
112 else:
117 - print("User or movie not found.")
113 + return "User not found."
114
115 def get_user_movies(self, user_id: int) -> List[Dict[str, Any]]:
116 """
@@ -135,12 +131,30 @@ class SQliteDataManager(DataManagerInterface):
131 "director": movie.director,
132 "year": movie.year,
133 "poster": movie.poster,
138 - "user_rating": association.user_rating,
134 + "rating": movie.rating,
135 + "user_rating": association.user_rating
136 })
137 return movies_with_ratings
138 return []
139
140
141 + def get_user_movie(self, user_id: int, movie_id: int) -> Dict[str, Any]:
142 + with self.SessionFactory() as session:
143 + user_movie = session.query(UserMovie).filter_by(user_id=user_id, movie_id=movie_id).first()
144 + if user_movie:
145 + movie = session.query(Movie).filter_by(id=movie_id).first()
146 + return {
147 + "id": movie.id,
148 + "name": movie.name,
149 + "director": movie.director,
150 + "year": movie.year,
151 + "poster": movie.poster,
152 + "rating": movie.rating,
153 + "user_rating": user_movie.user_rating,
154 + }
155 + return {}
156 +
157 +
158 def set_movie(self, movie_title: str) -> Type[Movie] | Movie:
159 """
160 Add a new movie to the database.
@@ -148,7 +162,7 @@ class SQliteDataManager(DataManagerInterface):
162 """
163 with self.SessionFactory() as session:
164 movie = session.query(Movie).filter_by(name=movie_title).first()
151 - if movie is not None:
165 + if movie:
166 return movie
167 new_movie = OMDBClient().get_movie(title=movie_title)
168 if new_movie is None:
@@ -158,43 +172,74 @@ class SQliteDataManager(DataManagerInterface):
172 session.commit()
173 return movie
174
161 - def update_movie(self, movie_id: int, update_data: dict) -> dict | None:
175 + def update_user_movie(self, user_id: int, movie_id: int, update_data: dict) -> dict | None:
176 with self.SessionFactory() as session:
177 movie = session.query(Movie).filter_by(id=movie_id).first()
178 if movie:
165 - for key, value in update_data.items():
166 - setattr(movie, key, value)
179 + session.query(UserMovie).filter_by(user_id=user_id, movie_id=movie_id).update(update_data)
180 session.commit()
181 return {
182 "id": movie.id,
183 "name": movie.name,
184 "director": movie.director,
185 "year": movie.year,
173 - "poster": movie.poster
186 + "poster": movie.poster,
187 + "user_rating": update_data["user_rating"]
188 }
189 return None
190
191 def delete_movie(self, movie_id: int) -> bool:
192 """
179 - Delete a movie and its associations.
180 -
181 - Args:
182 - movie_id: The ID of the movie to delete.
183 -
184 - Returns:
185 - True if the movie was successfully deleted, False otherwise.
193 + Delete a movie from the movies table in the database
194 + :param movie_id:
195 + :return:
196 """
187 - with (self.SessionFactory() as db):
188 - movie = db.query(Movie).filter_by(id = movie_id).first()
197 + with self.SessionFactory() as session:
198 + movie = session.query(Movie).filter_by(id = movie_id).first()
199 if movie:
190 - # Safely clear all associations through the association object
191 - movie.user_movies.clear()
200 + session.flush() # Make sure pending updates are flushed
201 + session.query(UserMovie).filter_by(movie_id = movie_id).delete()
202 + session.query(Movie).filter_by(id = movie_id).delete()
203 + session.commit()
204 + return True
205 + return False
206 +
207 + def delete_user(self, user_id: int) -> bool:
208 + """
209 + Delete a user from the users table in the database
210 + :param user_id:
211 + :return:
212 + """
213 + with self.SessionFactory() as session:
214 + user = session.query(User).filter_by(id = user_id).first()
215 + if user:
216 + session.flush() # Make sure pending updates are flushed
217 + session.query(UserMovie).filter_by(user_id = user_id).delete()
218 + session.query(User).filter_by(id = user_id).delete()
219 + session.commit()
220 + return True
221 + return False
222 +
223
193 - db.delete(movie)
194 - db.commit()
224 + def delete_user_movie(self, user_id: int, movie_id: int) -> bool:
225 + with self.SessionFactory() as session:
226 + association = session.query(UserMovie).filter_by(user_id=user_id, movie_id=movie_id).first()
227 + if association:
228 + session.delete(association)
229 + session.commit()
230 return True
231 return False
232
233 + def get_movie(self, movie_id: int) -> Movie | None:
234 + """
235 + Get a movie from the movies table in the database
236 + :param movie_id:
237 + :return:
238 + """
239 + with self.SessionFactory() as session:
240 + movie = session.query(Movie).filter_by(id = movie_id).first()
241 + return movie
242 +
243 def main():
244 data_manager = SQliteDataManager("sqlite:///movie_app.db")
245
@@ -213,7 +258,7 @@ def main():
258
259 # Associate movies with users and assign ratings
260 print(f"user1:{user1}, movie1:{movie1}")
216 - data_manager.set_user_movies(user1.id, movie1.id, 8.9)
261 + data_manager.set_user_movies(user1.id, movie1.id)
262 print(data_manager.get_user_movies(user1.id))
263 data_manager.set_user_movies(user1.id, movie2.id, 8.5)
264 data_manager.set_user_movies(user2.id, movie2.id, 9.2)
@@ -227,8 +272,8 @@ def main():
272 print("All movies:", session.query(Movie).all())
273
274 # Example of updating a movie (use a new session)
230 - updated_movie = data_manager.update_movie(movie1.id,
231 - {"name": "The Matrix Reloaded", "user_rating":
275 + updated_movie = data_manager.update_user_movie(user1.id, movie1.id,
276 + {"user_rating":
277 7.2})
278 print("Updated movie:", updated_movie)
279
requirements.txt
+8 -2
@@ -1,2 +1,8 @@
1 -flask
2 -flask_sqlalchemy
\ No newline at end of file
1 +flask~=3.1.0
2 +Django~=5.2.1
3 +pytest~=8.3.5
4 +SQLAlchemy~=2.0.40
5 +dotenv~=0.9.9
6 +python-dotenv~=1.1.0
7 +requests~=2.32.3
8 +google~=3.0.0
\ No newline at end of file
static/styles.css new
+29
@@ -0,0 +1,29 @@
1 +/* Add a black background color to the top navigation */
2 +.topnav {
3 + border: 1px solid black;
4 + border-radius: 5px;
5 + background-color: #333;
6 + overflow: hidden;
7 +}
8 +
9 +/* Style the links inside the navigation bar */
10 +.topnav a {
11 + float: left;
12 + color: #f2f2f2;
13 + text-align: center;
14 + padding: 14px 16px;
15 + text-decoration: none;
16 + font-size: 17px;
17 +}
18 +
19 +/* Change the color of links on hover */
20 +.topnav a:hover {
21 + background-color: #ddd;
22 + color: black;
23 +}
24 +
25 +/* Add a color to the active/current link */
26 +.topnav a.active {
27 + background-color: #04AA6D;
28 + color: white;
29 +}
\ No newline at end of file
templates/base.html new
+22
@@ -0,0 +1,22 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <meta http-equiv="X-UA-Compatible" content="IE=edge">
7 +
8 + <title>{% block title %}{% endblock %}</title>
9 + <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
10 +</head>
11 +<body>
12 +<header class="topnav">
13 + <a class="active" href="/">Home</a>
14 + <a href="/users">Users</a>
15 + <a href="/movies">Movies</a>
16 + <a href="/users/new">New User</a>
17 + <a href="/movies/new">New Movie</a>
18 +
19 +</header>
20 + {% block content %}{% endblock %}
21 +</body>
22 +</html>
\ No newline at end of file
templates/home.html
+4 -9
@@ -1,14 +1,9 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>Movie - WebApp</title>
6 -</head>
7 -<body>
1 +{% extends "base.html" %}
2 + {% block title %}Movie - WebApp{% endblock %}
3 + {% block content %}
4 <h1>Welcome to the Movie Web App</h1>
5 <ul>
6 <li><a href="/users" methods="GET">Users</a></li>
7 <li><a href="/movies" methods="GET">Movies</a></li>
8 </ul>
13 -</body>
14 -</html>
\ No newline at end of file
9 + {% endblock %}
\ No newline at end of file
templates/movies.html
+11 -14
@@ -1,25 +1,22 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>Movies List</title>
6 -</head>
7 -<body>
1 +{% extends 'base.html' %}
2 +{% block title %}Movies List{% endblock %}
3 +{% block content %}
4 <h1>Movies List</h1>
5 + {% if success == True %}
6 + <p>Movie deleted successfully!</p>
7 + {% elif success == False %}
8 + <p>Failed to delete movie. Please try again.</p>
9 + {% elif success == None %}
10 + {% endif %}
11
12 {% for movie in movies %}
13 <img height="200" alt="{{ movie.name }}" src="{{ movie.poster }}">
14 <ul>
15 <li><a href="/movies/{{ movie.id }}">{{ movie.name }} ({{ movie.year }})</a></li>
16 <form action="/movies" method="post">
15 - <button type="submit" name = "movie_id" value ="{{ movie.id }}">
17 + <button type="submit" id = "movie_id" name = "movie_id" value="{{ movie.id }}">
18 Delete</button></form>
19 </ul>
20 {% endfor %}
19 - <a href="/">Home</a>
20 - <a href="/users">Users</a>
21 - <a href="/movies/new">Add Movie</a>
22 - <a href="/users/new">Add User</a>
21
24 -</body>
25 -</html>
\ No newline at end of file
22 +{% endblock %}
templates/new_movie.html
+5 -13
@@ -1,13 +1,9 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>Add new Movie</title>
6 -</head>
7 -<body>
1 +{% extends "base.html" %}
2 +{% block title %}Add Movie{% endblock %}
3 +{% block content %}
4 <h1>Add new Movie</h1>
5 {% if success == True %}
10 - <p>Movie added successfully!</p>
6 + <p>Movie: {{ movie.name }} added successfully!</p>
7 {% elif success == False %}
8 <p>Failed to add movie. Please try again.</p>
9 {% elif success == None %}
@@ -17,8 +13,4 @@
13 <input type="text" id="name" name="name" required>
14 <button type="submit">Add Movie</button>
15 </form>
20 - <a href="/">Home</a>
21 - <a href="/users">Users</a>
22 - <a href="/movies">Movies</a>
23 -</body>
24 -</html>
\ No newline at end of file
16 + {% endblock %}
\ No newline at end of file
templates/new_user.html
+4 -12
@@ -1,10 +1,6 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>Add new User</title>
6 -</head>
7 -<body>
1 +{% extends 'base.html' %}
2 +{% block title %}Add new User{% endblock %}
3 +{% block content %}
4 <h1>Add new User</h1>
5 {% if success == True %}
6 <p>User added successfully!</p>
@@ -17,8 +13,4 @@
13 <input type="text" id="name" name="name" required>
14 <button type="submit">Add User</button>
15 </form>
20 - <a href="/">Home</a>
21 - <a href="/users">Users</a>
22 - <a href="/movies">Movies</a>
23 -</body>
24 -</html>
\ No newline at end of file
16 +{% endblock %}
\ No newline at end of file
templates/user_movie.html new
+31
@@ -0,0 +1,31 @@
1 +{% extends "base.html" %}
2 +{% block title %}{{user.name}} - Movie List{% endblock %}
3 +
4 +{% block content %}
5 + <h1 class="title" style="text-align: center;">{{user.name}} - Movie List</h1>
6 +
7 + <header>
8 + {% if success == True %}
9 + <p>Movie updated successfully!</p>
10 + {% elif success == False %}
11 + <p>Failed to update movie. <br>
12 + Please try again!</p>
13 + {% elif success == None %}
14 + {% endif %}
15 +
16 + </header>
17 +
18 + <img height="200" alt="{{ movie.name }}" src="{{ movie.poster }}">
19 + <ul><li>{{ movie.name }} ({{ movie.year }})</li>
20 + <li>Director: {{ movie.director }}</li>
21 + <li>Rating: {{ movie.rating }} </li>
22 + <li>User Rating: {{ user_movie.user_rating }}</li>
23 + <li><form action="/users/{{user.id}}/{{movie.id}}"
24 + method="POST">
25 + <label for="user_rating">New User Rating:</label>
26 + <input type="text" id="user_rating" name="user_rating" required>
27 + <button type="submit">Update</button></form></li>
28 + <hr>
29 + </ul>
30 +
31 +{% endblock %}
\ No newline at end of file
templates/user_movies.html
+25 -14
@@ -1,19 +1,23 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>{{user.name}} - Movie List</title>
6 -</head>
7 -<body>
8 - <h1>{{user.name}} - Movie List</h1>
1 +{% extends "base.html" %}
2 +{% block title %} {{ user.name }} - Movie List{% endblock %}
3 +{% block content %}
4 + <h1 class="title" style="text-align: center;">{{user.name}} - Movie List</h1>
5
6 <header>
7 {% if success == True %}
8 <p>Movie added successfully!</p>
9 {% elif success == False %}
14 - <p>Failed to add movie. Please try again.</p>
10 + <p>Failed to add movie. Either movie already associated with user or movie does not
11 + exist. <br>
12 + Please try again!</p>
13 {% elif success == None %}
14 {% endif %}
15 + {% if movie_deleted == True %}
16 + <p>Movie deleted successfully!</p>
17 + {% elif movie_deleted == False %}
18 + <p>Failed to delete movie. Please try again.</p>
19 + {% elif movie_deleted == None %}
20 + {% endif %}
21 <form action="/users/{{user.id}}" method="POST">
22 <label for="name">Name:</label>
23 <input type="text" id="name" name="name" required>
@@ -23,13 +27,20 @@
27 {% for movie in user_movies %}
28 <img height="200" alt="{{ movie.name }}" src="{{ movie.poster }}">
29 <ul><li>{{ movie.name }} ({{ movie.year }})</li>
30 + <form action="/users/{{user.id}}/delete/{{movie.id}}"
31 + method="POST">
32 + <button type="submit" id = "movie_id" name = "movie_id" value="{{ movie.id }}">
33 + Delete</button></form>
34 <li>Director: {{ movie.director }}</li>
27 - <li>Rating: <input type="number" id="rating" name="rating" required> {{ movie.rating }} <form action="/users/{{user.id}}/{{movie.id}}"
28 - method="UPDATE"><button type="submit">Update
29 - </button></form></li>
35 + <li>Rating: {{ movie.rating }} </li>
36 + <li>User Rating: {{ movie.user_rating }}</li>
37 + <li><form action="/users/{{user.id}}/{{movie.id}}"
38 + method="POST">
39 + <label for="user_rating">New User Rating:</label>
40 + <input type="text" id="user_rating" name="user_rating" required>
41 + <button type="submit">Update</button></form></li>
42 <hr>
43 </ul>
44 {% endfor %}
45
34 -</body>
35 -</html>
\ No newline at end of file
46 +{% endblock %}
\ No newline at end of file
templates/users.html
+4 -10
@@ -1,10 +1,6 @@
1 -<!DOCTYPE html>
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>Users - MovieWeb App</title>
6 -</head>
7 -<body>
1 +{% extends 'base.html' %}
2 +{% block title %}Users - MovieWeb{% endblock %}
3 +{% block content %}
4 <h1>Users</h1>
5 <ul><a href="/users/new" methods="GET">Add User</a></ul>
6 <ul><a href="/" methods="GET">Home</a></ul>
@@ -14,6 +10,4 @@
10 <li><a href="/users/{{user.id}}" methods="GET">{{user.name}}</a></li>
11 {% endfor %}
12 </ul>
17 -
18 -</body>
19 -</html>
\ No newline at end of file
13 +{% endblock %}