iT邦幫忙

DAY 17
0

為程式人寫的 Django Tutorial系列 第 17

Django Tutorial for Programmers: 17. Custom template tags/filters, and Ajax

內容diff

我們現在是在 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 %}



```

接著我們要來寫 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。

大功告成!試著刪除點東西試試吧。

上一篇
Django Tutorial for Programmers: 16. 續・django.contrib.auth
下一篇
Django Tutorial for Programmers: 18. Events
系列文
為程式人寫的 Django Tutorial30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
不惑的胖虎
iT邦新手 4 級 ‧ 2016-12-16 12:19:51

請問完成這個章節
用ajax刪除的資料
是真的刪除嗎?

我照著範例做
在網頁上刪除資料
但按下f5後,剛剛刪除的資料又回來了
請問這是正常的嗎??

我要留言

立即登入留言