Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1494e6d
bfs and better run script
RespectMathias May 13, 2025
46b5298
fixed
RespectMathias May 13, 2025
d542002
Update launch.json
RespectMathias May 13, 2025
f105731
Update README.md
RespectMathias May 13, 2025
992c7c0
Update README.md
RespectMathias May 13, 2025
06a590c
Update run.py
RespectMathias May 13, 2025
b102d80
Automation
RespectMathias May 13, 2025
2bd8992
Fix check
RespectMathias May 13, 2025
51c11e4
Fix favicon
RespectMathias May 13, 2025
d4f0383
Fix cbf IO
RespectMathias May 14, 2025
f92f0d2
Fix home page book display issues
GiGwebs May 20, 2025
fa196e3
bfs and better run script
RespectMathias May 13, 2025
d3ec8c5
fixed
RespectMathias May 13, 2025
38bac1f
Update launch.json
RespectMathias May 13, 2025
9982a26
Update README.md
RespectMathias May 13, 2025
f655eb0
Update README.md
RespectMathias May 13, 2025
63e42ce
Update run.py
RespectMathias May 13, 2025
431d4b5
Automation
RespectMathias May 13, 2025
e6d5cea
Fix check
RespectMathias May 13, 2025
068def6
Fix favicon
RespectMathias May 13, 2025
160a7c9
Fix cbf IO
RespectMathias May 14, 2025
14713d5
Rebase and use django auth
RespectMathias May 22, 2025
c9e04d0
Fix styling
RespectMathias May 22, 2025
123253e
Merge branch 'improvement' of https://github.com/SP-SDU/Undershelf in…
RespectMathias May 22, 2025
6291e4d
Admin panel button
RespectMathias May 22, 2025
eb840a7
Add missing tests
RespectMathias May 22, 2025
c3ff55a
Update src/presentation/templates/base.html
RespectMathias May 22, 2025
65f23ca
Resolve review issues
RespectMathias May 22, 2025
3aef881
Update src/presentation/templates/book_details.html
RespectMathias May 22, 2025
03eb1bd
Merge pull request #19 from SP-SDU/improvement
RespectMathias May 22, 2025
2a7c12c
Fix home page book display issues
GiGwebs May 20, 2025
5a7b8fb
Fix duplicate issue
RespectMathias May 22, 2025
051eee0
Merge branch 'feature/fix-book-display' of https://github.com/SP-SDU/…
RespectMathias May 22, 2025
fbb4c47
Update top_k.py
RespectMathias May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
],
"django": true,
"autoStartBrowser": true,
"program": "${workspaceFolder}\\src\\manage.py",
"program": "${workspaceFolder}/src/manage.py",
"preLaunchTask": "Setup Django Project",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
14 changes: 14 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Setup Django Project",
"type": "process",
"command": "python",
"args": [
"${workspaceFolder}/run.py",
"--setup-only"
],
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Create Virtual Environment",
"type": "shell",
Expand Down
42 changes: 12 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,47 +17,28 @@
## Getting Started 🚀

### Prerequisites

- Python 3.12 or higher
- pip (Python package manager)
- Download [merged_dataframe.csv](https://drive.google.com/file/d/1MVRHs_CwKTBR2Rpakx920f277IcJ0q6X/view) and place it in the `src/data_access` directory.
- ~~Download [merged_dataframe.csv](https://drive.google.com/file/d/1MVRHs_CwKTBR2Rpakx920f277IcJ0q6X/view) and place it in the `src/data_access` directory.~~
- The dataset is gotten during the setup process.

### Installation

1. Clone the repository:
```
git clone https://github.com/SP-SDU/Undershelf.git
cd Undershelf
```

2. Create and activate a virtual environment:
```bash
# Windows
python -m venv .venv
.\.venv\Scripts\activate

# Linux/MacOS
python -m venv .venv
source .venv/bin/activate
```

3. Install dependencies:
```
pip install -r requirements.txt
git clone https://github.com/SP-SDU/Undershelf.git
cd Undershelf
```

### Running the Application (Or debug F5 in VSCode)
### Setup and Running the Application

1. Ensure virtual environment is activated (you should see `(.venv)` in your terminal)
Press `F5` in VS Code to start debugging or run it in terminal:

2. Start the django server:
```
python src/manage.py runserver
```

3. Open your browser and navigate to:
```
http://localhost:8000
```
```bash
python run.py
```

### Available Commands 🔧

Expand All @@ -76,9 +57,11 @@ The following commands are available in addition to those explained above:
## Contributing 🤝

1. **Clone** Open [GitHub Desktop](https://desktop.github.com/), go to `File > Clone Repository`, and enter:

```
https://github.com/SP-SDU/Undershelf
```

2. **Branch**: In GitHub Desktop, switch to `main` and create a new branch (e.g., `add-login-feature`).
3. **Commit & Push**: Commit changes in GitHub Desktop, then click `Push origin`.
4. **Pull Request**: Open a pull request on GitHub, choosing `main` as the base branch, and tag a teammate for review.
Expand All @@ -92,4 +75,3 @@ Join the [Discord server](https://discord.gg/a2ARm52WwE) for discussions and upd
## License 📝

Distributed under the Apache 2.0 License. See [LICENSE](LICENSE) for details.

187 changes: 187 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import argparse
import os
import platform
import subprocess
import urllib.request
import venv
import zipfile


def run_command(command, description=None):
if description:
print(f"\033[92m{description}...\033[0m")

process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)

if process.stdout:
for line in process.stdout:
print(line, end="")

process.wait()
if process.returncode != 0:
print(f"\033[91mCommand failed with return code {process.returncode}\033[0m")
return False
return True


def get_venv_python_path():
if platform.system() == "Windows":
python_exe = os.path.join(os.getcwd(), ".venv", "Scripts", "python.exe")
else:
python_exe = os.path.join(os.getcwd(), ".venv", "bin", "python")
return python_exe


def create_venv_if_not_exists():
if not os.path.exists(".venv"):
print("\033[92mCreating virtual environment...\033[0m")
venv.create(".venv", with_pip=True)
return True
return False


def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument(
"--setup-only",
action="store_true",
)
return parser.parse_args()


def create_superuser(python_exe):
print("\033[92mCreating superuser if needed...\033[0m")

os.environ.setdefault("DJANGO_SUPERUSER_USERNAME", "root")
os.environ.setdefault("DJANGO_SUPERUSER_PASSWORD", "root")
os.environ.setdefault("DJANGO_SUPERUSER_EMAIL", "root@undershelf.com")

run_command(
f"{python_exe} src/manage.py createsuperuser --noinput", "Creating superuser"
)


def check_if_data_exists(python_exe):
check_script = (
"import os; "
"os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings'); "
"import django; django.setup(); "
"from data_access.models import Book; "
"print(1 if Book.objects.count()>0 else 0)"
)

try:
result = subprocess.run(
[python_exe, "-c", check_script],
capture_output=True,
text=True,
cwd=os.path.join(os.getcwd(), "src"),
)

return result.returncode == 0 and result.stdout.strip() == "1"
except Exception as e:
print(f"\033[93mWarning: Could not check database: {e}\033[0m")
return False


def download_or_get_csv():
csv_path = os.path.join("src", "data_access", "merged_dataframe.csv")

if os.path.exists(csv_path):
print("\033[92mCSV file found.\033[0m")
return csv_path

print("\033[93mCSV file not found. Downloading...\033[0m")
url = "https://drive.usercontent.google.com/download?id=1MVRHs_CwKTBR2Rpakx920f277IcJ0q6X&authuser=0&confirm=t"
zip_path = os.path.join("src", "data_access", "merged_dataframe.zip")

try:
os.makedirs(os.path.dirname(csv_path), exist_ok=True)

# Download the ZIP file
print("\033[92mDownloading ZIP file...\033[0m")
with urllib.request.urlopen(url) as response:
if response.getcode() != 200:
raise Exception(f"HTTP error: {response.getcode()}")

with open(zip_path, "wb") as file:
chunk_size = 8192
while True:
chunk = response.read(chunk_size)
if not chunk:
break
file.write(chunk)

# Extract the ZIP file
print("\033[92mExtracting ZIP file...\033[0m")
data_access_dir = os.path.join("src", "data_access")
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(data_access_dir)

# Remove the ZIP file after extraction
os.remove(zip_path)

if os.path.exists(csv_path):
print("\033[92mCSV file extracted successfully.\033[0m")
return csv_path
else:
print("\033[91mNo CSV file found in the extracted ZIP.\033[0m")
return None

except Exception as e:
print(f"\033[91mError processing file: {e}\033[0m")
return None


def main():
create_venv_if_not_exists()
python_exe = get_venv_python_path()

run_command(
f"{python_exe} -m pip install -r requirements.txt",
"Installing requirements",
)
run_command(
f"{python_exe} src/manage.py makemigrations",
"Making migrations",
)
run_command(f"{python_exe} src/manage.py migrate", "Running migrations")

create_superuser(python_exe)

print("\033[92mChecking if data needs to be imported...\033[0m")
if not check_if_data_exists(python_exe):
csv_path = download_or_get_csv()
if not csv_path:
print("\033[91mFailed to get CSV file. Exiting.\033[0m")
exit(1)
run_command(
f"{python_exe} src/manage.py import_data {csv_path}",
"Importing data from CSV",
)
else:
print("\033[92mData already exists in the database. Skipping import.\033[0m")

args = parse_arguments()
if args.setup_only:
print("\033[92mSetup complete.\033[0m")
else:
print("\033[92mStarting development server...\033[0m")
print("\033[93mPress Ctrl+C to stop the server\033[0m")
subprocess.call([python_exe, "src/manage.py", "runserver"])


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\033[93mInterrupted. Exiting.\033[0m")
except Exception as e:
print(f"\n\033[91mAn error occurred: {e}\033[0m")
exit(1)
63 changes: 63 additions & 0 deletions src/business_logic/bfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from collections import defaultdict, deque
from typing import List

from data_access.models import Book


class GraphRecommender:
"""
Graph-based recommender using BFS.
Nodes: books; edges: same author or same category.
"""

@staticmethod
def get_recommendations(
start_book_id: str, max_depth: int = 2, max_results: int = 10
) -> List[Book]:
# Build feature maps: author -> books, category -> books
author_map = defaultdict(set)
category_map = defaultdict(set)

all_books = list(Book.objects.only("id", "authors", "categories")) # O(n)
for b in all_books: # O(n * f)
if b.authors:
for author in b.authors.split(","):
author_map[author.strip().lower()].add(b.id)
if b.categories:
for cat in b.categories.split(","):
category_map[cat.strip().lower()].add(b.id)

visited = set([start_book_id])
queue = deque([(start_book_id, 0)])
recommendations = []

while queue and len(recommendations) < max_results:
current_id, depth = queue.popleft()
if depth >= max_depth:
continue

# neighbors by author
b = Book.objects.only("id", "authors", "categories").get(pk=current_id)
neighbors = set()
if b.authors:
for author in b.authors.split(","):
neighbors |= author_map[author.strip().lower()]
if b.categories:
for cat in b.categories.split(","):
neighbors |= category_map[cat.strip().lower()]

for nb_id in neighbors:
if nb_id not in visited:
visited.add(nb_id)
queue.append((nb_id, depth + 1))
if nb_id != start_book_id:
recommendations.append(nb_id)
if len(recommendations) >= max_results:
break

# Fetch Book instances preserving order
books = list(Book.objects.filter(id__in=recommendations))
# maintain recommendation order
id_to_book = {book.id: book for book in books}
ordered = [id_to_book[rid] for rid in recommendations if rid in id_to_book]
return ordered
Loading