小編我最喜歡寫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)