from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import login, authenticate, logout from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.decorators import login_required from django.contrib import messages from django.contrib.auth.decorators import user_passes_test from django.core.paginator import Paginator from django.db.models import Q from django.db import transaction from .forms import UserRegisterForm, BookForm, ImportBookForm from .models import Book, Order, OrderItem, User, Comment import json import threading import os import tempfile # Global dictionary to track import status: {user_id: {'status': 'running'|'done', 'count': 0}} import_status = {} def process_import_thread(user_id, file_path): global import_status count = 0 try: with open(file_path, 'r', encoding='utf-8') as f: for line in f: try: data = json.loads(line) metadata = data.get('metadata') if not metadata: continue isbn = metadata.get('isbn') if not isbn: continue defaults = { 'title': metadata.get('title', 'Unknown Title'), 'author': metadata.get('author', 'Unknown Author'), 'publisher': metadata.get('publisher', 'Unknown Publisher'), 'price': int(metadata.get('price', 50) or 50), 'stock': 10, 'description': metadata.get('description', ''), 'cover': metadata.get('cover', '') } Book.objects.update_or_create( isbn=isbn, defaults=defaults ) count += 1 import_status[user_id]['count'] = count except Exception as e: print(f"Error importing line: {e}") continue except Exception as e: print(f"File processing error: {e}") finally: import_status[user_id]['status'] = 'done' if os.path.exists(file_path): os.remove(file_path) def is_staff(user): return user.is_staff or user.is_superuser or (user.roles and 'admin' in user.roles) @user_passes_test(is_staff) def import_books(request): global import_status user_id = request.user.id if request.method == 'POST': # Block new import if one is already running if user_id in import_status and import_status[user_id]['status'] == 'running': count = import_status[user_id]['count'] messages.warning(request, f"Import already in progress. {count} books imported so far.") return redirect('import_books') form = ImportBookForm(request.POST, request.FILES) if form.is_valid(): uploaded_file = request.FILES['file'] # Save to temporary file to read in thread fd, path = tempfile.mkstemp() try: with os.fdopen(fd, 'wb') as tmp: for chunk in uploaded_file.chunks(): tmp.write(chunk) except Exception as e: os.remove(path) messages.error(request, f"Error saving file: {e}") return redirect('import_books') # Initialize status import_status[user_id] = {'status': 'running', 'count': 0} # Start thread thread = threading.Thread(target=process_import_thread, args=(user_id, path)) thread.daemon = True thread.start() messages.success(request, "Import started in the background. You can continue using the site.") return redirect('book_list') else: form = ImportBookForm() if user_id in import_status and import_status[user_id]['status'] == 'running': messages.info(request, f"An import is currently running. {import_status[user_id]['count']} books imported so far.") return render(request, 'store/import_books.html', {'form': form, 'title': 'Import Books'}) @user_passes_test(is_staff) def add_book(request): if request.method == 'POST': form = BookForm(request.POST, request.FILES) if form.is_valid(): form.save() messages.success(request, "Book added successfully.") return redirect('book_list') else: form = BookForm() return render(request, 'store/add_edit_book.html', {'form': form, 'title': 'Add Book'}) @user_passes_test(is_staff) def edit_book(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': form = BookForm(request.POST, request.FILES, instance=book) if form.is_valid(): form.save() messages.success(request, "Book updated successfully.") return redirect('book_detail', pk=book.pk) else: form = BookForm(instance=book) return render(request, 'store/add_edit_book.html', {'form': form, 'book': book, 'title': 'Edit Book'}) @user_passes_test(is_staff) def delete_book(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': book.delete() messages.success(request, "Book deleted successfully.") return redirect('book_list') return render(request, 'store/delete_book_confirm.html', {'book': book}) @user_passes_test(is_staff) def order_management(request): orders = Order.objects.all().order_by('-createdAt') return render(request, 'store/order_management.html', {'orders': orders}) @user_passes_test(is_staff) def update_order_status(request, pk): order = get_object_or_404(Order, pk=pk) if request.method == 'POST': new_status = request.POST.get('status') if new_status in ['2', '3'] and str(order.status) != new_status: # 2 for shipped, 3 for rejected try: with transaction.atomic(): if new_status == '2': # Shipped if order.status == 1: # Only ship if pending for item in order.items.all(): book = Book.objects.select_for_update().get(pk=item.book.pk) # Stock was already reduced at checkout, no need to reduce again order.status = 2 # Mark as shipped messages.success(request, f"Order #{order.id} marked as shipped.") else: messages.error(request, f"Order #{order.id} cannot be shipped from its current status.") elif new_status == '3': # Rejected if order.status == 1: # Only reject if pending for item in order.items.all(): book = Book.objects.select_for_update().get(pk=item.book.pk) book.stock += item.amount # Add stock back book.save() order.status = 3 # Mark as rejected messages.success(request, f"Order #{order.id} marked as rejected and stock restored.") else: messages.error(request, f"Order #{order.id} cannot be rejected from its current status.") order.save() except Exception as e: messages.error(request, f"Error updating order status: {e}") else: messages.error(request, "Invalid status update.") return redirect('order_management') def book_list(request): query = request.GET.get('q') search_type = request.GET.get('type', 'title') if query: if search_type == 'author': books_queryset = Book.objects.filter(author__icontains=query) elif search_type == 'publisher': books_queryset = Book.objects.filter(publisher__icontains=query) elif search_type == 'isbn': books_queryset = Book.objects.filter(isbn__icontains=query) else: # Default to title search books_queryset = Book.objects.filter(title__icontains=query) else: books_queryset = Book.objects.all() paginator = Paginator(books_queryset, 12) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'store/book_list.html', { 'books': page_obj, 'query': query, 'search_type': search_type }) def book_detail(request, pk): book = get_object_or_404(Book, pk=pk) comments = Comment.objects.filter(book=book).order_by('-createdAt') return render(request, 'store/book_detail.html', {'book': book, 'comments': comments}) @login_required def add_to_cart(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': try: quantity_to_add = int(request.POST.get('quantity', '1')) if quantity_to_add <= 0: raise ValueError except (ValueError, TypeError): messages.error(request, "Invalid quantity.") return redirect(request.META.get('HTTP_REFERER', 'book_list')) cart = request.session.get('cart', {}) current_quantity = cart.get(str(book.id), 0) new_quantity = current_quantity + quantity_to_add if book.stock < new_quantity: messages.error(request, f"Sorry, only {book.stock} copies of '{book.title}' are available.") return redirect(request.META.get('HTTP_REFERER', 'book_list')) cart[str(book.id)] = new_quantity request.session['cart'] = cart request.session.modified = True messages.success(request, f"{quantity_to_add} {'copy' if quantity_to_add == 1 else 'copies'} of '{book.title}' added to your cart.") return redirect('cart_detail') return redirect('book_list') @login_required def view_cart(request): session_cart = request.session.get('cart', {}) cart_items_data = [] total_price = 0 for book_id, quantity in session_cart.items(): book = get_object_or_404(Book, pk=book_id) item_total = book.price * quantity total_price += item_total cart_items_data.append({ 'book': book, 'quantity': quantity, 'total_price': item_total, 'id': book_id # For removing/updating }) return render(request, 'store/cart.html', {'cart_items': cart_items_data, 'total_price': total_price}) @login_required def remove_from_cart(request, pk): cart = request.session.get('cart', {}) book_id_str = str(pk) if book_id_str in cart: del cart[book_id_str] request.session['cart'] = cart request.session.modified = True messages.success(request, "Item removed from cart.") return redirect('cart_detail') @login_required def update_cart_quantity(request, pk): cart = request.session.get('cart', {}) book_id_str = str(pk) if book_id_str not in cart: messages.error(request, "Item not found in cart.") return redirect('cart_detail') book = get_object_or_404(Book, pk=pk) if request.method == 'POST': try: quantity = int(request.POST.get('quantity', 1)) except (ValueError, TypeError): messages.error(request, "Invalid quantity.") return redirect('cart_detail') if quantity > 0: if book.stock < quantity: messages.error(request, f"Sorry, only {book.stock} copies of '{book.title}' are available.") return redirect('cart_detail') cart[book_id_str] = quantity messages.success(request, "Cart updated.") else: del cart[book_id_str] messages.success(request, "Item removed from cart.") request.session['cart'] = cart request.session.modified = True return redirect('cart_detail') @login_required def checkout(request): session_cart = request.session.get('cart', {}) if not session_cart: messages.warning(request, "Your cart is empty.") return redirect('book_list') cart_items_data = [] total_price = 0 for book_id, quantity in session_cart.items(): book = get_object_or_404(Book, pk=book_id) item_total = book.price * quantity total_price += item_total cart_items_data.append({ 'book': book, 'quantity': quantity, 'total_price': item_total, 'id': book_id }) if request.method == 'POST': address = request.POST.get('address') if not address: messages.error(request, "Please provide a shipping address.") return render(request, 'store/checkout.html', {'cart_items': cart_items_data, 'total_price': total_price}) try: with transaction.atomic(): # First, verify stock for all items in the current session cart for item_data in cart_items_data: book = Book.objects.select_for_update().get(pk=item_data['book'].pk) if book.stock < item_data['quantity']: raise ValueError(f"Insufficient stock for '{book.title}'. Only {book.stock} left.") order = Order.objects.create(buyer=request.user, address=address) for item_data in cart_items_data: book = Book.objects.get(pk=item_data['book'].pk) book.stock -= item_data['quantity'] book.save() OrderItem.objects.create( order=order, book=book, amount=item_data['quantity'], bookPrice=book.price ) request.session['cart'] = {} # Clear the cart request.session.modified = True messages.success(request, f"Order #{order.id} placed successfully!") return redirect('order_list') except ValueError as e: messages.error(request, str(e)) return redirect('cart_detail') return render(request, 'store/checkout.html', {'cart_items': cart_items_data, 'total_price': total_price}) @login_required def order_list(request): orders = Order.objects.filter(buyer=request.user).order_by('-createdAt') return render(request, 'store/order_list.html', {'orders': orders}) @login_required def post_comment(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': content = request.POST.get('content') if content: Comment.objects.create(user=request.user, book=book, content=content) messages.success(request, "Comment added.") else: messages.error(request, "Comment cannot be empty.") return redirect('book_detail', pk=pk) @login_required def profile_view(request): if request.method == 'POST': user = request.user user.name = request.POST.get('name') user.phone = request.POST.get('phone') user.address = request.POST.get('address') if 'avatar' in request.FILES: user.avatar = request.FILES['avatar'] user.save() messages.success(request, "Profile updated.") return redirect('profile') return render(request, 'store/profile.html') def register_view(request): if request.method == 'POST': form = UserRegisterForm(request.POST) if form.is_valid(): user = form.save() login(request, user) messages.success(request, "Registration successful.") return redirect('book_list') messages.error(request, "Unsuccessful registration. Invalid information.") else: form = UserRegisterForm() return render(request, 'store/register.html', {'form': form}) def login_view(request): if request.method == "POST": form = AuthenticationForm(request, data=request.POST) if form.is_valid(): username = form.cleaned_data.get('username') password = form.cleaned_data.get('password') user = authenticate(username=username, password=password) if user is not None: login(request, user) messages.info(request, f"You are now logged in as {username}.") return redirect("book_list") else: messages.error(request, "Invalid username or password.") else: messages.error(request, "Invalid username or password.") form = AuthenticationForm() return render(request, 'store/login.html', {"form": form}) def logout_view(request): logout(request) messages.info(request, "You have successfully logged out.") return redirect("book_list")