使用Django完成的电子拍卖商城【CS50 web commerce】

使用Django完成的电子拍卖商城【CS50 web commerce】

问题的描述

需要设计一个电子拍卖商城,能够允许用户可以上传商品,对商品进行出价,以及进行评论,并将商品添加到watchlist。

商城拍卖系统

最近在学习CS50的web课程,CS50系列课程的最大亮点是一个又一个难度高的project,这个项目我断断续续的花了3,4周的时间完成,克服了很多困难,也学到了很多的知识,我将完整的代码在文末。CS50作业地址在 这里 ,最终商城界面如下:
请添加图片描述

这次项目的任务是创建一个名为"Commerce"的网站。虽然官方提供了代码的模板,但项目本身涵盖了广泛的内容,考验着参与者的综合素质。为了实现整体页面的美观,需要对CSS有深入的了解和掌握。如果你想更进一步地掌握样式的高级和简便控制,就需要了解一些常见的CSS框架,比如Bootstrap等。此外,为了实现出色的用户与前端交互体验,需要使用JavaScript来操作页面上的元素。JavaScript也有许多方便的工具,比如jQuery等,这些工具在处理大型项目时能够提供更便捷的操作。

这些技术都是构建更好网页的辅助工具。然而,在这个项目中,最核心的目标还是掌握Django框架。在CS50课程中,只会论述Django框架的基础知识。但在这个项目中,我们需要制作更复杂的管理应用程序,例如处理复杂的图片上传请求、JavaScript与Django后端的协作,以及使用Django管理界面等等。这些都需要查阅资料,掌握更多技能,来完成应用的目标。

下面我会开始按照项目的完成顺序来介绍项目以及讲解部分代码

数据库模型的设计

1.设计电子拍卖商城模型,商城拥有商品,用户,竞价,评论等多个要素,而且要素之间会互相关联。商品需要关联用户为创造者,竞价有出价者(用户),以及价格两个要素,评论与竞价相同。设计Django模型,也需要数据库的基础以及运用,需要了解建表,数据的增删改查,这些知识。

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    pass

class AuctionListing(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()
    starting_bid = models.DecimalField(max_digits=10, decimal_places=2)
    current_bid = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    image_url = models.ImageField(upload_to='gimages/')
    category = models.CharField(max_length=50, choices=[
        ('Fashion', 'Fashion'),
        ('Toys', 'Toys'),
        ('Electronics', 'Electronics'),
        ('Home', 'Home'),
        # 添加更多类别
    ])
    creator = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return self.title

# 投标模型
class Bid(models.Model):
    listing = models.ForeignKey(AuctionListing, on_delete=models.CASCADE, related_name='bids')
    bidder = models.ForeignKey(User, on_delete=models.CASCADE)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Bid on {self.listing.title} by {self.bidder.username}"

# 评论模型
class Comment(models.Model):
    listing = models.ForeignKey(AuctionListing, on_delete=models.CASCADE, related_name='comments')
    commenter = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Comment on {self.listing.title} by {self.commenter.username}"

最核心的AuctionListing 模型储存着各个商品,这个模型有着title,description等域,以及category域,这个域有着Choice这个参数,规定了category的选项,在前端的表单中,可将category渲染成可选参数的表单.creator这个域关联着创建商品的用户,在程序中,creator可以简单的表示成request.user,Create_at为自动生成的时间域。

值得注意的是creator中有着on_delete选项,这个选项规定了在外键所指的User对象被删除时,如何处理对应的AuctionListing元素。

关于域的设计可以参考Django的models模块,Model field reference | Django documentation | Django,同时这里的image_url域会在后文提到。

Bid 竞价模型,以及Comment评论模型见代码部分。

设计完数据库模型后,我们需要首先创造出一些简单的网页,完成用于上传商品,展示商品等功能 。在Django的应用中,这意味着**views**模块(处理后端视图),**urls**模块(处理访问的url),以及储存模版的**template**文件夹(HTML文件)。

2.模型的图片上传问题 商品界面需要展示图片,我们遇到了图片上传的问题,在这里我参考了很多的博客,来解决媒体文件以及图片上传的问题,我在setting.py 总加入了这几行设置参数,

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

以及在数据库模型中使用ImageField来对应图片文件。

image_url = models.ImageField(upload_to='gimages/')

使用ImageField上传文件后,在需要用到图片的地方使用 Field_Name.url来进行展示图片 , 注意这里的Field_Name是所设置域的名字,url属性返回图片的地址,完成图片的上传,设置后,商品的展示问题也被解决 。

    <div class="listing-item">
    {% if listing.image_url %}
        <img class="l-img" src="{{ listing.image_url.url }}" alt="{{ listing.title }}">
    {% endif %}
解决商品上传问题后的整合难题:Django模板和变量传递

在上文,我们讨论了如何解决单个商品的上传问题,但接下来,我们需要整合商品索引页和商品详情页。这些HTML文件是通过Django模板一个个生成的,因此我们需要学习如何处理文件之间的配合(layout文件),以及文件内部如何与Django程序进行有效的通信,包括变量传递、for循环、if语句和Django URL的运用。

1. 文件的整合与布局

首先,让我们谈一下文件的整合与布局。在Django项目中,通常会有多个HTML文件,包括基础布局文件layout.html 、索引页index.html和详情页detail.html。这些文件之间需要进行协调,引用,以确保整个网站的外观和行为是一致的。

  • 基础布局文件:我们可以为网站提供一个通用的基础HTML文件,其中包含网站的通用结构,例如头部、导航栏和底部。这个基础布局文件通常命名为layout.html

    @
  • 索引页和详情页:对于索引页和详情页,你可以创建单独的模板文件,如index.htmldetail.html。这些文件可以继承基础布局文件,通过{% extends ‘layout.html’ %}语句指定。

  • 块区域:在基础布局文件中,你可以使用块区域(block)来标识可以被子模板覆盖的内容。在子模板中,使用{% block block_name %}…{% endblock %}来定义和覆盖这些块区域。

2. 变量传递

Django模板允许你将变量从视图传递到HTML文件中,以动态生成内容。在视图函数中,你可以使用render函数将变量传递给模板。例如:

from django.shortcuts import render

def index(request):
    products = Product.objects.all()
    return render(request, 'index.html', {'products': products})

index.html模板中,你可以通过双括号语法({{ variable_name }})来显示传递的变量,如:

{% for product in products %}
    <p>{{ product.name }}</p>
{% endfor %}

3. for循环和if语句

在Django模板中,你可以使用{% for %}循环和{% if %}条件语句来处理数据和控制显示。例如,你可以使用for循环遍历商品列表,并使用if语句根据条件显示特定的内容。

{% for product in products %}
    <p>{{ product.name }}</p>
    {% if product.price > 50 %}
        <p>Expensive</p>
    {% else %}
        <p>Affordable</p>
    {% endif %}
{% endfor %}

通过良好的文件布局、变量传递、for循环、if语句,我们可以更加便利的构建一个网站。构建出令人满意的Web应用程序。更多的Django template信息请参阅 Django官方文档

这部分相应的部分代码如下,注意这里展示的是完整的代码,因此有一些函数目前文章中还没有提到相关的知识。

views.py

from django.contrib.auth import authenticate, login, logout
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseRedirect,JsonResponse
from django.shortcuts import render,redirect,get_object_or_404
from django.urls import reverse
from .models import User,AuctionListing,Bid,Comment,WatchList
from .form import AuctionListingForm,BidForm,CommentForm,WatchListForm
from django.conf import settings
import os
import pdb

IMAGE_ROOT = settings.MEDIA_ROOT

def create_listing(request):
    """处理表单 以及上传的文件"""
    if request.method == 'POST':
        if request.user.is_authenticated:
            # pdb.set_trace()
            form = AuctionListingForm(request.POST,request.FILES) #  new_listing.image_url = request.FILES
            if form.is_valid():
                # 保存商品到数据库
                new_listing = form.save(commit=False)
                new_listing.creator = request.user  # 关联创建者
                new_listing.save()
                return redirect('listing_detail',new_listing.pk)  # 重定向到新商品的详情页
        else:
            return redirect("index")
    else:
        form = AuctionListingForm()

    return render(request, 'auctions/create_listing.html', {'form': form})

# 使用login require 装饰,需要进行登录
def watch_list(request):
    """add table to the watch list """
    if request.method == 'POST':
        if 'name' in request.POST: # == 'btn-delete-item'
            # pdb.set_trace()
            # delete btn 传来的数据 删除这个数据
            auction_id = request.POST['auction_id']
            item = WatchList.objects.filter(liker=request.user,auction=auction_id)
            item.delete()
            data = request.POST.dict()
            data['status'] = 'success'
            return JsonResponse(data)

        elif 'auction_id' in request.POST:
            # 获取到user-id , auction - id  储存watch form
            # pdb.set_trace()
            watch_info = request.POST
            watch_form = WatchList()
            watch_form.liker = request.user
            watch_form.auction = AuctionListing.objects.get(pk=watch_info['auction_id'])
            watch_form.save()
            return redirect("listing_detail",watch_info['auction_id'])

    # 得到auction id
    auction_ids = WatchList.objects.filter(liker=request.user).values_list('auction',flat=True)
    # 得到 acution list 使用数据库的In方法 来筛选所有id在列表中的商品
    auction_list = AuctionListing.objects.filter(id__in=auction_ids) # [get_object_or_404(AuctionListing,id=wid) for wid in watch_id]
    context = {'auction_list':auction_list}
    return render(request,'auctions/watch_list.html',context)

def index(request):
    """重名函数 也可能会导致原本的变量名字被占用"""
    # 查询数据库获取所有活动商品列表
    active_listings = AuctionListing.objects.filter(is_active=True)
    # pdb.set_trace()

    return render(request, 'auctions/index.html', {'active_listings': active_listings })

def listing_detail(request, listing_id):
    """为每一个单独的商品简历 使用最高的进行排序 使用urls来传递图片的真实路径"""
    listing = get_object_or_404(AuctionListing, id=listing_id)
    # 完整的comments
    bids = Bid.objects.filter(listing=listing).order_by('-amount')
    comments = Comment.objects.filter(listing=listing).order_by('-timestamp')
    bid_form = BidForm()
    comment_form = CommentForm()
    if request.method == 'POST':
        if 'bid_button' in request.POST:  # 处理投标表单
            bid_form = BidForm(request.POST)
            new_bid = bid_form.save(commit=False)
            new_bid.bidder = request.user
            new_bid.listing = listing
            new_bid.save()

            # 获取最高的投标金额, 并且保存最高投标金额
            highest_bid = Bid.objects.filter(listing=listing).order_by('-amount').first()
            if highest_bid:
                listing.current_bid = highest_bid.amount
                listing.save()

        elif 'comment_button' in request.POST:  # 处理评论表单
            comment_form = CommentForm(request.POST)
            if comment_form.is_valid():
                new_comment = comment_form.save(commit=False)
                new_comment.commenter = request.user
                new_comment.listing = listing
                new_comment.save()
        return redirect('listing_detail', listing_id=listing_id)


    context = { 'listing': listing,
                'bids': bids,
                'bid_form': bid_form,
                'comment_form': comment_form,
                'comments': comments,
                # 'image_url':os.path.join(IMAGE_ROOT,listing.image_url),
                }

    return render(request, 'auctions/listing_detail.html', context)

def login_view(request):
    if request.method == "POST":

        # Attempt to sign user in
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)

        # Check if authentication successful
        if user is not None:
            login(request, user)
            return HttpResponseRedirect(reverse("index"))
        else:
            return render(request, "auctions/login.html", {
                "message": "Invalid username and/or password."
            })
    else:
        return render(request, "auctions/login.html")


def logout_view(request):
    logout(request)
    return HttpResponseRedirect(reverse("index"))


def register(request):
    if request.method == "POST":
        username = request.POST["username"]
        email = request.POST["email"]

        # Ensure password matches confirmation
        password = request.POST["password"]
        confirmation = request.POST["confirmation"]
        if password != confirmation:
            return render(request, "auctions/register.html", {
                "message": "Passwords must match."
            })

        # Attempt to create new user
        try:
            user = User.objects.create_user(username, email, password)
            user.save()
        except IntegrityError:
            return render(request, "auctions/register.html", {
                "message": "Username already taken."
            })
        login(request, user)
        return HttpResponseRedirect(reverse("index"))
    else:
        return render(request, "auctions/register.html")


url.py

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("login", views.login_view, name="login"),
    path("logout", views.logout_view, name="logout"),
    path("register", views.register, name="register"),
    path('create_listing',views.create_listing,name="create_listing"),
    path('watch_list',views.watch_list,name='watch_list'),
    # 使用id的方式 使用id来证明 这样你的函数在接收到id的时候就会使用这个url
    path('<int:listing_id>/',views.listing_detail,name='listing_detail'),
]
进阶功能以及商城界面的优化

到目前为止,我们已经完成了一个线上拍卖商城的初步雏形,该网站已经包括了上传商品和展示商品等功能。接下来,我们需要添加一些高级功能和进一步优化商城的界面。这些功能包括:

  • 在商品详情页中添加评论以及竞价的功能

  • 实现一个watch - listing ,类似于听歌软件中,我喜欢的歌。同理,在本项目中可以添加喜欢的商品到一个列表中去。

  • 使用css样式以及javascript 来美化只有HTML的界面

1.商品的评论与竞价

我们可以在商品的详情页中,添加评论表单以及竞价表单,来实现用户与商品之间的交互。

class BidForm(forms.ModelForm):
    class Meta:
        model = Bid
        fields = ['amount']

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['text']

bid,以及comment的表单,两者的模型可以见上文模型代码。

def listing_detail(request, listing_id):
    """为每一个单独的商品简历 使用最高的进行排序 使用urls来传递图片的真实路径"""
    listing = get_object_or_404(AuctionListing, id=listing_id)
    # 完整的comments
    bids = Bid.objects.filter(listing=listing).order_by('-amount')
    comments = Comment.objects.filter(listing=listing).order_by('-timestamp')
    bid_form = BidForm()
    comment_form = CommentForm()
    if request.method == 'POST':
        if 'bid_button' in request.POST:  # 处理投标表单
            bid_form = BidForm(request.POST)
            new_bid = bid_form.save(commit=False)
            new_bid.bidder = request.user
            new_bid.listing = listing
            new_bid.save()

            # 获取最高的投标金额, 并且保存最高投标金额
            highest_bid = Bid.objects.filter(listing=listing).order_by('-amount').first()
            if highest_bid:
                listing.current_bid = highest_bid.amount
                listing.save()

        elif 'comment_button' in request.POST:  # 处理评论表单
            comment_form = CommentForm(request.POST)
            if comment_form.is_valid():
                new_comment = comment_form.save(commit=False)
                new_comment.commenter = request.user
                new_comment.listing = listing
                new_comment.save()
        return redirect('listing_detail', listing_id=listing_id)


    context = { 'listing': listing,
                'bids': bids,
                'bid_form': bid_form,
                'comment_form': comment_form,
                'comments': comments,
                # 'image_url':os.path.join(IMAGE_ROOT,listing.image_url),
                }

    return render(request, 'auctions/listing_detail.html', context)

投标部分

  • 投标表单创建与提交:在商品详情页面,用户可以使用投标表单 BidForm() 进行投标。当用户填写表单并提交时,我们检查HTTP POST请求中的 “bid_button” 字段,以确定用户正在提交投标表单。

  • 数据验证与保存:我们验证用户提交的投标表单数据,确保数据合法。有效数据将创建一个新的投标记录 new_bid,其中包括投标者(bidder)、商品(listing),然后将其保存到数据库。

  • 获取最高投标金额:为了维护商品的当前最高投标金额 current_bid,我们检索所有投标记录,按照投标金额降序排序,获取最高投标。如果有最高投标,将其金额保存到商品的 current_bid 字段,并保存商品记录。更新完成后,我们刷新整个页面,来

评论部分

  • 评论表单创建:商品详情页面允许用户提交评论,因此我们创建了一个评论表单 CommentForm()

  • 处理评论表单的提交:当用户填写评论表单并提交时,我们检查HTTP POST请求中的 “comment_button” 字段,以确定用户正在提交评论表单。

  • 数据验证与保存:我们验证用户提交的评论表单数据,确保数据合法。有效数据将创建一个新的评论记录 new_comment,其中包括评论者(commenter)、商品(listing),然后将其保存到数据库。储存完成后,我们重定向用户回到当前商品的详情页面,以便他们可以查看新的评论记录。

  <div class="listing-container">
    <div class="listing-item" style="max-width: max-content;">
    {% if listing.image_url %}
        <img class="l-img" src="{{ listing.image_url.url }}" alt="{{ listing.title }}">
    {% endif %}
      <div class="bid-comment-form">
        <p class="para-title">Place a Bid:</p>
        <p>Current bid: ${{ listing.current_bid }}</p>

        <form method="post" class="custom-form">
          {% csrf_token %}
          {{ bid_form.amount.label_tag }}
          {{ bid_form.amount }}
          <button type="submit" name="bid_button">Submit Bid</button>
        </form>

        <p class="para-title">Add a Comment:</p>
        <div>
        {{ comment_form.text.label_tag }}
        </div>
        <form method="post" class="custom-form">
          {% csrf_token %}
          {{ comment_form.text }}
          <br>
          <button type="submit" name="comment_button" style="padding: 3px 10px;">Submit Comment</button>
        </form>
        </div>
    </div>
  </div>

最终界面如下

在这里插入图片描述

2.添加watchlist 的功能:

      **1)首先设计watchlist 模型类**,建立商品与用户之间的映射关系
class WatchList(models.Model):

    liker = models.ForeignKey(User,on_delete=models.CASCADE)
    auction = models.ForeignKey(AuctionListing,on_delete=models.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)
    **2)再通过前端页面**用户按下Add to WatchList 按钮,将商品加入watch list
  <form action="{% url 'watch_list' %}" style="text-align: center" method="post">
  {% csrf_token %}
    <input type="hidden" name="auction_id" value="{{listing.id}}">
    <button type="submit" class="add-watch-list">Add to watchlist</button>
  </form>
      **3)最后通过watchlist 的 user键**筛选出登录用户所有在Watchlist中的商品,并使用展示index的方式来展示watchlist 的商品。
    auction_ids = WatchList.objects.filter(liker=request.user).values_list('auction',flat=True)
    # 得到 acution list 使用数据库的In方法 来筛选所有id在列表中的商品
    auction_list = AuctionListing.objects.filter(id__in=auction_ids) # [get_object_or_404(AuctionListing,id=wid) for wid in watch_id]
    context = {'auction_list':auction_list}
    return render(request,'auctions/watch_list.html',context)

4)在页面中使用Ajax技术管理商品watchlist 页面中,我们额外使用了JavaScript中的AJAX技术,以实现不必刷新整个页面即可删除商品的功能。由于篇幅限制,本博客未涉及详细介绍AJAX、网页调试和CSS的方法,但这些内容将在其他博客中进行解释。

代码如下:

$(document).ready(function() {
    {# write javascript to send messiage #}
    $('.btn-delete-item').click(function() {
        {#获取到循环id#}
        var loop_id = $(this).data('looper');
        var div_id = 'div-'+loop_id ;
        var auction_id = $(this).siblings('input[name=listing-id]').val()
        $.ajax({
            type: 'POST',
            url: '/watch_list',
            data: {
                'name':'btn-delete-item',
                'auction_id': auction_id,  // 发送商品ID
                'div_id':div_id,
                'csrfmiddlewaretoken': $('input[name=csrfmiddlewaretoken]').val(),  // 包括CSRF令牌
            },
            success: function(data) {
                // data.remove() 删除对应的元素
                alert('删除成功!')
                console.log(data)
                $('#'+data['div_id']).remove()
            },
            error: function() {
                alert('删除失败!!')
            }
        });
    });
});

最终watch list如下

在这里插入图片描述

3.css样式美化商城界面 为了提高网站的外观和用户体验,我们使用了CSS样式来美化整个项目页面。没有CSS修饰的页面可能会显得奇怪和简陋。因此,我花了一些时间来学习CSS,并最终完成了整个项目的美化。我们可以使用CSS来规范元素的各个属性,包括背景颜色、居中、字体颜色、大小、字体类型以及弹性布局等。更多有关这些内容的详细方法将在下一篇博客中进行讲解。

我的一些感受:

虽然在文字中项目的制作很简单,流畅,但是实际开始会有一个一个的bug出现,一开始我的进度举步维艰,中途也有各种的事情阻挠,导致项目的进度拖延了很多,但好在我一步一步的坚持了下来,一开始其实也就是想着把这个项目直接简易的做完,快点上传项目,写出博客,进行下一个项目。但是做的途中,发现了很多可以优化,美观的点,都进行了尝试,虽然花了很久时间,但是获得的收获也是不可比拟的。现在我最大的感受也就是,花时间做十个一般的作品不如花好时间做一个优质的作品。只有沉下心来去做才能得到最大的收获以及满足

至此整个项目已经完成,项目的地址如下 地址,请期待其他CS50作业的作品。如果喜欢的话请点个赞哦❤️❤️。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值