Skip to content

Commit

Permalink
Chapter 17
Browse files Browse the repository at this point in the history
  • Loading branch information
uranusjr committed Oct 14, 2014
1 parent 41366d2 commit a33a665
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 4 deletions.
205 changes: 205 additions & 0 deletions 17-template-tags-and-ajax.md
@@ -0,0 +1,205 @@
我們現在是在 view 裡判斷是否能 delete。但是這種繁瑣的東西實在應該被放到 model 才對。我們在 `Store` 新增一個 method,來把這些東西 refactor 出去:

```python
# stores/models.py

class Store(models.Model):
# ...
def can_user_delete(self, user):
if not self.owner or self.owner == user:
return True
if user.has_perm('store_delete'):
return True
return False
```

這樣 view 就簡單多了:

```python
# stores/views.py

def store_delete(request, pk):
# ...
if store.can_user_delete(request.user):
store.delete()
return redirect('store_list')
# ...
```

但這要怎麼在 template 使用?這樣嗎?

```html
{{ store.can_user_delete(user) }}
```

餓,好像不行。因為 Django 不允許在 template 中呼叫帶引數的 methods。為了這個目的,我們必須實作一個 template tag。

我們前面已經看過幾個 template tags:內建的 `urls``for``extends` 等等都是。我們也看過第三方的元件,例如 Django Crispy Forms 的 `crispy` tag 與 filter。不過這些東西究竟是怎麼寫的?

`stores` 目錄下建立 `templatetags` 目錄,在裡面新增兩個檔案:`__init__.py``stores_tags.py`。在後者輸入以下內容:

```python
from django.template import Library

register = Library()

@register.filter
def deletable(store, user):
return store.can_user_delete(user)
```

Django template tag/filter 其實就是 Python function,只是我們要用一個 decorator 把它註冊到 template system 裡,才能被 Django 辨識。這個 filter 的作用其實就只是把 `can_user_delete` 包起來。

用法就和一般的 template tag/filter 一樣:

```html
{# stores/templates/stores/store_detail.html #}

{% load stores_tags %}

{% if store|deletable:user %}
<input type="submit" value="刪除" class="btn btn-danger">
{% endif %}
```

這樣我們就實作完成 detail view 的刪除了。現在我們想在列表頁也加上同樣的東西。不過這次我們來用 Ajax 實作。我們想要做到:

1. 在列表頁的每個店家旁邊都增加一個刪除按鈕(如果使用者可以刪除該店家)。
2. 按下按鈕後,用 Ajax 發一個 DELETE request。
3. Django 收到 DELETE request 後進行刪除,成功則回傳 200 OK。
4. 當 JavaScript 接收到刪除成功的訊息,直接把列表頁中的對應項目移出 DOM。

第一步很簡單:

```html
{# stores/templates/stores/store_list.html #}

{% load stores_tags %}

<!-- ... -->
<div class="store">
{% if store|deletable:user %}
<button data-href="{% url 'store_delete' store.pk %}"
class="btn btn-danger pull-right btn-delete">
刪除
</button>
{% endif %}
<!-- ... -->
```

接著是第二步。我們要寫 JavaScript 了。首先把 jQuery 加進來,然後新增一個 block:

```html
{# base/templates/base.html #}

<!-- 放在 body 最後面 -->
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
{% block js %}{% endblock js %}

</body>
</html>
```

接著我們要來寫 Ajax⋯⋯不,等等!我們還得做一件事情。還記得 Django 會檢查 CSRF token 嗎?當你使用 Ajax 時,它還是會檢查。所以我們必須多做一些設定。幸好 Django 官方文件就有[教你怎麼做](https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax)。不過這要放哪裡?

Django 把網站中不是動態產生的檔案統稱為 static files,並用 `django.contrib.staticfiles` 來管理它們。管理的邏輯和 templates 很像。我們來建立一個 `base.js`。首先建立 `base/static/base/js/` 目錄,並在其中建立一個 `base.js`,然後把官方文件中的範例程式碼抄進去:

```javascript
// base/static/base/js/base.js

function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
'beforeSend': function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
```

然後在 `base/templates/base.html` 中引入:

```html
{% load staticfiles %}

<!-- 要放在 jQuery 後面 -->
<script src="{% static 'base/js/base.js' %}"></script>
```

可以看到路徑的邏輯也和 template 很像。

在設定 CSRF token 後,我們就可以實作第二步與第四步。同樣在 `stores` 目錄中也建立 static 目錄,加入以下檔案:

```javascript
// stores/static/stores/js/store_list.js

(function ($) {
$('.btn-delete').click(function () {
var button = this;
$.ajax({
'url': $(button).data('href'),
'type': 'DELETE'
}).done(function () {
$(button).parent('.store').remove();
}).fail(function (e) {
console.log(e);
});
});
})(jQuery);
```

錯誤處理就不管了,如果需要的話自己寫。把這個檔案引入 template:

```html
{# stores/templates/stores/store_list.html #}

{% load staticfiles %}

{# 放在 endblock content 後面 #}

{% block js %}
<script src="{% static 'stores/js/store_list.js' %}"></script>
{% endblock js %}
```

最後是第三步,也就是 Python 部分:

```python
# stores/views.py
from django.http import HttpResponse

@login_required
@require_http_methods(['POST', 'DELETE'])
def store_delete(request, pk):
try:
store = Store.objects.get(pk=pk)
except Store.DoesNotExist:
raise Http404
if store.can_user_delete(request.user):
store.delete()
if request.is_ajax():
return HttpResponse()
return redirect('store_list')
return HttpResponseForbidden()
```

我們把 `DELETE` 也加入允許列表,並且在成功時使用 `request.is_ajax()` 判斷,如果是 Ajax request 則直接回傳 `HTTPResponse` 代表 200 OK。

大功告成!試著刪除點東西試試吧。
25 changes: 25 additions & 0 deletions lunch/lunch/base/static/base/js/base.js
@@ -0,0 +1,25 @@
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
'beforeSend': function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
7 changes: 7 additions & 0 deletions lunch/lunch/base/templates/base.html
@@ -1,3 +1,5 @@
{% load staticfiles %}

<!DOCTYPE html>
<html>
<head>
Expand Down Expand Up @@ -30,5 +32,10 @@
</nav>

{% endblock body %}

<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="{% static 'base/js/base.js' %}"></script>
{% block js %}{% endblock js %}

</body>
</html>
7 changes: 7 additions & 0 deletions lunch/lunch/stores/models.py
Expand Up @@ -17,6 +17,13 @@ def __str__(self):
def get_absolute_url(self):
return reverse('store_detail', kwargs={'pk': self.pk})

def can_user_delete(self, user):
if not self.owner or self.owner == user:
return True
if user.has_perm('store_delete'):
return True
return False


class MenuItem(models.Model):

Expand Down
13 changes: 13 additions & 0 deletions lunch/lunch/stores/static/stores/js/store_list.js
@@ -0,0 +1,13 @@
(function ($) {
$('.btn-delete').click(function () {
var button = this;
$.ajax({
'url': $(button).data('href'),
'type': 'DELETE'
}).done(function () {
$(button).parent('.store').remove();
}).fail(function (e) {
console.log(e);
});
});
})(jQuery);
4 changes: 4 additions & 0 deletions lunch/lunch/stores/templates/stores/store_detail.html
@@ -1,5 +1,7 @@
{% extends 'stores/base.html' %}

{% load stores_tags %}

{% block title %}{{ store.name }} | {{ block.super }}{% endblock title %}

{% block content %}
Expand All @@ -9,7 +11,9 @@ <h1>{{ store.name }}</h1>
<form method="post" action="{% url 'store_delete' store.pk %}" class="controls">
{% csrf_token %}
<a href="{% url 'store_update' pk=store.pk %}" class="btn btn-default">更新店家資訊</a>
{% if store|deletable:user %}
<input type="submit" value="刪除" class="btn btn-danger">
{% endif %}
</form>

<table class="table">
Expand Down
13 changes: 13 additions & 0 deletions lunch/lunch/stores/templates/stores/store_list.html
@@ -1,5 +1,7 @@
{% extends 'stores/base.html' %}

{% load staticfiles stores_tags %}

{% block title %}店家列表 | {{ block.super }}{% endblock title %}

{% block content %}
Expand All @@ -9,8 +11,19 @@

{% for store in stores %}
<div class="store">
{% if store|deletable:user %}
<button data-href="{% url 'store_delete' store.pk %}"
class="btn btn-danger pull-right btn-delete">
刪除
</button>
{% endif %}
<h2><a href="{{ store.get_absolute_url }}">{{ store.name }}</a></h2>
<p>{{ store.notes }}</p>
</div>
{% endfor %}
{% endblock content %}


{% block js %}
<script src="{% static 'stores/js/store_list.js' %}"></script>
{% endblock js %}
Empty file.
8 changes: 8 additions & 0 deletions lunch/lunch/stores/templatetags/stores_tags.py
@@ -0,0 +1,8 @@
from django.template import Library

register = Library()


@register.filter
def deletable(store, user):
return store.can_user_delete(user)
9 changes: 5 additions & 4 deletions lunch/lunch/stores/views.py
@@ -1,4 +1,4 @@
from django.http import Http404, HttpResponseForbidden
from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.shortcuts import render, redirect
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
Expand Down Expand Up @@ -51,14 +51,15 @@ def store_update(request, pk):


@login_required
@require_http_methods(['POST'])
@require_http_methods(['POST', 'DELETE'])
def store_delete(request, pk):
try:
store = Store.objects.get(pk=pk)
except Store.DoesNotExist:
raise Http404
if (not store.owner or store.owner == request.user
or request.user.has_perm('store_delete')):
if store.can_user_delete(request.user):
store.delete()
if request.is_ajax():
return HttpResponse()
return redirect('store_list')
return HttpResponseForbidden()

0 comments on commit a33a665

Please sign in to comment.