クラスベースビューとreverseとDjangoのURLディスパッチ

以下、django1.2.0時点の話です。


参照: クラスベースビューとデコレータとkwargs - logiqboard

viewクラスを使ったURL設計

前回の記事でいうところのshopとgoodsのように、URL・機能的にはshopという大枠にまとまってはいるんだけど、その中に小機能がいっぱいあってshopにベタ実装するときたねーという場合は、上位アプリに完全依存するサブアプリみたいのを作るのも一つの手です。


再掲ですがurlsのところはこんなイメージ。

class ShopViews(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("$", self.index, name=u"index"),
            url("goodsA/", include(goodsAView("goodsA").urls),
            url("goodsB/", include(goodsBView("goodsB").urls),
        )

class goodsA(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^$", self.index, name=u"index"),
            url("^(?P<goodsA_id>\d+)/", self.detail, name=u"detail"),
            url("^(?P<goodsA_id>\d+)/buy", self.buy, name=u"buy"),
            以下多数
        )

rootのurlconfには下記の形式でinclude

...
url("^shop/(?P<shop_id>\d+)/", include(ShopView("shop").urls)),
...

実はこの設計はうまく動きません。

正確にはURLの正引きは問題ないんですが逆引きがうまくいきません。

どういうことなの

上記の設計だと

>>>reverse("shop:goodsA:detail", args=[shop_id, goods_id])
"/shop/1/goodsA/123/"

となってくれそうなものですが、実際はNoReverseMatchとなります。まったくひどい罠です。

原因

詳しい原因は長いので下の方に書くとして、とりあえず原因。

url設計をする際に

url("mypage/(?P<id>\d+)/", include(MyViews().urls)),

のように、「引数をキャプチャするようなURLパターンに対してincludeを行なう」と発生します。

回避する

回避方法としては、悲しいことですが原因のところに書いてあるような設計をしないようにするしかありません。

はうはう

詳しい理由

url関数はRegexPatternを生成するものですが、viewを渡す部分でincludeをしている場合代わりにRegexURLResolverが生成され、その内部にinclude先のRegexURLPatternのリストを保持します。

RegexURLResolverは更にそれ自身の名前空間とそれが割り当てられるURLパターン文字列を保持します。

reverseの際は

1. 渡されたnameを:で分割してviewとnamespaceのリストに分け

2. namespaceのリストを先頭から順番に使ってURLResolverを全て辿り

3. 辿り着いた先でview名からURLPatternを探し

4. 見つかったURLPatternとreverseに渡されたargs, kwargsをマッチングし

5. マッチしたらそのURLパターンにargsを適用し、URL文字列にして返す

という処理がされているのですが、2番でURLResolverが持つURLパターンを結果URLとして連結する際に引数の解決がされていないようです。

実例

shopとgoodsの例では、shop_idを取るURLパターンがURLResolverに割り当てられているため、ここが解決されずに正規表現文字列のまま連結されてしまいます。

また、goodsAのname=detailのURLPatternではgoodsA_idしか取っていないにもかかわらず、argsとしてはshop_idとgoods_idが渡されるためマッチしないと判断され、NoReverseMatchとなります。

おしまい

僕はURL設計がばっちくなるのが嫌だったのでinclude_to_rootなんていうメソッドを作って、includeするURLパターンがが全て連結されてurlconfに直接登録されるようにして回避してみました。

うれしくなーい