From b45660fbcf5dd22188354bfa0193845e568bda53 Mon Sep 17 00:00:00 2001
From: Mole Shang <135e2@135e2.dev>
Date: Sat, 24 Jan 2026 19:47:41 +0800
Subject: initial commit
---
.gitignore | 15 +
.python-version | 1 +
README.md | 0
bookstore_project/__init__.py | 0
bookstore_project/asgi.py | 16 +
bookstore_project/settings.py | 128 ++++++++
bookstore_project/urls.py | 26 ++
bookstore_project/wsgi.py | 16 +
main.py | 6 +
manage.py | 22 ++
pyproject.toml | 11 +
store/__init__.py | 0
store/admin.py | 18 +
store/apps.py | 5 +
store/forms.py | 61 ++++
store/migrations/0001_initial.py | 248 ++++++++++++++
store/migrations/0002_create_superuser.py | 26 ++
store/migrations/__init__.py | 0
store/models.py | 59 ++++
store/templates/store/.book_list.html.kate-swp | 0
store/templates/store/add_edit_book.html | 84 +++++
store/templates/store/base.html | 83 +++++
store/templates/store/book_detail.html | 69 ++++
store/templates/store/book_list.html | 221 +++++++++++++
store/templates/store/cart.html | 52 +++
store/templates/store/checkout.html | 47 +++
store/templates/store/delete_book_confirm.html | 16 +
store/templates/store/import_books.html | 46 +++
store/templates/store/login.html | 32 ++
store/templates/store/order_list.html | 56 ++++
store/templates/store/order_management.html | 111 +++++++
store/templates/store/profile.html | 39 +++
store/templates/store/register.html | 49 +++
store/tests.py | 3 +
store/urls.py | 24 ++
store/views.py | 433 +++++++++++++++++++++++++
uv.lock | 103 ++++++
37 files changed, 2126 insertions(+)
create mode 100644 .gitignore
create mode 100644 .python-version
create mode 100644 README.md
create mode 100644 bookstore_project/__init__.py
create mode 100644 bookstore_project/asgi.py
create mode 100644 bookstore_project/settings.py
create mode 100644 bookstore_project/urls.py
create mode 100644 bookstore_project/wsgi.py
create mode 100644 main.py
create mode 100755 manage.py
create mode 100644 pyproject.toml
create mode 100644 store/__init__.py
create mode 100644 store/admin.py
create mode 100644 store/apps.py
create mode 100644 store/forms.py
create mode 100644 store/migrations/0001_initial.py
create mode 100644 store/migrations/0002_create_superuser.py
create mode 100644 store/migrations/__init__.py
create mode 100644 store/models.py
create mode 100644 store/templates/store/.book_list.html.kate-swp
create mode 100644 store/templates/store/add_edit_book.html
create mode 100644 store/templates/store/base.html
create mode 100644 store/templates/store/book_detail.html
create mode 100644 store/templates/store/book_list.html
create mode 100644 store/templates/store/cart.html
create mode 100644 store/templates/store/checkout.html
create mode 100644 store/templates/store/delete_book_confirm.html
create mode 100644 store/templates/store/import_books.html
create mode 100644 store/templates/store/login.html
create mode 100644 store/templates/store/order_list.html
create mode 100644 store/templates/store/order_management.html
create mode 100644 store/templates/store/profile.html
create mode 100644 store/templates/store/register.html
create mode 100644 store/tests.py
create mode 100644 store/urls.py
create mode 100644 store/views.py
create mode 100644 uv.lock
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cedd0d0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
+
+# data files
+*.jsonl
+*.sqlite3*
+media/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..6324d40
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.14
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/bookstore_project/__init__.py b/bookstore_project/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bookstore_project/asgi.py b/bookstore_project/asgi.py
new file mode 100644
index 0000000..c542d2f
--- /dev/null
+++ b/bookstore_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for bookstore_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore_project.settings")
+
+application = get_asgi_application()
diff --git a/bookstore_project/settings.py b/bookstore_project/settings.py
new file mode 100644
index 0000000..7f8b1c5
--- /dev/null
+++ b/bookstore_project/settings.py
@@ -0,0 +1,128 @@
+"""
+Django settings for temp_project project.
+
+Generated by 'django-admin startproject' using Django 6.0.1.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/6.0/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-mpnnw2d=87-u)6+vmidshrn2)2(lt497%j+4w$4*t^nv840nwk"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "mathfilters",
+ "store",
+]
+
+AUTH_USER_MODEL = "store.User"
+
+LOGIN_URL = "/login"
+
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "bookstore_project.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "bookstore_project.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/6.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/6.0/howto/static-files/
+
+STATIC_URL = "static/"
+
+MEDIA_URL = "/media/"
+MEDIA_ROOT = BASE_DIR / "media"
+
diff --git a/bookstore_project/urls.py b/bookstore_project/urls.py
new file mode 100644
index 0000000..0cfd748
--- /dev/null
+++ b/bookstore_project/urls.py
@@ -0,0 +1,26 @@
+"""
+URL configuration for bookstore_project project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/6.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+from django.contrib import admin
+from django.urls import path, include
+from django.conf import settings
+from django.conf.urls.static import static
+
+urlpatterns = [
+ path("admin/", admin.site.urls),
+ path("", include("store.urls")),
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/bookstore_project/wsgi.py b/bookstore_project/wsgi.py
new file mode 100644
index 0000000..64b0c49
--- /dev/null
+++ b/bookstore_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for bookstore_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore_project.settings")
+
+application = get_wsgi_application()
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..a08d28b
--- /dev/null
+++ b/main.py
@@ -0,0 +1,6 @@
+def main():
+ print("Hello from bookstore!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..598c795
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bookstore_project.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d67fc9d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,11 @@
+[project]
+name = "bookstore"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.14"
+dependencies = [
+ "django>=6.0.1",
+ "django-mathfilters>=1.0.0",
+ "pillow>=12.1.0",
+]
diff --git a/store/__init__.py b/store/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/store/admin.py b/store/admin.py
new file mode 100644
index 0000000..e010c58
--- /dev/null
+++ b/store/admin.py
@@ -0,0 +1,18 @@
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from .models import Book, User, Order, OrderItem, Comment
+
+class UserAdmin(BaseUserAdmin):
+ fieldsets = BaseUserAdmin.fieldsets + (
+ (None, {'fields': ('avatar', 'phone', 'address', 'name', 'roles')}),
+ )
+ add_fieldsets = BaseUserAdmin.add_fieldsets + (
+ (None, {'fields': ('email', 'phone', 'address', 'name')}),
+ )
+ list_display = BaseUserAdmin.list_display + ('phone', 'is_staff')
+
+admin.site.register(Book)
+admin.site.register(User, UserAdmin)
+admin.site.register(Order)
+admin.site.register(OrderItem)
+admin.site.register(Comment)
\ No newline at end of file
diff --git a/store/apps.py b/store/apps.py
new file mode 100644
index 0000000..c567eb0
--- /dev/null
+++ b/store/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class StoreConfig(AppConfig):
+ name = "store"
diff --git a/store/forms.py b/store/forms.py
new file mode 100644
index 0000000..1a07b6b
--- /dev/null
+++ b/store/forms.py
@@ -0,0 +1,61 @@
+from django import forms
+from django.conf import settings
+from django.contrib.auth.forms import UserCreationForm
+from django.core.files.storage import default_storage
+from .models import User, Book
+import os
+import hashlib
+import io
+
+class UserRegisterForm(UserCreationForm):
+ class Meta:
+ model = User
+ fields = ['username', 'email', 'phone', 'address', 'name']
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.roles = ['user']
+ if commit:
+ user.save()
+ return user
+
+class BookForm(forms.ModelForm):
+ cover_url = forms.URLField(required=False, label="Cover Image URL")
+ cover_upload = forms.ImageField(required=False, label="Cover Image File Upload")
+
+ class Meta:
+ model = Book
+ fields = ['title', 'author', 'publisher', 'price', 'stock', 'isbn', 'description']
+
+ def clean(self):
+ cleaned_data = super().clean()
+ cover_url = cleaned_data.get("cover_url")
+ cover_upload = cleaned_data.get("cover_upload")
+ if cover_url and cover_upload:
+ raise forms.ValidationError("Please provide either a URL or an upload, not both.")
+ return cleaned_data
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+ cover_url = self.cleaned_data.get('cover_url')
+ cover_upload = self.cleaned_data.get('cover_upload')
+
+ if cover_url:
+ instance.cover = cover_url
+ elif cover_upload:
+ file_content = cover_upload.read()
+ file_hash = hashlib.md5(file_content).hexdigest()[:16] # Using first 16 chars for brevity
+ cover_upload.seek(0)
+
+ name, ext = os.path.splitext(cover_upload.name)
+ unique_filename = f"{name}_{file_hash}{ext}"
+
+ file_name = default_storage.save(os.path.join('covers', unique_filename), cover_upload)
+ instance.cover = os.path.join(settings.MEDIA_URL, file_name)
+
+ if commit:
+ instance.save()
+ return instance
+
+class ImportBookForm(forms.Form):
+ file = forms.FileField(label="JSONL File")
diff --git a/store/migrations/0001_initial.py b/store/migrations/0001_initial.py
new file mode 100644
index 0000000..e966d1b
--- /dev/null
+++ b/store/migrations/0001_initial.py
@@ -0,0 +1,248 @@
+# Generated by Django 6.0.1 on 2026-01-24 11:45
+
+import django.contrib.auth.models
+import django.contrib.auth.validators
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("auth", "0012_alter_user_first_name_max_length"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Book",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=255)),
+ ("author", models.CharField(max_length=255)),
+ ("publisher", models.CharField(max_length=255)),
+ ("price", models.IntegerField()),
+ ("stock", models.IntegerField()),
+ ("isbn", models.CharField(max_length=20, unique=True)),
+ ("cover", models.CharField(blank=True, max_length=2048, null=True)),
+ ("description", models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="User",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("password", models.CharField(max_length=128, verbose_name="password")),
+ (
+ "last_login",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="last login"
+ ),
+ ),
+ (
+ "is_superuser",
+ models.BooleanField(
+ default=False,
+ help_text="Designates that this user has all permissions without explicitly assigning them.",
+ verbose_name="superuser status",
+ ),
+ ),
+ (
+ "username",
+ models.CharField(
+ error_messages={
+ "unique": "A user with that username already exists."
+ },
+ help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
+ max_length=150,
+ unique=True,
+ validators=[
+ django.contrib.auth.validators.UnicodeUsernameValidator()
+ ],
+ verbose_name="username",
+ ),
+ ),
+ (
+ "first_name",
+ models.CharField(
+ blank=True, max_length=150, verbose_name="first name"
+ ),
+ ),
+ (
+ "last_name",
+ models.CharField(
+ blank=True, max_length=150, verbose_name="last name"
+ ),
+ ),
+ (
+ "is_staff",
+ models.BooleanField(
+ default=False,
+ help_text="Designates whether the user can log into this admin site.",
+ verbose_name="staff status",
+ ),
+ ),
+ (
+ "is_active",
+ models.BooleanField(
+ default=True,
+ help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
+ verbose_name="active",
+ ),
+ ),
+ (
+ "date_joined",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date joined"
+ ),
+ ),
+ (
+ "avatar",
+ models.ImageField(blank=True, null=True, upload_to="avatars/"),
+ ),
+ ("email", models.EmailField(max_length=254, unique=True)),
+ ("phone", models.CharField(max_length=20, unique=True)),
+ ("address", models.CharField(blank=True, max_length=255, null=True)),
+ ("name", models.CharField(blank=True, max_length=255, null=True)),
+ ("roles", models.JSONField(default=list)),
+ (
+ "groups",
+ models.ManyToManyField(
+ blank=True,
+ help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.group",
+ verbose_name="groups",
+ ),
+ ),
+ (
+ "user_permissions",
+ models.ManyToManyField(
+ blank=True,
+ help_text="Specific permissions for this user.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.permission",
+ verbose_name="user permissions",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "user",
+ "verbose_name_plural": "users",
+ "abstract": False,
+ },
+ managers=[
+ ("objects", django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Comment",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("content", models.TextField()),
+ ("createdAt", models.DateTimeField(auto_now_add=True)),
+ (
+ "book",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="store.book"
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Order",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("address", models.CharField(max_length=255)),
+ ("createdAt", models.DateTimeField(auto_now_add=True)),
+ (
+ "status",
+ models.SmallIntegerField(
+ choices=[(1, "Pending"), (2, "Shipped"), (3, "Rejected")],
+ default=1,
+ ),
+ ),
+ (
+ "buyer",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="OrderItem",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("amount", models.IntegerField()),
+ ("bookPrice", models.IntegerField()),
+ (
+ "book",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="store.book"
+ ),
+ ),
+ (
+ "order",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="items",
+ to="store.order",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/store/migrations/0002_create_superuser.py b/store/migrations/0002_create_superuser.py
new file mode 100644
index 0000000..4f322d6
--- /dev/null
+++ b/store/migrations/0002_create_superuser.py
@@ -0,0 +1,26 @@
+from django.db import migrations
+import os
+
+def create_superuser(apps, schema_editor):
+ User = apps.get_model('store', 'User')
+ db_alias = schema_editor.connection.alias
+
+ if not User.objects.using(db_alias).filter(username='admin').exists():
+ User.objects.create_superuser(
+ username=os.environ.get('DJANGO_SUPERUSER_USERNAME', 'admin'),
+ email=os.environ.get('DJANGO_SUPERUSER_EMAIL', 'admin@example.com'),
+ password=os.environ.get('DJANGO_SUPERUSER_PASSWORD', 'admin'),
+ is_staff=True,
+ is_superuser=True,
+ roles=['user', 'admin']
+ )
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('store', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(create_superuser, migrations.RunPython.noop),
+ ]
diff --git a/store/migrations/__init__.py b/store/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/store/models.py b/store/models.py
new file mode 100644
index 0000000..9e321fc
--- /dev/null
+++ b/store/models.py
@@ -0,0 +1,59 @@
+from django.contrib.auth.models import AbstractUser
+from django.db import models
+
+class User(AbstractUser):
+ avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
+ email = models.EmailField(unique=True)
+ phone = models.CharField(max_length=20, unique=True)
+ address = models.CharField(max_length=255, null=True, blank=True)
+ name = models.CharField(max_length=255, null=True, blank=True)
+ roles = models.JSONField(default=list)
+
+ def save(self, *args, **kwargs):
+ if not self.roles:
+ self.roles = ['user']
+ if (self.is_staff or self.is_superuser) and 'admin' not in self.roles:
+ self.roles.append('admin')
+ super().save(*args, **kwargs)
+
+class Book(models.Model):
+ title = models.CharField(max_length=255)
+ author = models.CharField(max_length=255)
+ publisher = models.CharField(max_length=255)
+ price = models.IntegerField()
+ stock = models.IntegerField()
+ isbn = models.CharField(max_length=20, unique=True)
+ cover = models.CharField(max_length=2048, null=True, blank=True)
+ description = models.TextField(null=True, blank=True)
+
+
+class Order(models.Model):
+ STATUS_CHOICES = [
+ (1, 'Pending'),
+ (2, 'Shipped'),
+ (3, 'Rejected'),
+ ]
+ buyer = models.ForeignKey(User, on_delete=models.CASCADE)
+ address = models.CharField(max_length=255)
+ createdAt = models.DateTimeField(auto_now_add=True)
+ status = models.SmallIntegerField(choices=STATUS_CHOICES, default=1)
+
+ @property
+ def total_price(self):
+ return sum(item.total_price for item in self.items.all())
+
+class OrderItem(models.Model):
+ order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
+ book = models.ForeignKey(Book, on_delete=models.CASCADE)
+ amount = models.IntegerField()
+ bookPrice = models.IntegerField()
+
+ @property
+ def total_price(self):
+ return self.bookPrice * self.amount
+
+class Comment(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ book = models.ForeignKey(Book, on_delete=models.CASCADE)
+ content = models.TextField()
+ createdAt = models.DateTimeField(auto_now_add=True)
diff --git a/store/templates/store/.book_list.html.kate-swp b/store/templates/store/.book_list.html.kate-swp
new file mode 100644
index 0000000..e69de29
diff --git a/store/templates/store/add_edit_book.html b/store/templates/store/add_edit_book.html
new file mode 100644
index 0000000..c87a9c7
--- /dev/null
+++ b/store/templates/store/add_edit_book.html
@@ -0,0 +1,84 @@
+{% extends 'store/base.html' %}
+{% load static %}
+
+{% block content %}
+
+
+ {{ title }}
+
+{% endblock %}
+
+{% block extra_script %}
+
+{% endblock %}
diff --git a/store/templates/store/base.html b/store/templates/store/base.html
new file mode 100644
index 0000000..18a9557
--- /dev/null
+++ b/store/templates/store/base.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ SEU Bookstore
+
+
+
+
+
+
+
+
+
+
+ SEU Bookstore
+
+
+ {% if user.is_authenticated and not user.avatar %}{{ user.username|first|upper }}{% endif %}
+
+ {% if user.is_authenticated %}
+ Profile
+ Logout
+ {% else %}
+ Login
+ Register
+ {% endif %}
+
+
+
+
+
+ Books
+ Cart
+ Orders
+
+
+
+ {% if user.is_staff %}
+ Manage
+ {% endif %}
+
+
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
+
+
+ {% block extra_script %}{% endblock %}
+
+
+
diff --git a/store/templates/store/book_detail.html b/store/templates/store/book_detail.html
new file mode 100644
index 0000000..e9cc2b8
--- /dev/null
+++ b/store/templates/store/book_detail.html
@@ -0,0 +1,69 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+
+
+ {% if book.cover %}
+

+ {% else %}
+
+ No Image Available
+
+ {% endif %}
+
+
+
+
{{ book.title }}
+
{{ book.author }}
+
Publisher: {{ book.publisher }}
+
ISBN: {{ book.isbn }}
+
Price: {{ book.price }}
+
Stock: {{ book.stock }}
+
{{ book.description|safe }}
+
+
+
+
+ Back to List
+
+ {% if user.is_staff %}
+ Edit
+ Delete
+ {% endif %}
+
+
+
+
+
+
+
+
Comments
+ {% if user.is_authenticated %}
+
+ {% else %}
+
Login to post a comment.
+ {% endif %}
+
+
+ {% for comment in comments %}
+
+
+ {{ comment.user.username }}
+ {{ comment.createdAt|date:"M d, Y H:i" }}
+
+ {{ comment.content }}
+
+ {% empty %}
+
No comments yet.
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/store/templates/store/book_list.html b/store/templates/store/book_list.html
new file mode 100644
index 0000000..5d68340
--- /dev/null
+++ b/store/templates/store/book_list.html
@@ -0,0 +1,221 @@
+{% extends 'store/base.html' %}
+{% load mathfilters %}
+{% block content %}
+
+
+
Books
+
+ Search
+ {% if user.is_staff %}
+ Import Books
+ Add Book
+ {% endif %}
+
+
+
+
+
+
+
+ {% for book in books %}
+
+
+ {% if book.cover %}
+

+ {% else %}
+
+ No Image
+
+ {% endif %}
+
+
+
+
+ {{ book.title }}
+
+
+ {{ book.author }}
+
+
+
+
+
{{ book.price }}¥
+
+
+
+
+
+
+
+ {% empty %}
+
+
+
+
No books yet
+ {% if query %}
+
We couldn't find any books matching "{{ query }}".
+
Clear Search
+ {% else %}
+
Check back later!
+ {% endif %}
+
+
+ {% endfor %}
+
+
+{% if books.paginator.num_pages > 1 %}
+
+
+
+
+ {% if books.has_previous %}{{ books.number|sub:1 }}{% endif %}
+
+
+
+
+
+
+ of {{ books.paginator.num_pages }}
+
+
+
+
+
+ {% if books.has_next %}{{ books.number|add:1 }}{% endif %}
+
+
+
+
+{% endif %}
+{% endblock %}
+
+{% block extra_script %}
+
+{% endblock %}
diff --git a/store/templates/store/cart.html b/store/templates/store/cart.html
new file mode 100644
index 0000000..f634c58
--- /dev/null
+++ b/store/templates/store/cart.html
@@ -0,0 +1,52 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+ Shopping Cart
+ {% if cart_items %}
+
+
+
+
+ | Book |
+ Price |
+ Quantity |
+ Total |
+ Actions |
+
+
+
+ {% for item in cart_items %}
+
+ | {{ item.book.title }} |
+ {{ item.book.price }}¥ |
+
+
+ |
+ {{ item.total_price }}¥ |
+
+ Remove
+ |
+
+ {% endfor %}
+
+
+
+ | Total: |
+ {{ total_price }}¥ |
+ |
+
+
+
+
+
+ Checkout
+
+ {% else %}
+ Your cart is empty.
+ Go Shopping
+ {% endif %}
+{% endblock %}
diff --git a/store/templates/store/checkout.html b/store/templates/store/checkout.html
new file mode 100644
index 0000000..11dc99c
--- /dev/null
+++ b/store/templates/store/checkout.html
@@ -0,0 +1,47 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+ Checkout
+
+
+
Order Summary
+
+
+
+ | Book |
+ Price |
+ Quantity |
+ Total |
+
+
+
+ {% for item in cart_items %}
+
+ | {{ item.book.title }} |
+ ${{ item.book.price }} |
+ {{ item.quantity }} |
+ ${{ item.total_price }} |
+
+ {% endfor %}
+
+
+
+ | Total: |
+ ${{ total_price }} |
+
+
+
+
+
+
Shipping Information
+
+
+
+{% endblock %}
+
diff --git a/store/templates/store/delete_book_confirm.html b/store/templates/store/delete_book_confirm.html
new file mode 100644
index 0000000..8450973
--- /dev/null
+++ b/store/templates/store/delete_book_confirm.html
@@ -0,0 +1,16 @@
+{% extends 'store/base.html' %}
+{% load static %}
+
+{% block content %}
+
+
+ Confirm Deletion
+ Are you sure you want to delete "{{ book.title }}"?
+
+
+
+{% endblock %}
diff --git a/store/templates/store/import_books.html b/store/templates/store/import_books.html
new file mode 100644
index 0000000..81d4f0e
--- /dev/null
+++ b/store/templates/store/import_books.html
@@ -0,0 +1,46 @@
+{% extends 'store/base.html' %}
+{% load static %}
+
+{% block content %}
+
+
+ {{ title }}
+
+
Upload a JSONL file to import books. Each line should be a JSON object with a "metadata" key containing book details.
+
+
+
+
+{% endblock %}
+
+{% block extra_script %}
+
+{% endblock %}
diff --git a/store/templates/store/login.html b/store/templates/store/login.html
new file mode 100644
index 0000000..ff34b5e
--- /dev/null
+++ b/store/templates/store/login.html
@@ -0,0 +1,32 @@
+{% extends 'store/base.html' %}
+{% load static %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/store/templates/store/order_list.html b/store/templates/store/order_list.html
new file mode 100644
index 0000000..c45680e
--- /dev/null
+++ b/store/templates/store/order_list.html
@@ -0,0 +1,56 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+ My Orders
+ {% if orders %}
+
+ {% for order in orders %}
+
+
+ Order #{{ order.id }} - {{ order.createdAt|date:"M d, Y" }} - {{ order.total_price }}¥ -
+ {% if order.status == 1 %}
+ Pending
+ {% elif order.status == 2 %}
+ Shipped
+ {% elif order.status == 3 %}
+ Rejected
+ {% endif %}
+
+
+
Shipping Address: {{ order.address }}
+
+
+
+ | Book |
+ Price |
+ Quantity |
+ Subtotal |
+
+
+
+ {% for item in order.items.all %}
+
+ | {{ item.book.title }} |
+ {{ item.bookPrice }}¥ |
+ {{ item.amount }} |
+ {{ item.total_price }}¥ |
+
+ {% endfor %}
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+ You haven't placed any orders yet.
+ {% endif %}
+{% endblock %}
+
diff --git a/store/templates/store/order_management.html b/store/templates/store/order_management.html
new file mode 100644
index 0000000..18b6e4b
--- /dev/null
+++ b/store/templates/store/order_management.html
@@ -0,0 +1,111 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+ Order Management
+
+
+
+
+
+ | ID |
+ Buyer |
+ Address |
+ Date |
+ Total |
+ Status |
+ Actions |
+ |
+
+
+
+ {% for order in orders %}
+
+ | #{{ order.id }} |
+ {{ order.buyer.username }} |
+
+
+ {{ order.address }}
+
+ |
+ {{ order.createdAt|date:"M d, Y H:i" }} |
+ ${{ order.total_price }} |
+
+ {% if order.status == 1 %}
+ Pending
+ {% elif order.status == 2 %}
+ Shipped
+ {% elif order.status == 3 %}
+ Rejected
+ {% endif %}
+ |
+
+
+ {% if order.status == 1 %}
+
+
+ {% else %}
+ {{ order.get_status_display }}
+ {% endif %}
+
+ |
+
+
+ |
+
+
+
+
+ Ordered Items
+
+
+
+ | Book |
+ Price |
+ Quantity |
+ Subtotal |
+
+
+
+ {% for item in order.items.all %}
+
+ | {{ item.book.title }} |
+ ${{ item.bookPrice }} |
+ {{ item.amount }} |
+ ${{ item.total_price }} |
+
+ {% endfor %}
+
+
+
+ |
+
+ {% endfor %}
+
+
+
+{% endblock %}
+
+{% block extra_script %}
+
+{% endblock %}
diff --git a/store/templates/store/profile.html b/store/templates/store/profile.html
new file mode 100644
index 0000000..d06cc7e
--- /dev/null
+++ b/store/templates/store/profile.html
@@ -0,0 +1,39 @@
+{% extends 'store/base.html' %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/store/templates/store/register.html b/store/templates/store/register.html
new file mode 100644
index 0000000..3d68916
--- /dev/null
+++ b/store/templates/store/register.html
@@ -0,0 +1,49 @@
+{% extends 'store/base.html' %}
+{% load static %}
+
+{% block content %}
+
+
+ Register
+
+
+
Already have an account? Login
+
+
+
+{% endblock %}
diff --git a/store/tests.py b/store/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/store/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/store/urls.py b/store/urls.py
new file mode 100644
index 0000000..3c52997
--- /dev/null
+++ b/store/urls.py
@@ -0,0 +1,24 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('', views.book_list, name='book_list'),
+ path('book//', views.book_detail, name='book_detail'),
+ path('cart/', views.view_cart, name='cart_detail'),
+ path('cart/add//', views.add_to_cart, name='add_to_cart'),
+ path('cart/remove//', views.remove_from_cart, name='remove_from_cart'),
+ path('cart/update//', views.update_cart_quantity, name='update_cart_quantity'),
+ path('checkout/', views.checkout, name='checkout'),
+ path('orders/', views.order_list, name='order_list'),
+ path('manage/', views.order_management, name='order_management'),
+ path('orders/update_status//', views.update_order_status, name='update_order_status'),
+ path('book/add/', views.add_book, name='add_book'),
+ path('book/import/', views.import_books, name='import_books'),
+ path('book//edit/', views.edit_book, name='edit_book'),
+ path('book//delete/', views.delete_book, name='delete_book'),
+ path('book//comment/', views.post_comment, name='post_comment'),
+ path('profile/', views.profile_view, name='profile'),
+ path('register/', views.register_view, name='register'),
+ path('login/', views.login_view, name='login'),
+ path('logout/', views.logout_view, name='logout'),
+]
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")
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..6eb819f
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,103 @@
+version = 1
+revision = 3
+requires-python = ">=3.14"
+
+[[package]]
+name = "asgiref"
+version = "3.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
+]
+
+[[package]]
+name = "bookstore"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "django" },
+ { name = "django-mathfilters" },
+ { name = "pillow" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "django", specifier = ">=6.0.1" },
+ { name = "django-mathfilters", specifier = ">=1.0.0" },
+ { name = "pillow", specifier = ">=12.1.0" },
+]
+
+[[package]]
+name = "django"
+version = "6.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "sqlparse" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b5/9b/016f7e55e855ee738a352b05139d4f8b278d0b451bd01ebef07456ef3b0e/django-6.0.1.tar.gz", hash = "sha256:ed76a7af4da21551573b3d9dfc1f53e20dd2e6c7d70a3adc93eedb6338130a5f", size = 11069565, upload-time = "2026-01-06T18:55:53.069Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" },
+]
+
+[[package]]
+name = "django-mathfilters"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/a6/1e8262e52aa3cbfae277bc97c23dd4f1f6c7d7c3adaf45fb41132c312a40/django-mathfilters-1.0.0.tar.gz", hash = "sha256:c9b892ef6dfc893683e75cfd0279c187a601ca68f4684c38f9da44657fb64b07", size = 5699, upload-time = "2020-02-10T12:02:06.569Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/c6/107083a63a564664830e352af330563763654972d27d56e42d9b6e3c744f/django_mathfilters-1.0.0-py3-none-any.whl", hash = "sha256:64200a21bb249fbf27be601d4bbb788779e09c6e063170c097cd82c4d18ebb83", size = 5909, upload-time = "2020-02-10T12:02:09.819Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
+ { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
+ { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
+ { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
+ { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
+ { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
+ { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.5.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
--
cgit v1.2.3