Django實戰教程: 開發餐廳在線點評網站(1)

小編我最喜歡寫Django的基礎知識,尤其是一個一個細小的知識點。因爲我相信無論你是新手還是高手,熟練地掌握基礎知識才能在實際Web開發項目中游刃有餘。然而我們學習Django的最終目的還是應用,今天小編我就帶你用Django開發一個餐廳在線點評的APP,也算應讀者的要求。如果你對本文中的代碼閱讀起來還感覺有點吃力,建議關注我的微信公衆號,點擊經典原創閱讀Django基礎(1)到(12)。如果對代碼有任何問題或不理解,可以在評論區留言。

 

總體思路

我們要開發一個餐廳點評網站(APP),具體包括以下幾個功能性頁面。

  • 查看餐廳(restaurants)列表 - 所有用戶

  • 查看餐廳詳情(包括名稱,地址,電話,菜品和點評) - 所有用戶

  • 創建餐廳 - 僅限登錄用戶

  • 修改餐廳 - 僅限登錄用戶,且每個用戶只能修改自己創建的餐廳

  • 給餐廳添加菜品(dishes) - 僅限登錄用戶

  • 修改菜品信息 - 每個登錄用戶只能修改自己創建的菜品

  • 查看菜品詳情(品名,描述, 圖片和價格)

  • 給餐廳添加評論(review)和評分(rating)

 

如果匿名用戶查看餐廳列表和詳情時,它們會被要求先登錄後再創建餐廳或給餐廳添加評論。我們預期的效果如下圖所示。本教程分2部分,本文僅介紹前4個功能性頁面。

 

項目開發環境

Django 2.0 + Python 3.5 + SQLite。因爲我們需要上傳顯示圖片,所以請確保你已通過pip安裝python的pillow圖片庫。

 

項目配置settings.py

我們通過python manage.py startapp myrestaurants創建一個叫myrestaurants的APP,把它加到settings.py裏INSATLLED_APP裏去,如下所示。users是對Django自帶AUTH User的擴展應用。如果不清楚,請閱讀這裏

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.staticfiles',
    'django.contrib.sites',
    'myrestaurants',
    'users',
]

因爲我們要用到靜態文件如css和圖片,我們需要在settings.py裏設置STATIC_URL和MEDIA。用戶上傳的圖片會放在/media/文件夾裏。

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"), ]

# specify media root for user uploaded files,
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

整個項目的urls.py如下所示。我們把myrestaurants的urls.py也加進去了。別忘了在結尾部分加static配置。

from django.conf.urls import url, include
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static


urlpatterns = [
    url(r'^myrestaurants/', include('myrestaurants.urls')),
    url(r'^admin/', admin.site.urls),
    url(r'^accounts/', include('users.urls')),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

模型models.py

對於Django而言,能設計一個良好的models,我們已經成功了一半。在本例中我們創建了Restuarant, Dish和Review的模型。因爲我們在視圖中會應用Django的通用視圖,所以我們的模型裏還需要定義get_abosolute_url。Django的CreateView和UpdateView在完成對象的創建或編輯後會自動跳轉到這個絕對url。

from django.db import models
from django.contrib.auth.models import User
from datetime import date
from django.urls import reverse

class Restaurant(models.Model):
    name = models.TextField()
    address = models.TextField(blank=True, default='')
    telephone = models.TextField(blank=True,  default='')
    url = models.URLField(blank=True, null=True)
    user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
    date = models.DateField(default=date.today)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('myrestaurants:restaurant_detail', args=[str(self.id)])

class Dish(models.Model):
    name = models.TextField()
    description = models.TextField(blank=True,  default='')
    price = models.DecimalField('USD amount', max_digits=8, decimal_places=2, blank=True, null=True)
    user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
    date = models.DateField(default=date.today)
    image = models.ImageField(upload_to="myrestaurants", blank=True, null=True)
    restaurant = models.ForeignKey(Restaurant, null=True, related_name='dishes', on_delete=models.CASCADE)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('myrestaurants:dish_detail', args=[str(self.restaurant.id), str(self.id)])

# This Abstract Review can be used to create RestaurantReview and DishReview
class Review(models.Model):
    RATING_CHOICES = ((1, 'one'), (2, 'two'), (3, 'three'), (4, 'four'), (5, 'five'))
    rating = models.PositiveSmallIntegerField('Rating (stars)', blank=False, default=3, choices=RATING_CHOICES)
    comment = models.TextField(blank=True, null=True)
    user = models.ForeignKey(User, default=1, on_delete=models.CASCADE)
    date = models.DateField(default=date.today)

    class Meta:
        abstract = True

class RestaurantReview(Review):
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, related_name="reviews")
    
    def __str__(self):
        return "{} review".format(self.restaurant.name)

 

URLConf配置urls.py

每個path都對應一個視圖,一個命名的url和我們本文剛開始介紹的一個功能性頁面。本文中包含了4個urls。我們在查看餐廳詳情和編輯餐廳信息的url中傳遞了pk(餐廳id)作爲參數。

from django.urls import path, re_path
from . import views

# namespace
app_name = 'myrestaurants'
urlpatterns = [

# 查看餐廳列表
    path('', views.RestaurantList.as_view(), name='restaurant_list'),

# 查看餐廳詳情, 如/myrestaurants/restaurant/1/
    re_path(r'^restaurant/(?P<pk>\d+)/$',
        views.RestaurantDetail.as_view(), name='restaurant_detail'),

# 創建餐廳, 如:/myrestaurants/restaurant/create/
    re_path(r'^restaurant/create/$', views.RestaurantCreate.as_view(), name='restaurant_create'),

# 編輯餐廳詳情, 如: /myrestaurants/restaurant/1/edit/
    re_path(r'^restaurant/(?P<pk>\d+)/edit/$',
        views.RestaurantEdit.as_view(), name='restaurant_edit'),

]

 

視圖views.py

爲了簡化開發,本例中使用了Django自帶的通用視圖。我們使用ListView來顯示餐廳列表,使用DetailView來顯示餐廳詳情,使用CreateView來創建餐廳,使用UpdateView來編輯餐廳信息。如果你對通用視圖不瞭解,請閱讀下文:

# Create your views here.

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import DetailView, ListView, UpdateView
from django.views.generic.edit import CreateView
from .models import RestaurantReview, Restaurant, Dish
from .forms import RestaurantForm, DishForm


class RestaurantList(ListView):

    queryset = Restaurant.objects.all().order_by('-date')
    context_object_name = 'latest_restaurant_list'
    template_name = 'myrestaurants/restaurant_list.html'


class RestaurantDetail(DetailView):
    model = Restaurant
    template_name = 'myrestaurants/restaurant_detail.html'

    def get_context_data(self, **kwargs):
        context = super(RestaurantDetail, self).get_context_data(**kwargs)
        context['RATING_CHOICES'] = RestaurantReview.RATING_CHOICES
        return context


class RestaurantCreate(CreateView):
    model = Restaurant
    template_name = 'myrestaurants/form.html'
    form_class = RestaurantForm

    # Associate form.instance.user with self.request.user
    def form_valid(self, form):
        form.instance.user = self.request.user
        return super(RestaurantCreate, self).form_valid(form)


class RestaurantEdit(UpdateView):
    model = Restaurant
    template_name = 'myrestaurants/form.html'
    form_class = RestaurantForm

重要知識點:

  • 在RestaurantDetail視圖裏,我們通過get_context_data方法向模板傳遞了額外的變量RATING_CHOICES。DetailView視圖會接受url傳遞來的pk值,並顯示該模型的所有信息。

  • 在RestaurantCreate視圖裏,我們使用了form_valid方法。form_valid方法作用是添加前端表單字段以外的信息。在用戶在創建餐廳時,我們不希望用戶能更改創建用戶,於是在前端表單裏把user故意除外了(見forms.py),而選擇在後臺添加user信息。

 

表單forms.py

創建和編輯對象時需要用到表單,我們的表單如下所示。我們在前端表單裏移除了user,而採用後臺添加的方式。我們添加了widget和labels。添加widget的目的時爲了定製用戶輸入控件(比如URLInput),並給其添加css樣式(因爲boostrap表單需要form-control這個樣式)。

from django.forms import ModelForm,  TextInput, URLInput, ClearableFileInput
from .models import Restaurant, Dish


class RestaurantForm(ModelForm):
    class Meta:
        model = Restaurant
        exclude = ('user', 'date',)

        widgets = {
            'name': TextInput(attrs={'class': 'form-control'}),
            'address': TextInput(attrs={'class': 'form-control'}),
            'telephone': TextInput(attrs={'class': 'form-control'}),
            'url': URLInput(attrs={'class': 'form-control'}),
        }

        labels = {
            'name': '名稱',
            'address': '地址',
            'telephone': '電話',
            'url': '網站',
        }

 

模板文件

我們在目錄中創建templates/myrestaurants/目錄,添加如下html模板。

# restaurant_list.html

{% extends "myrestaurants/base.html" %}

{% block content %}
<h3>餐廳列表</h3>

<ul>
  {% for restaurant in latest_restaurant_list %}
    <li><a href="{% url 'myrestaurants:restaurant_detail' restaurant.id %}">
    {{ restaurant.name }}</a></li>
  {% empty %}<li>對不起,沒有餐廳點評。</li>
  {% endfor %}
</ul>

 {% if request.user.is_authenticated %}
<p><span class="glyphicon glyphicon-plus"></span> <a href="{% url 'myrestaurants:restaurant_create' %}">添加餐廳</a></p>
 {% else %}

<p>請<a href="{% url 'users:login' %}?next={% url 'myrestaurants:restaurant_create' %}">登錄</a>後添加餐廳。</p>
  {% endif %}
{% endblock %}

重要知識點:

  • 請觀察我們是如何把參數(如餐廳id)傳遞給命名url的。

  • 請觀察我們是如何設置next實現匿名用戶登錄後立即跳轉到餐廳創建頁面的。

# restaurant_detail.html

{% extends "myrestaurants/base.html" %}

{% block content %}
<h3>
  {{ restaurant.name }}
  {% if request.user == restaurant.user %}
    (<a href="{% url 'myrestaurants:restaurant_edit' restaurant.id %}">修改</a>)
  {% endif %}
</h3>

<h4>地址</h4>

<p>
  {{ restaurant.address }}, <br/>
  {{ restaurant.telephone }}
</p>

<h4>菜單
  {% if request.user.is_authenticated %}
    (<a href="{% url 'myrestaurants:dish_create' restaurant.id %}">添加</a>)
  {% endif %}
</h4>

<ul>
  {% for dish in restaurant.dishes.all %}
    <li><a href="{% url 'myrestaurants:dish_detail' restaurant.id dish.id %}">
    {{ dish.name }}</a> - {{ dish.price }}元</li>
  {% empty %}<li>對不起,該餐廳還沒有菜餚。</li>
  {% endfor %}
</ul>

<h4>用戶點評</h4>
{% if restaurant.reviews.all %}
  {% for review in restaurant.reviews.all %}

      <p>{{ review.rating}}星, {{ review.user }}點評, {{ review.date | date:"Y-m-d" }}</p>
      <p>{{ review.comment }}</p>

  {% endfor %}

{% else %}
<p>目前還沒有用戶點評。</p>{% endif %}

<h4>添加點評</h4>
{% if request.user.is_authenticated %}
<form action="{% url 'myrestaurants:review_create' restaurant.id %}" method="post">
  {% csrf_token %}
 <p>評論</p>
  <textarea name="comment" id="comment"></textarea>
  <p>評分</p>
  <p>
    {% for rate in RATING_CHOICES %}
      <input type="radio" name="rating" id="rating{{ forloop.counter }}" value="{{ rate.0 }}" />
      <label for="choice{{ forloop.counter }}">{{ rate.0 }}星</label>
      <br/>
    {% endfor %}
  </p>
  <input type="submit" value="提交" />
</form>
{% else %}
<p>請先<a href="{% url 'users:login' %}?next={% firstof request.path '/' %}">登錄</a>再評論。</p>
{% endif %}

{% endblock %}

重要知識點:

  • 請觀察我們是如何通過 if request.user == restaurant.user 來控制每個用戶只能編輯自己創建的餐廳的。

     

# form.html

注意: 創建餐廳和編輯餐廳,我們使用了同樣一個模板。

{% extends "myrestaurants/base.html" %}

{% block content %}

<form action="" method="post" enctype="multipart/form-data" >
  {% csrf_token %}

  {% for hidden_field in form.hidden_fields %}
  {{ hidden_field }}
{% endfor %}

{% if form.non_field_errors %}
  <div class="alert alert-danger" role="alert">
    {% for error in form.non_field_errors %}
      {{ error }}
    {% endfor %}
  </div>
{% endif %}

{% for field in form.visible_fields %}
  <div class="form-group">
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}
      <small class="form-text text-muted">{{ field.help_text }}</small>
    {% endif %}
  </div>
{% endfor %}
  <input type="submit" value="提交"/>
</form>

{% endblock %}

# base.html

這個文件基本上只包括樣式,不包括邏輯,可以不看。

{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %} Django餐廳點評系統{% endblock %} </title>
<meta charset="utf-8">
<meta name="keywords" content="{% block keywords %} some keywords{% endblock %} ">
<meta name="description" content="{% block description %} Django web application example.{% endblock %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="{% static 'myrestaurants/custom.css' %}">


</head>

<body class="bs-docs-home">
<nav class="navbar navbar-inverse navbar-static-top bs-docs-nav">
  <div class="container">
    <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>                        
      </button>
        <a class="navbar-brand" href="#"><strong>Django餐廳點評APP</strong></a>
     </div>

      <div class="collapse navbar-collapse" id="myNavbar">

       <ul class="nav navbar-nav navbar-right">
       {% if user.is_authenticated %}

          <li class="dropdown">
              <a class="dropdown-toggle btn-green" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-user"></span>  {{ user.username }} <span class="caret"></span></a>
            <ul class="dropdown-menu">
              <li><a  href="{% url 'users:profile' user.id %}">我的賬戶</a></li>
              <li><a  href="{% url 'users:logout' %}">退出登錄</a></li>
            </ul>
          </li>  
         {% else %}         
            <li class="dropdown"><a class="dropdown-toggle btn-green" href="{% url 'users:register' %}"><span class="glyphicon glyphicon-user"></span> 註冊</a></li>
         <li class="dropdown"><a class="dropdown-toggle" href="{% url 'users:login' %}" ><span class="glyphicon glyphicon-log-in"></span> 登錄</a></li>
       {% endif %}
       </ul>

    </div>

  </div>
</nav>    

 <!-- Page content of course! -->
<main id="section1" class="container-fluid">
<div class="container">
  <div class="row">
 <div class="col-sm-12">
   <div class="wrapper">
        <div class="newsbox">
             <div class="container1">       

 {% block content %}
  {% if error_message %}<p><strong>{{ error_message}}</strong></p>{% endif %}
  {% endblock %}

        </div>
      </div>
   </div>
</div>
</div>
    
</div> 
</main>
<footer class="footer">
  {% block footer %}{% endblock %}
</footer>
<!--End of Footer-->

<!-- Bootstrap core JavaScript
================================================== -->

<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

</body>
</html>

 

查看效果

連續運行python manage.py makemigrations, python manage.py migrate和python manage.py runserver,  打開http://127.0.0.1:8000/myrestaurants/就可以看到以下效果了。

 

餐廳列表(用戶登錄後跳轉到添加餐廳頁面)

創建餐廳(用戶創建餐廳後跳轉到餐廳詳情)

餐廳詳情頁面(用戶登錄後才能點評)

用戶登錄後可以修改餐廳信息或提交點評

修改餐廳詳情(每個登錄用戶只能修改自己創建的餐廳)

 

小結

我們利用Django開發了一個簡單的餐廳點評網站,實現了以下4個標黃的功能性頁面。下篇教程中,我們將開發剩餘4個功能性頁面,歡迎關注我的微信公衆號獲取更多更新。

  • 查看餐廳(restaurants)列表

  • 查看餐廳詳情(包括名稱,地址,電話,菜品和點評)

  • 創建餐廳 - 僅限登錄用戶

  • 修改餐廳 - 每個登錄用戶只能修改自己創建的餐廳

  • 給餐廳添加菜品(dishes) - 僅限登錄用戶

  • 修改菜品信息 - 每個登錄用戶只能修改自己創建的菜品

  • 查看菜品詳情(品名,描述, 圖片和價格)

  • 給餐廳添加評論(review)和評分(rating)