feat(search): added basic crude search to project

This commit is contained in:
DEV-KYO
2026-04-17 03:37:55 -07:00
parent 6b79752bb9
commit 4eda5fbf1e
7 changed files with 258 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

View File

@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% load static %}
{% block sidenav %}
{% for item in item_list %}
<li>
<a href="{{ item.link }}">{{ item.item }}</a>
</li>
{% endfor %}
{% endblock sidenav %}
{% block content %}
<h1>Search Books</h1>
<form method="get" action="{% url 'search' %}" style="margin-bottom: 20px;">
<input type="text" name="q" value="{{ query }}" placeholder="Search by book name..." style="padding: 6px; width: 300px;">
<button type="submit" style="padding: 6px 12px;">Search</button>
</form>
{% if searched %}
{% if books %}
<h2>Results for "{{ query }}"</h2>
<p>{{ books|length }} book{{ books|length|pluralize }} found</p>
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
{% for book in books %}
<div style="border: 1px solid #ccc; padding: 10px; width: 220px;">
<a href="{% url 'book_detail' book.id %}">
<img src="{% static book.pic_path %}" alt="{{ book.name }}" style="max-width: 200px; max-height: 200px;">
<h3>{{ book.name }}</h3>
</a>
<p>Price: ${{ book.price }}</p>
<p>Posted by: {{ book.username }}</p>
</div>
{% endfor %}
</div>
{% elif related %}
<h2>No exact matches for "{{ query }}"</h2>
<p>Here are some related books you might like:</p>
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
{% for book in related %}
<div style="border: 1px solid #ccc; padding: 10px; width: 220px;">
<a href="{% url 'book_detail' book.id %}">
<img src="{% static book.pic_path %}" alt="{{ book.name }}" style="max-width: 200px; max-height: 200px;">
<h3>{{ book.name }}</h3>
</a>
<p>Price: ${{ book.price }}</p>
<p>Posted by: {{ book.username }}</p>
</div>
{% endfor %}
</div>
{% else %}
<h2>No books found</h2>
<p>There are no books in the system yet. Try posting one!</p>
{% endif %}
{% else %}
<p>Enter a search term above to find books.</p>
{% endif %}
{% endblock content %}

View File

View File

@@ -0,0 +1,157 @@
# Management command to seed the database with test books.
# Usage: python manage.py seed_books
# Or: python manage.py seed_books --count 500
# Or: python manage.py seed_books --clear (wipes existing books first)
import random
import base64
from pathlib import Path
from decimal import Decimal
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
from bookMng.models import Book
# Minimal 1x1 transparent PNG (base64). Used as placeholder image for all seeded books.
PLACEHOLDER_PNG_B64 = (
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlE"
"QVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
)
# Name generator components for varied, searchable titles
SUBJECTS = [
"Python", "JavaScript", "Java", "C++", "Rust", "Go", "Ruby", "Swift",
"Algorithms", "Data Structures", "Machine Learning", "Deep Learning",
"Neural Networks", "Databases", "Web Development", "Cloud Computing",
"Cybersecurity", "DevOps", "Docker", "Kubernetes", "Linux",
"Calculus", "Linear Algebra", "Statistics", "Discrete Math",
"Physics", "Chemistry", "Biology", "Astronomy", "Psychology",
"History", "Philosophy", "Economics", "Finance", "Marketing",
"Cooking", "Gardening", "Photography", "Drawing", "Music Theory",
"Guitar", "Piano", "Chess", "Poker", "Mindfulness",
"Fantasy", "Science Fiction", "Mystery", "Romance", "Thriller",
]
ADJECTIVES = [
"Complete", "Essential", "Practical", "Advanced", "Beginner's",
"Modern", "Classic", "Ultimate", "Concise", "Illustrated",
"Pocket", "Comprehensive", "Quick", "Deep", "Hands-On",
]
AUDIENCES = [
"Beginners", "Students", "Professionals", "Developers", "Everyone",
"Kids", "Experts", "Hobbyists", "Engineers", "Researchers",
]
TEMPLATES = [
"{adj} {subj}",
"{adj} Guide to {subj}",
"{subj} for {aud}",
"The Art of {subj}",
"Introduction to {subj}",
"Mastering {subj}",
"Learning {subj}",
"{subj} Cookbook",
"{subj} in Action",
"The {subj} Handbook",
"Understanding {subj}",
"Practical {subj}",
"{subj} Fundamentals",
"The Book of {subj}",
"{subj}: A {adj} Approach",
]
def generate_book_name():
template = random.choice(TEMPLATES)
return template.format(
adj=random.choice(ADJECTIVES),
subj=random.choice(SUBJECTS),
aud=random.choice(AUDIENCES),
)
def ensure_placeholder_image():
# Creates the placeholder image in the uploads directory if it doesn't exist.
# Returns the relative path Django should store for the picture field.
base_dir = Path(settings.BASE_DIR)
upload_dir = base_dir / "bookEx" / "static" / "uploads"
upload_dir.mkdir(parents=True, exist_ok=True)
placeholder_path = upload_dir / "placeholder.png"
if not placeholder_path.exists():
with open(placeholder_path, "wb") as f:
f.write(base64.b64decode(PLACEHOLDER_PNG_B64))
# Path stored in FileField (relative to project)
return "bookEx/static/uploads/placeholder.png"
class Command(BaseCommand):
help = "Seed the database with fake books for testing"
def add_arguments(self, parser):
parser.add_argument(
"--count",
type=int,
default=1000,
help="Number of books to create (default: 1000)",
)
parser.add_argument(
"--clear",
action="store_true",
help="Delete all existing books before seeding",
)
def handle(self, *args, **options):
count = options["count"]
clear = options["clear"]
# Make sure we have a user to attach books to
user, created = User.objects.get_or_create(username="seeduser")
if created:
user.set_password("seedpassword123")
user.save()
self.stdout.write(self.style.SUCCESS("Created test user 'seeduser'"))
# Set up placeholder image
picture_path = ensure_placeholder_image()
self.stdout.write(f"Using placeholder image: {picture_path}")
# Optionally clear existing books
if clear:
deleted, _ = Book.objects.all().delete()
self.stdout.write(
self.style.WARNING(f"Deleted {deleted} existing book(s)")
)
# Bulk create books for speed
self.stdout.write(f"Generating {count} books...")
books_to_create = []
for i in range(count):
books_to_create.append(Book(
name=generate_book_name(),
web=f"https://example.com/books/{i}",
price=Decimal(str(round(random.uniform(5.00, 89.99), 2))),
picture=picture_path,
username=user,
))
# Flush in batches of 500 to keep memory reasonable
if len(books_to_create) >= 500:
Book.objects.bulk_create(books_to_create)
books_to_create = []
self.stdout.write(f" ...created {i + 1} so far")
if books_to_create:
Book.objects.bulk_create(books_to_create)
total = Book.objects.count()
self.stdout.write(
self.style.SUCCESS(
f"Done! Created {count} books. Total books in database: {total}"
)
)

View File

@@ -7,6 +7,7 @@ urlpatterns = [path('',views.index,name='index'),
path('book_detail/<int:book_id>', views.book_detail, name='book_detail'),
path('mybooks', views.mybooks, name='mybooks'),
path('book_delete/<int:book_id>', views.book_delete, name='book_delete'),
path('search', views.search_books, name='search'),
path("inbox/", views.inbox, name="inbox"),
path("compose/", views.compose_message, name="compose_message"),
path("thread/<int:thread_id>/", views.thread_detail, name="thread_detail"),

View File

@@ -253,6 +253,46 @@ def mark_thread_read(request: HttpRequest, thread_id: int) -> HttpResponse:
messages.success(request, "Thread marked as read.")
return redirect("thread_detail", thread_id=thread.id)
def search_books(request):
query = request.GET.get('q', '').strip()
books = []
related = []
searched = False
if query:
searched = True
# primary search: match book name (case-insensitive partial match)
books = list(Book.objects.filter(name__icontains=query))
# fallback: if no direct hits, try matching individual words
if not books:
words = query.split()
q_filter = Q()
for word in words:
q_filter |= Q(name__icontains=word)
if q_filter:
related = list(Book.objects.filter(q_filter).distinct())
# final fallback: show some recent books as suggestions
if not books and not related:
related = list(Book.objects.all().order_by('-id')[:5])
# attach pic_path for template rendering
for b in books:
b.pic_path = b.picture.url[14:]
for b in related:
b.pic_path = b.picture.url[14:]
return render(request,
'bookMng/search.html',
{
'item_list': MainMenu.objects.all(),
'books': books,
'related': related,
'query': query,
'searched': searched,
})
class Register(CreateView):
template_name = 'registration/register.html'
form_class = UserCreationForm