feat(search): added basic crude search to project
This commit is contained in:
BIN
bookEx/bookEx/static/uploads/placeholder.png
Normal file
BIN
bookEx/bookEx/static/uploads/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
60
bookEx/bookEx/templates/bookMng/search.html
Normal file
60
bookEx/bookEx/templates/bookMng/search.html
Normal 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 %}
|
||||
0
bookEx/bookMng/management/__init__.py
Normal file
0
bookEx/bookMng/management/__init__.py
Normal file
0
bookEx/bookMng/management/commands/__init__.py
Normal file
0
bookEx/bookMng/management/commands/__init__.py
Normal file
157
bookEx/bookMng/management/commands/seed_books.py
Normal file
157
bookEx/bookMng/management/commands/seed_books.py
Normal 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}"
|
||||
)
|
||||
)
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user