メモ〜djangoでのself.objectの注意点

今回のお題
-
今回はdjangoアプリのクラスベースビューの中で`self.object`を使う際の注意点を書きます。

ほとんど自分用のメモです。

発生した問題
-

```python
class ShopDetailView(UserPassesTestMixin, DetailView):
  model = Shop
  def test_func(self):
    return self.request.user.shop.pk == self.object.pk
  
  get_success_url(self):
    return reverse("shop:detal", kwargs = { "pk": self.object.pk })
```
としたときにエラーになった(selfにはobjectという属性値はないと言われた）。

このエラーはtest_funcをコメントアウトすると解消された。

結論
-
`UserPassesMixin`などの`AccessMinin`を継承しているMixinの中では

```python
self.object
```
は使えない。

```python
self.kwargs["pk"]
```
で`pk`を拾って参照中のオブジェクトにアクセスする。

理由
-

まず汎用ビューで`self.object`とすると参照中のオブジェクトが取得できる理由について。

例えば`DetailView`であれば継承のルートは`SingleObjectMixin` > `BaseDetailVeiw` > `DetailView`

そして`self.object`の取得は`BaseDeatilView`の`get`メソッドの中で定義されており、そこで使われている`get_object`メソッドは大元の`SingleObjectMixin`で定義されている。

この流れは`CreateView`と`UpdateView`でも同様で、`SingleObjectMixin` > `ModelFormMixin` > `BaseCreateView, BaseUpdateView` > `CreateView, UpdateView`という順序で継承されている(objectについては`self.object = form.save()`として`ModelFormMixin`の中で取得している）。 

```python:django.views.generic.detail.py
class SingleObjectMixin(ContextMixin):
    """
    Provide the ability to retrieve a single object for further manipulation.
    """
    model = None
    queryset = None
    slug_field = 'slug'
    context_object_name = None
    slug_url_kwarg = 'slug'
    pk_url_kwarg = 'pk'
    query_pk_and_slug = False

    def get_object(self, queryset=None):
        """
        Return the object the view is displaying.

        Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
        Subclasses can override this to return any object.
        """
        # Use a custom queryset if provided; this is required for subclasses
        # like DateDetailView
        if queryset is None:
            queryset = self.get_queryset()

        # Next, try looking up by primary key.
        pk = self.kwargs.get(self.pk_url_kwarg)
        slug = self.kwargs.get(self.slug_url_kwarg)
        if pk is not None:
            queryset = queryset.filter(pk=pk)

        # Next, try looking up by slug.
        if slug is not None and (pk is None or self.query_pk_and_slug):
            slug_field = self.get_slug_field()
            queryset = queryset.filter(**{slug_field: slug})

        # If none of those are defined, it's an error.
        if pk is None and slug is None:
            raise AttributeError(
                "Generic detail view %s must be called with either an object "
                "pk or a slug in the URLconf." % self.__class__.__name__
            )

        try:
            # Get the single item from the filtered queryset
            obj = queryset.get()
        except queryset.model.DoesNotExist:
            raise Http404(_("No %(verbose_name)s found matching the query") %
                          {'verbose_name': queryset.model._meta.verbose_name})
        return obj
```
```python:django.views.generic.detail.py
class BaseDetailView(SingleObjectMixin, View):
    """A base view for displaying a single object."""
    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
    """
    Render a "detail" view of an object.

    By default this is a model instance looked up from `self.queryset`, but the
    view will support display of *any* object by overriding `self.get_object()`.
    """
```

一方でAccessMixin由来のMixinでは`object`という属性値が定義されていない。

このため、実装するテンプレートビューが他のテンプレートビューとの多重継承であったとしても、`test_func`などの上記Mixin由来のものの中では`self.object`を使うことはできない。

対処法
-

``python
self.kwargs["pk"]
```
を用いて参照中のオブジェクトにアクセスする。

なお、以下のように記述するのは問題ない（すなわち`self.kwargs`はMixinでもTemplateViewでも使える）。

```python
class ShopDetailView(UserPassesTestMixin, DetailView):
  model = Shop
  
  get_success_url(self):
    return reverse("shop:detal", kwargs = { "pk": self.kwargs["pk"] })
```