diff --git a/src/backend/app.py b/src/backend/app.py index f8990c8ad..4fcb907e2 100644 --- a/src/backend/app.py +++ b/src/backend/app.py @@ -30,7 +30,6 @@ def login(): if user_login.login(username, password): # session object makes User accessible in the backend session["username"] = username - print(session.get("username")) return f"welcome {username}", 200 return "User not found, please try again", 401 return "", 400 @@ -57,7 +56,6 @@ def register(): @app.route("/user", methods=["GET", "POST"]) def userpage(): """Handles userpage requests""" - print(session.get("username")) if session.get("username") is None: return "user does not exist", 403 name = session.get("username") or "" @@ -97,7 +95,6 @@ def logout(): @app.route("/api/whoami") def whoami(): """Shows whether a user is logged in and returns session username""" - print(session.get("username")) if session.get("username") is None: return "user logged out", 403 username = session.get("username", "") @@ -109,10 +106,10 @@ def whoami(): def mainpage(): """Handle mainpage requests""" mainpage_obj = MainPage() + args = request.args if request.method == "POST": - return mainpage_post(mainpage_obj) + return mainpage_post(mainpage_obj, args) - args = request.args return mainpage_get(mainpage_obj, args) @@ -138,6 +135,7 @@ def mainpage_get(mainpage_obj: MainPage, args: MultiDict): args.get("searchQuery", type=str), args.get("priceSort", type=int), args.get("ratingSort", type=int), + args.get("checkReview", type=bool), ) return mainpage_process_get(mainpage_obj, action, param) @@ -147,10 +145,15 @@ def mainpage_process_get( mainpage_obj: MainPage, action: GetRequestType, param: GetRequestParams ): """Process the get requests""" + user = session.get("username", "") query_result = "" - if action.is_search is True and param.search_query is not None: - apts = mainpage_obj.search_apartments(param.search_query) - query_result = dataclasses_into_json(apts) + if action.is_search is True: + if param.search_query is not None: + apts = mainpage_obj.search_apartments(param.search_query) + query_result = dataclasses_into_json(apts) + if param.apt_id is not None: + apt = mainpage_obj.get_single_apt(param.apt_id) + query_result = json.dumps(dataclasses.asdict(apt)) elif action.is_populate is True and param.num_apts is not None: apts = [] @@ -171,8 +174,12 @@ def mainpage_process_get( query_result = dataclasses_into_json(apts) elif action.is_review is True and param.apt_id is not None: - reviews = mainpage_obj.get_apartments_reviews(param.apt_id) - query_result = dataclasses_into_json(reviews) + if param.check_review is None: + reviews = mainpage_obj.get_apartments_reviews(param.apt_id, user) + query_result = dataclasses_into_json(reviews) + else: + if mainpage_obj.check_user_reviewed(param.apt_id, user): + query_result = "True" elif action.is_pictures is True and param.apt_id is not None: query_result = json.dumps(mainpage_obj.get_apartments_pictures(param.apt_id)) @@ -182,15 +189,16 @@ def mainpage_process_get( return "", 400 -def mainpage_post(mainpage_obj: MainPage): +def mainpage_post(mainpage_obj: MainPage, args: MultiDict): """ Helper for mainpage post requests Actions that use post request: - Writing a review to an apartment + - Delete an existing review """ json_form = request.get_json(force=True) - is_delete = request.args.get("delete", default=False, type=bool) + is_delete = args.get("delete", default=False, type=bool) if isinstance(json_form, dict): if not is_delete: diff --git a/src/backend/database/database_prod.db b/src/backend/database/database_prod.db index 1d86eeeed..4df1d4a7a 100644 Binary files a/src/backend/database/database_prod.db and b/src/backend/database/database_prod.db differ diff --git a/src/backend/database/database_test.db b/src/backend/database/database_test.db index 6a2902beb..2f113693b 100644 Binary files a/src/backend/database/database_test.db and b/src/backend/database/database_test.db differ diff --git a/src/backend/dataholders/mainpage_get.py b/src/backend/dataholders/mainpage_get.py index df9498d92..dd38c6d27 100644 --- a/src/backend/dataholders/mainpage_get.py +++ b/src/backend/dataholders/mainpage_get.py @@ -22,3 +22,4 @@ class GetRequestParams: search_query: Union[str, None] rating_sort: Union[int, None] price_sort: Union[int, None] + check_review: Union[bool, None] diff --git a/src/backend/pages/mainpage.py b/src/backend/pages/mainpage.py index e81ef52ca..f1f8495f0 100644 --- a/src/backend/pages/mainpage.py +++ b/src/backend/pages/mainpage.py @@ -27,6 +27,11 @@ class MainPage: FROM Apartments LEFT JOIN Reviews ON Apartments.apt_id = Reviews.apt_id \ GROUP BY Apartments.apt_id) " + search_apt_query = "SELECT Apartments.apt_id, Apartments.apt_name, Apartments.apt_address, \ + COALESCE(SUM(Reviews.vote), 0) AS 'total_vote', \ + Apartments.price_min, Apartments.price_max \ + FROM Apartments LEFT JOIN Reviews ON Apartments.apt_id = Reviews.apt_id " + def __init__(self) -> None: """Constructor""" @@ -35,11 +40,8 @@ def search_apartments(self, query: str) -> List[Apt]: """Returns a list of apartments with name matching query""" query_sql = "%" + query.lower() + "%" apt_query = self.search_apartments.cursor.execute( - "SELECT Apartments.apt_id, Apartments.apt_name, Apartments.apt_address, \ - COALESCE(SUM(Reviews.vote), 0) AS 'total_vote', \ - Apartments.price_min, Apartments.price_max \ - FROM Apartments LEFT JOIN Reviews ON Apartments.apt_id = Reviews.apt_id \ - WHERE LOWER(Apartments.apt_name) LIKE ? \ + self.search_apt_query + + "WHERE LOWER(Apartments.apt_name) LIKE ? \ GROUP BY Apartments.apt_id", (query_sql,), ).fetchall() @@ -216,13 +218,10 @@ def get_apartments_pictures(self, apt_id: int) -> List[str]: @use_database def write_apartment_review( - self, apt_id: int, username: str, comment: str, vote: int + self, apt_id: int, user: str, comment: str, vote: int ) -> List[Review]: """Write a new review for apartment""" - user_id = self.write_apartment_review.cursor.execute( - "SELECT user_id FROM Users WHERE username = ? OR email = ?", - (username, username), - ).fetchone()[0] + user_id = self.get_user_id_from_user(user) today = date.today().strftime("%Y-%m-%d") self.write_apartment_review.cursor.execute( "INSERT INTO Reviews (apt_id, user_id, date_of_rating, comment, vote) \ @@ -239,21 +238,23 @@ def write_apartment_review( FROM Users INNER JOIN Reviews \ ON Users.user_id = Reviews.user_id \ WHERE Reviews.apt_id = ? \ - ORDER BY Users.username = ? DESC, Reviews.date_of_rating DESC", - (apt_id, username), + ORDER BY Users.user_id = ? DESC, Reviews.date_of_rating DESC", + (apt_id, user_id), ).fetchall() return self.create_reviews_helper(ratings_query) @use_database - def get_apartments_reviews(self, apt_id: int) -> List[Review]: + def get_apartments_reviews(self, apt_id: int, user: str) -> List[Review]: """Returns a list of apartment reviews""" + user_id = self.get_user_id_from_user(user) ratings_query = self.get_apartments_reviews.cursor.execute( "SELECT Users.username, Reviews.date_of_rating, Reviews.comment, Reviews.vote \ FROM Users INNER JOIN Reviews \ ON Users.user_id = Reviews.user_id \ WHERE Reviews.apt_id = ? \ - ORDER BY Reviews.date_of_rating DESC", - (apt_id,), + ORDER BY Users.user_id = ? DESC, \ + Reviews.date_of_rating DESC", + (apt_id, user_id), ).fetchall() return self.create_reviews_helper(ratings_query) @@ -269,12 +270,49 @@ def create_reviews_helper(self, ratings_query: List[Tuple]) -> List[Review]: return reviews @use_database - def delete_apartment_review(self, apt_id: int, username: str) -> List[Review]: + def delete_apartment_review(self, apt_id: int, user: str) -> List[Review]: """Delete an apartment reviews""" + user_id = self.get_user_id_from_user(user) self.delete_apartment_review.cursor.execute( - "DELETE FROM Reviews WHERE (apt_id = ? AND user_id = \ - (SELECT user_id FROM Users WHERE username = ?))", - (apt_id, username), + "DELETE FROM Reviews WHERE (apt_id = ? AND user_id = ?)", + (apt_id, user_id), ) self.delete_apartment_review.connection.commit() - return self.get_apartments_reviews(apt_id) + return self.get_apartments_reviews(apt_id, "") + + @use_database + def check_user_reviewed(self, apt_id: int, user: str) -> bool: + """Check if review exists for an user""" + user_id = self.get_user_id_from_user(user) + print(user_id) + review = self.check_user_reviewed.cursor.execute( + "SELECT * FROM Reviews WHERE (apt_id = ? AND user_id = ?)", + ( + apt_id, + user_id, + ), + ).fetchone() + print(review) + return review is not None + + @use_database + def get_user_id_from_user(self, user) -> int: + """Gets user_id corresponding to certain username/email""" + user_id = self.get_user_id_from_user.cursor.execute( + "SELECT user_id FROM Users WHERE username = ? OR email = ?", + (user, user), + ).fetchone() + if user_id is None: + return -1 + return user_id[0] + + @use_database + def get_single_apt(self, apt_id: int) -> Apt: + """Get a single apt from apt id""" + apt = self.get_single_apt.cursor.execute( + self.search_apt_query + + "WHERE Apartments.apt_id = ? \ + GROUP BY Apartments.apt_id", + (apt_id,), + ).fetchone() + return Apt(apt[0], apt[1], apt[2], apt[3], apt[4], apt[5]) diff --git a/src/backend/tests/test_app.py b/src/backend/tests/test_app.py index 7111ebf05..fd36a90aa 100644 --- a/src/backend/tests/test_app.py +++ b/src/backend/tests/test_app.py @@ -133,6 +133,44 @@ def test_mainpage_get_valid_review(client): assert res.text == sample_json +@use_test +def test_mainpage_get_user_reviewed(client): + """Mainpage handles review checking request""" + mainpage = MainPageStaging() + mainpage.initialize_all() + connection = sqlite3.connect("database/database_test.db") + cursor = connection.cursor() + far_id = cursor.execute( + "SELECT apt_id FROM Apartments WHERE (apt_name = 'FAR')" + ).fetchone()[0] + query = {"review": "True", "checkReview": True, "aptId": far_id} + log_info = {"user": "Big_finger", "password": "big_password1"} + res_1 = client.get("/login", json=log_info) + res = client.get("/main", query_string=query) + mainpage.clean_all() + connection.close() + assert res_1.status_code == 200 + assert res.status_code == 200 + + +@use_test +def test_mainpage_get_search_single_apt(client): + """Mainpage handles getting single apt info""" + mainpage = MainPageStaging() + mainpage.initialize_all() + connection = sqlite3.connect("database/database_test.db") + cursor = connection.cursor() + far_id = cursor.execute( + "SELECT apt_id FROM Apartments WHERE (apt_name = 'FAR')" + ).fetchone()[0] + query = {"search": "True", "aptId": far_id} + res = client.get("/main", query_string=query) + mainpage.clean_all() + connection.close() + print(res.text) + assert res.status_code == 200 + + @use_test def test_mainpage_get_valid_search(client): """Test mainpage handles valid search request""" diff --git a/src/backend/tests/test_mainpage.py b/src/backend/tests/test_mainpage.py index 4ed4b0836..38fb07d13 100644 --- a/src/backend/tests/test_mainpage.py +++ b/src/backend/tests/test_mainpage.py @@ -327,7 +327,7 @@ def test_get_apartments_reviews(self): "SELECT apt_id FROM Apartments WHERE (apt_name = 'Sherman')" ).fetchone()[0] connection.close() - res = self.main_page.get_apartments_reviews(sherman_id) + res = self.main_page.get_apartments_reviews(sherman_id, "Big_finger") self.main_page_stage.clean_all() assert sample_apts_review == res @@ -375,7 +375,7 @@ def test_get_apartments_reviews_empty(self): ).fetchone()[0] self.main_page_stage.clean_up_reviews(cursor, connection) connection.close() - res = self.main_page.get_apartments_reviews(sherman_id) + res = self.main_page.get_apartments_reviews(sherman_id, "") self.main_page_stage.clean_all() assert sample_apts_review == res @@ -402,3 +402,32 @@ def test_delete_apartment_review(self): connection.close() self.main_page_stage.clean_all() assert modified_review == sample_apts_review + + @use_test + def test_check_user_reviewed(self): + """Test checking a review exists for a user""" + self.main_page_stage.initialize_all() + connection = sqlite3.connect("database/database_test.db") + cursor = connection.cursor() + par_id = cursor.execute( + "SELECT apt_id FROM Apartments WHERE (apt_name = 'PAR')" + ).fetchone()[0] + check = self.main_page.check_user_reviewed(par_id, "Big_finger") + self.main_page_stage.clean_all() + connection.close() + assert check + + @use_test + def test_get_single_apt(self): + """Test gets a single apt""" + self.main_page_stage.initialize_all() + connection = sqlite3.connect("database/database_test.db") + cursor = connection.cursor() + par_id = cursor.execute( + "SELECT apt_id FROM Apartments WHERE (apt_name = 'PAR')" + ).fetchone()[0] + par = Apt(par_id, "PAR", "901 W College Ct", -1, 5000, 6000) + check = self.main_page.get_single_apt(par_id) + self.main_page_stage.clean_all() + connection.close() + assert par == check diff --git a/src/frontend/src/components/SearchBar.tsx b/src/frontend/src/components/SearchBar.tsx index 2e69e2558..2b5e4711f 100644 --- a/src/frontend/src/components/SearchBar.tsx +++ b/src/frontend/src/components/SearchBar.tsx @@ -3,13 +3,42 @@ import React, { useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import './SearchBarStyles.css'; import getSuggestions from './mainpageleft/getSearchBarSuggestions'; +import { AptType } from './Types'; +import axios from 'axios'; +interface Props { + handleAptChange: (apt: AptType) => void; +} -export default function SearchBar() { +const baseURL = 'http://127.0.0.1:5000/main'; +export default function SearchBar({ handleAptChange }: Props) { const [query, setQuery] = useState(''); const [searchParams, setSearchParams] = useSearchParams(); const [search, setSearch] = useState(false); - const { names } = getSuggestions(query, search); - + const { apts } = getSuggestions(query, search); + const queryApt = (id: number) => { + axios({ + method: 'GET', + url: `${baseURL}?search=True&aptId=${id}`, + }) + .then((response) => { + console.log(response); + handleAptChange({ + id: response.data.apt_id, + name: response.data.name, + address: response.data.address, + price_min: response.data.price_min, + price_max: response.data.price_max, + rating: response.data.rating, + }); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + } + }); + }; const handleChange = ( event: React.SyntheticEvent, value: string @@ -35,7 +64,14 @@ export default function SearchBar() { id="free-solo-demo" freeSolo onInputChange={handleChange} - options={names.map((option) => option.name)} + onChange={(event, value) => { + console.log(event); + queryApt((value as AptType).id); + }} + options={apts} + getOptionLabel={(option) => (option as AptType).name} + //.map((option) => option.name) + filterOptions={(x) => x} renderInput={(params) => ( )} diff --git a/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx b/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx index 2c1a36b52..ff2efa745 100644 --- a/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx +++ b/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx @@ -1,15 +1,13 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; +import { AptType } from '../Types'; export default function getSuggestions(query: string, search: boolean) { - const emptyarray: { - name: string; - }[] = []; - const [names, setNames] = useState(emptyarray); + const [apts, setApts] = useState([]); useEffect(() => { // clears the names - setNames(emptyarray); + setApts([]); }, [query]); useEffect(() => { @@ -23,17 +21,20 @@ export default function getSuggestions(query: string, search: boolean) { withCredentials: true, }) .then((res) => { - const newNames: { - name: string; - }[] = []; + const newApts: AptType[] = []; for (let i = 0; i < res.data.length; i++) { if (res.data[i].name !== undefined) { - newNames.push({ + newApts.push({ + id: res.data[i].apt_id, name: res.data[i].name, + address: res.data[i].address, + price_min: res.data[i].price_min, + price_max: res.data[i].price_max, + rating: res.data[i].rating, }); } } - setNames(newNames); + setApts(newApts); }) .catch((e) => { if (axios.isCancel(e)) return; @@ -43,5 +44,5 @@ export default function getSuggestions(query: string, search: boolean) { }; }, [query]); - return { names }; + return { apts }; } diff --git a/src/frontend/src/components/mainpageright/AddReview.tsx b/src/frontend/src/components/mainpageright/AddReview.tsx index 2569632e8..f7c69cacf 100644 --- a/src/frontend/src/components/mainpageright/AddReview.tsx +++ b/src/frontend/src/components/mainpageright/AddReview.tsx @@ -10,18 +10,51 @@ import { Button, Radio, Stack, + ButtonGroup, } from '@mui/material'; interface Props { apt: AptType | undefined; setReviews: Dispatch>; username: string; + hasReview: boolean; + handleAptChange: (apt: AptType) => void; } const baseURL = 'http://127.0.0.1:5000/main'; -export default function AddReview({ apt, setReviews, username }: Props) { +export default function AddReview({ + apt, + setReviews, + username, + hasReview, + handleAptChange, +}: Props) { const [text, setText] = useState(''); const [vote, setVote] = useState(0); + const queryApt = () => { + axios({ + method: 'GET', + url: `${baseURL}?search=True&aptId=${apt?.id}`, + }) + .then((response) => { + console.log(response); + handleAptChange({ + id: response.data.apt_id, + name: response.data.name, + address: response.data.address, + price_min: response.data.price_min, + price_max: response.data.price_max, + rating: response.data.rating, + }); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + } + }); + }; const addReviewHandler = async (text: string, vote: number) => { // post review on submit const result = await axios.post(`${baseURL}`, { @@ -30,9 +63,33 @@ export default function AddReview({ apt, setReviews, username }: Props) { comment: text, vote: vote, }); + queryApt(); setReviews(result.data); console.log(result); }; + const deleteReview = () => { + axios({ + method: 'POST', + url: 'http://127.0.0.1:5000/main?delete=True', + withCredentials: true, + data: { + apt_id: apt?.id, + username: username, + }, + }) + .then((response) => { + console.log(response); + setReviews(response.data); + queryApt(); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + } + }); + }; const radioHandler = (event: React.ChangeEvent) => { console.log(typeof event.target.value); // set the vote @@ -50,12 +107,18 @@ export default function AddReview({ apt, setReviews, username }: Props) { } addReviewHandler(text, vote); }; + return ( {/* {error && {error}} */} - Create a review + {hasReview === false && Create a review} + {hasReview === true && ( + + You already reviewed this apartment, edit or delete it here + + )} setText(e.target.value)} @@ -72,9 +135,14 @@ export default function AddReview({ apt, setReviews, username }: Props) { label="Downvote" /> - + + + {hasReview === true && ( + + )} + diff --git a/src/frontend/src/components/mainpageright/ReviewsList.tsx b/src/frontend/src/components/mainpageright/ReviewsList.tsx index 8fed6b67f..843f53392 100644 --- a/src/frontend/src/components/mainpageright/ReviewsList.tsx +++ b/src/frontend/src/components/mainpageright/ReviewsList.tsx @@ -11,14 +11,14 @@ const ReviewsList = ({ reviews }: Props) => { {reviews.length === 0 && ( - No comment yet. Write your first comment + No review yet. Write your first review )} {reviews.length !== 0 && ( - Comments + Reviews {reviews.map((review, i) => ( (apartments[0]); const [logged, setLogged] = useState(false); const [username, setUsername] = useState(''); + const handleAptChange = (apt: AptType) => { + setTo(apt); + }; function checkLoggedIn() { axios({ url: 'http://127.0.0.1:5000/api/whoami', @@ -42,7 +45,9 @@ function MainPage() { } }); } - checkLoggedIn(); + useEffect(() => { + checkLoggedIn(); + }, []); return ( <> @@ -124,7 +129,7 @@ function MainPage() { alignItems="center" > - + @@ -137,6 +142,7 @@ function MainPage() { apt={to || apartments[0]} logged={logged} username={username} + handleAptChange={handleAptChange} /> diff --git a/src/frontend/src/pages/Register.tsx b/src/frontend/src/pages/Register.tsx index 8350cdd6d..12438af3d 100644 --- a/src/frontend/src/pages/Register.tsx +++ b/src/frontend/src/pages/Register.tsx @@ -22,7 +22,7 @@ export default function Register() { const [res, setRes] = useState(); const paperStyle = { padding: 20, - height: '80vh', + height: '100%', width: 310, margin: '20px auto', }; diff --git a/src/frontend/src/sections/MainPageRightSection.tsx b/src/frontend/src/sections/MainPageRightSection.tsx index 2061dcaff..3d634789c 100644 --- a/src/frontend/src/sections/MainPageRightSection.tsx +++ b/src/frontend/src/sections/MainPageRightSection.tsx @@ -12,21 +12,35 @@ interface apt { apt: AptType | undefined; // in case of null logged: boolean; username: string; + handleAptChange: (apt: AptType) => void; } -function RightSection({ apt, logged, username }: apt) { +function RightSection({ apt, logged, username, handleAptChange }: apt) { const [reviews, setReviews] = useState([]); const [pics, setPics] = useState([ 'https://www.salonlfc.com/wp-content/uploads/2018/01/image-not-found-scaled.png', ]); - // const [aptInfo, setAptInfo] = useState({ - // id: -1, - // name: 'Apartment Name', - // address: 'Apartment Address', - // price_min: 0, - // price_max: 9999, - // votes: -1, - // }); - // setAptInfo(apt); + const [hasReview, setHasReview] = useState(false); + function checkHasReview() { + axios({ + url: `${baseURL}?review=True&aptId=${apt?.id || 1}&checkReview=True`, + withCredentials: true, + }) + .then((response) => { + console.log(response); + setHasReview(true); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + setHasReview(false); + } + }); + } + useEffect(() => { + checkHasReview(); + }, [apt]); const retrieveReviews = async () => { const response = await axios.get( `${baseURL}?review=True&aptId=${apt?.id || 1}` @@ -60,16 +74,18 @@ function RightSection({ apt, logged, username }: apt) { {logged === true && ( - - )} - {logged === true && ( - + + + + )}