diff options
Diffstat (limited to 'store/views.py')
| -rw-r--r-- | store/views.py | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/store/views.py b/store/views.py new file mode 100644 index 0000000..e77cd29 --- /dev/null +++ b/store/views.py @@ -0,0 +1,433 @@ +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") |
