Skip to content

Commit

Permalink
Merge pull request #41 from CS222-UIUC/minh/touch_up_backend
Browse files Browse the repository at this point in the history
Added support for pagination, separated prod and test databases
  • Loading branch information
MinhPhan8803 authored Nov 7, 2022
2 parents f1bfa98 + 911877d commit a8c61b5
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 107 deletions.
3 changes: 1 addition & 2 deletions src/backend/.coveragerc
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
[run]
omit = *tests*
*init*
*decorators*
*init*
8 changes: 7 additions & 1 deletion src/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,18 @@ def mainpage_get(mainpage_obj: MainPage, args: MultiDict):
apts = []
price_sort = 0
rating_sort = 0
apt_id = -1
if param.price_sort is not None:
price_sort = param.price_sort

if param.rating_sort is not None:
rating_sort = param.rating_sort
apts = mainpage_obj.populate_apartments(param.num_apts, price_sort, rating_sort)

if param.apt_id is not None:
apt_id = param.apt_id
apts = mainpage_obj.populate_apartments(
param.num_apts, price_sort, rating_sort, apt_id
)
apts_dict = [dataclasses.asdict(apt) for apt in apts]
query_result = json.dumps(apts_dict)

Expand Down
3 changes: 3 additions & 0 deletions src/backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Special configurations"""

DB_NAME = "database_prod.db"
Binary file not shown.
Binary file added src/backend/database/database_test.db
Binary file not shown.
4 changes: 3 additions & 1 deletion src/backend/database/init_db.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Initialize database"""
import sqlite3
import sys

connection = sqlite3.connect("database.db")
ARG = sys.argv[1]
connection = sqlite3.connect(ARG)
with open("schema.sql", "r", encoding="utf-8") as f:
connection.executescript(f.read())
cur = connection.cursor()
Expand Down
19 changes: 17 additions & 2 deletions src/backend/decorators.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""Stores decorators for common functionalities"""
import sqlite3
import functools
import config


def use_database(func):
"""Decorator to utilize DB connections"""

@functools.wraps(func)
def wrapped(*args):
"""Wrapper about the actual function"""
connection = sqlite3.connect("database/database.db")
"""Wrapper around the actual function"""
connection = sqlite3.connect(f"database/{config.DB_NAME}")
cursor = connection.cursor()
wrapped.cursor = cursor
wrapped.connection = connection
Expand All @@ -25,3 +26,17 @@ def wrapped(*args):
return caller

return wrapped


def use_test(func):
"""Decorator to switch database for testing"""

@functools.wraps(func)
def wrapped(*args, **kwargs):
"""Wrapper around the actual function"""
config.DB_NAME = "database_test.db"
func_instance = func(*args, **kwargs)
config.DB_NAME = "database_prod.db"
return func_instance

return wrapped
193 changes: 133 additions & 60 deletions src/backend/pages/mainpage.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
"""Contains Main page class"""
import sqlite3
from datetime import date
from typing import List
from typing import Tuple
from dataclasses import dataclass
from dataholders.apt import Apt
from dataholders.review import Review
from decorators import use_database


@dataclass(frozen=True)
class LatestPopulatedApt:
"""Stores details related to the latest apt in a scroll"""

apt_name: str
apt_id: int


class MainPage:
"""Mainpage class, interacts with the mainpage frontend"""

populate_query = "WITH temp(id, name, address, total_vote, p_min, p_max, p_avg) AS \
(SELECT Apartments.apt_id, Apartments.apt_name, Apartments.apt_address, \
COALESCE(SUM(Reviews.vote), 0), \
Apartments.price_min, Apartments.price_max, \
(Apartments.price_min + Apartments.price_max)/2 \
FROM Apartments LEFT JOIN Reviews ON Apartments.apt_id = Reviews.apt_id \
GROUP BY Apartments.apt_id) "

def __init__(self) -> None:
"""Constructor"""

Expand All @@ -37,96 +53,153 @@ def search_apartments(self, query: str) -> List[Apt]:

@use_database
def populate_apartments(
self, num_apts: int, price_sort: int, rating_sort: int
self, num_apts: int, price_sort: int, rating_sort: int, apt_id: int
) -> List[Apt]:
"""Returns num_apts apartments with sorting criterias"""

apts = []
apt_query = []

apt_name = ""
if apt_id >= 0:
apt_name = self.populate_apartments.cursor.execute(
"SELECT apt_name FROM Apartments WHERE apt_id = ?", (apt_id,)
).fetchone()[0]
latest_row = LatestPopulatedApt(apt_name, apt_id)
if price_sort == 0:
apt_query = self.rating_sort_helper(
num_apts, rating_sort, self.populate_apartments.cursor
)
apt_query = self.rating_sort_helper(num_apts, rating_sort, latest_row)
elif rating_sort == 0 and price_sort != 0:
apt_query = self.price_sort_helper(
num_apts, price_sort, self.populate_apartments.cursor
)
apt_query = self.price_sort_helper(num_apts, price_sort, latest_row)
else:
apt_query = self.both_sort_helper(
num_apts, price_sort, rating_sort, self.populate_apartments.cursor
num_apts, price_sort, rating_sort, latest_row
)

for entry in apt_query:
apts.append(Apt(entry[0], entry[1], entry[2], entry[3], entry[4], entry[5]))
return apts

@use_database
def rating_sort_helper(
self, num_apts: int, rating_sort: int, cursor: sqlite3.Cursor
self, num_apts: int, rating_sort: int, latest_row: LatestPopulatedApt
) -> List[Tuple]:
"""Helper for rating-only sort"""
rating_order = ""
if rating_sort in (0, 1):
rating_order = "DESC"
apt_query = cursor.execute(
f"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 \
GROUP BY Apartments.apt_id \
ORDER BY total_vote {rating_order}, \
Apartments.apt_name \
LIMIT ?",
(num_apts,),
).fetchall()

rating_order = "DESC" if rating_sort in (0, 1) else ""
rating_comp = "<" if rating_sort in (0, 1) else ">"
apt_query = []
if latest_row.apt_id >= 0:
apt_query = self.rating_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
WHERE \
(total_vote = (SELECT total_vote FROM temp WHERE id = ?) \
AND (name, id) > (?, ?)) \
OR total_vote {rating_comp} (SELECT total_vote FROM temp WHERE id = ?) \
ORDER BY total_vote {rating_order}, name, id \
LIMIT ?",
(
latest_row.apt_id,
latest_row.apt_name,
latest_row.apt_id,
latest_row.apt_id,
num_apts,
),
).fetchall()
else:
apt_query = self.rating_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
ORDER BY total_vote {rating_order}, name, id \
LIMIT ?",
(num_apts,),
).fetchall()
return apt_query

@use_database
def price_sort_helper(
self, num_apts: int, price_sort: int, cursor: sqlite3.Cursor
self, num_apts: int, price_sort: int, latest_row: LatestPopulatedApt
) -> List[Tuple]:
"""Helper for price-only sorts"""
price_order = ""
if price_sort == 1:
price_order = "DESC"
apt_query = cursor.execute(
f"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 \
GROUP BY Apartments.apt_id \
ORDER BY (Apartments.price_min + Apartments.price_max)/2 {price_order}, \
Apartments.apt_name \
LIMIT ?",
(num_apts,),
).fetchall()

price_order = "DESC" if price_sort == 1 else ""
price_comp = "<" if price_sort == 1 else ">"
apt_query = []
if latest_row.apt_id >= 0:
apt_query = self.price_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
WHERE \
(p_avg = (SELECT p_avg FROM temp WHERE id = ?) \
AND (name, id) > (?, ?)) \
OR p_avg {price_comp} (SELECT p_avg FROM temp WHERE id = ?) \
ORDER BY p_avg {price_order}, name, id \
LIMIT ?",
(
latest_row.apt_id,
latest_row.apt_name,
latest_row.apt_id,
latest_row.apt_id,
num_apts,
),
).fetchall()
else:
apt_query = self.price_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
ORDER BY p_avg {price_order}, name, id \
LIMIT ?",
(num_apts,),
).fetchall()
return apt_query

@use_database
def both_sort_helper(
self, num_apts: int, price_sort: int, rating_sort: int, cursor: sqlite3.Cursor
self,
num_apts: int,
price_sort: int,
rating_sort: int,
latest_row: LatestPopulatedApt,
) -> List[Tuple]:
"""Helper to sort both params"""

price_order = ""
if price_sort == 1:
price_order = "DESC"
rating_order = ""
if rating_sort == 1:
rating_order = "DESC"
price_comp = "<" if price_sort == 1 else ">"
price_order = "DESC" if price_sort == 1 else ""

apt_query = cursor.execute(
f"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 \
GROUP BY Apartments.apt_id \
ORDER BY (Apartments.price_min + Apartments.price_max)/2 {price_order}, \
total_vote {rating_order}, \
Apartments.apt_name \
LIMIT ?",
(num_apts,),
).fetchall()
rating_comp = "<" if rating_sort == 1 else ">"
rating_order = "DESC" if rating_sort == 1 else ""

if latest_row.apt_id >= 0:
apt_query = self.both_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
WHERE p_avg {price_comp} (SELECT p_avg FROM temp WHERE id = ?) \
OR ( \
p_avg = (SELECT p_avg FROM temp WHERE id = ?) \
AND ( \
total_vote {rating_comp} (SELECT total_vote FROM temp WHERE id = ?) \
OR (total_vote = (SELECT total_vote FROM temp WHERE id = ?) \
AND (name, id) > (?, ?)) \
) \
) \
ORDER BY p_avg {price_order}, total_vote {rating_order}, name, id \
LIMIT ?",
(
latest_row.apt_id,
latest_row.apt_id,
latest_row.apt_id,
latest_row.apt_id,
latest_row.apt_name,
latest_row.apt_id,
num_apts,
),
).fetchall()
else:
apt_query = self.both_sort_helper.cursor.execute(
self.populate_query
+ f"SELECT * FROM temp \
ORDER BY p_avg {price_order}, total_vote {rating_order}, name, id \
LIMIT ?",
(num_apts,),
).fetchall()

return apt_query

Expand Down
31 changes: 16 additions & 15 deletions src/backend/tests/mainpage_staging.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Stage the database for"""
import sqlite3
from decorators import use_database


class MainPageStaging:
Expand All @@ -10,8 +11,8 @@ def insert_apartments(self, cursor: sqlite3.Cursor, connection: sqlite3.Connecti
args = [
("Sherman", "909 S 5th St", 5500, 6500, ""),
("FAR", "901 W College Ct", 6000, 7000, ""),
("Lincoln", "1005 S Lincoln Ave", 5000, 6000, ""),
("PAR", "901 W College Ct", 5000, 6000, ""),
("Lincoln", "1005 S Lincoln Ave", 5000, 6000, ""),
("ISR", "918 W Illinois", 6000, 7000, ""),
]
cursor.executemany(
Expand Down Expand Up @@ -130,22 +131,22 @@ def clean_up_pics(self, cursor: sqlite3.Cursor, connection: sqlite3.Connection):
cursor.execute("DELETE FROM AptPics WHERE apt_id = ?", (sherman_id,))
connection.commit()

@use_database
def initialize_all(self):
"""Initialize test data"""
connection = sqlite3.connect("database/database.db")
cursor = connection.cursor()
self.insert_apartments(cursor, connection)
self.insert_users(cursor, connection)
self.insert_reviews(cursor, connection)
self.insert_pics(cursor, connection)
connection.close()
conn = self.initialize_all.connection
cur = self.initialize_all.cursor
self.insert_apartments(cur, conn)
self.insert_users(cur, conn)
self.insert_reviews(cur, conn)
self.insert_pics(cur, conn)

@use_database
def clean_all(self):
"""Clean up test data"""
connection = sqlite3.connect("database/database.db")
cursor = connection.cursor()
self.clean_up_reviews(cursor, connection)
self.clean_up_pics(cursor, connection)
self.clean_up_apartments(cursor, connection)
self.clean_up_users(cursor, connection)
connection.close()
conn = self.clean_all.connection
cur = self.clean_all.cursor
self.clean_up_reviews(cur, conn)
self.clean_up_pics(cur, conn)
self.clean_up_apartments(cur, conn)
self.clean_up_users(cur, conn)
Loading

3 comments on commit a8c61b5

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCover
app.py910100%
config.py10100%
decorators.py270100%
dataholders
   apt.py90100%
   review.py70100%
pages
   login.py310100%
   mainpage.py950100%
TOTAL2610100%

Tests Skipped Failures Errors Time
44 0 💤 0 ❌ 0 🔥 0.690s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCover
app.py910100%
config.py10100%
decorators.py270100%
dataholders
   apt.py90100%
   review.py70100%
pages
   login.py310100%
   mainpage.py950100%
TOTAL2610100%

Tests Skipped Failures Errors Time
44 0 💤 0 ❌ 0 🔥 0.721s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCover
app.py910100%
config.py10100%
decorators.py270100%
dataholders
   apt.py90100%
   review.py70100%
pages
   login.py310100%
   mainpage.py950100%
TOTAL2610100%

Tests Skipped Failures Errors Time
44 0 💤 0 ❌ 0 🔥 0.737s ⏱️

Please sign in to comment.