drf-nested-routers icon indicating copy to clipboard operation
drf-nested-routers copied to clipboard

Expanded Nested Router Generation Principle

Open yiakwy opened this issue 9 years ago • 8 comments

In the original proposal, we see patterns by alanjds:

""" main_resources/ + {?fields=} + {?filter: __eq= | __ne= | ...} + {?additonal args like : signature= | request_target_for_query_str}

main_resources/{master_pk} main_resources/{master_pk}/slave_resources/ main_resources/{master_pk}/slave_resources/{slave_pk} """

Actually, I do personally believe that is not enough:

(1) one kind resource might intercept another kind of resource (2) {master_pk} can be repalced by '/'(group select) '{master_selector}'

Hence we have relational pattern for automatic resources generation:

master_resource/ {priority 1} master_resource/{master_pk}/ {priority 2} master_resource/>related_resources/ {priority 3}, plural keyword 'related_resources' is reserved master_resource/>related_resources/{field_name}/ {priority 4} master_resource/{master_pk}/related_resources/{relate_res_pk or slector expression} {priority 5}

Hereby we expand original url mapping pattern.

yiakwy avatar Mar 16 '16 01:03 yiakwy

Thanks for pointing @yiakwy, but I did not understood what is the problem and, by consequence, the solution.

Can you please send a PR or offer an test/example showing where the actual implementation fails? Or is this an optimisation? (no problem if it is. Just trying to understand)

alanjds avatar Mar 16 '16 21:03 alanjds

Before I send PR, you can review my result:

    def test_dynamic_query_router(self):
        from api.router import DefaultDynamicQueryRouter
        from api.viewsets import NestedViewSetRouter, WeChatNestedViewSetRouter

        router = DefaultDynamicQueryRouter(is_method_fetch=False)

        router.register('WeChat', WeChatNestedViewSetRouter)
        urls = router.urls
        self.logger.info("router.urls: %s", json.dumps([str(url) for url in urls], sort_keys=True, indent=4))
        self.logger.info("attached methods: %s", json.dumps(router._attached, sort_keys=True, indent=4))

Here are results, much simpler:

Fri, 18, Mar, 2016 09:59:06 [INFO]:tests.py, api.tests, in line 43 >> 
TestDynamicQueryRouter router.urls: [
    "<RegexURLPattern root ^$>", 
    "<RegexURLPattern root ^\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_account(s) ^WeChat/$>", 
    "<RegexURLPattern wtc_account(s) ^WeChat\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)$>", 
    "<RegexURLPattern wtc_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_resource(s) ^WeChat/>resources/$>", 
    "<RegexURLPattern wtc_resource(s) ^WeChat/>resources\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_resource(s)_by_name ^WeChat/>resources/(?P<resource_name>[a-z0-9_\\s]+)$>", 
    "<RegexURLPattern wtc_resource(s)_by_name ^WeChat/>resources/(?P<resource_name>[a-z0-9_\\s]+)\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_resource(s)_from_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)/>resources/$>", 
    "<RegexURLPattern wtc_resource(s)_from_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)/>resources\\.(?P<format>[a-z0-9]+)/?$>", 
    "<RegexURLPattern wtc_resource_from_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)/>resources/(?P<resource_name>[a-z0-9_\\s]+)/$>", 
    "<RegexURLPattern wtc_resource_from_account ^WeChat/(?P<wechat_id>[a-z0-9\\s]+)/>resources/(?P<resource_name>[a-z0-9_\\s]+)\\.(?P<format>[a-z0-9]+)/?$>"
]
Fri, 18, Mar, 2016 09:59:06 [INFO]:tests.py, api.tests, in line 44 >> 
TestDynamicQueryRouter attached methods: [
    {
        "WeChatAccount": {
            "get": "get_accounts"
        }
    }, 
    {
        "WeChatAccount": {
            "get": "get_account_by_wechat_id"
        }
    }, 
    {
        "WeChatAccount": {
            "get": "get_resources"
        }
    }, 
    {
        "WeChatAccount": {
            "get": "get_resources_within_wechat"
        }
    }, 
    {
        "WeChatAccount": {
            "get": "get_resources_from_account"
        }
    }, 
    {
        "WeChatAccount": {
            "get": "get_resource_from_account"
        }
    }
]

Here is routine I used to identify resource api.utils.v_toolkit

def import_model(*args, **kwargs):
    if kwargs == {}:
        map_o = map(to_klass, args)
        if len(args) == 1:
            klass_path = PREFIX + '.'.join(map_o)
        else:
            klass_path = '.'.join(map_o)

        from django.utils.module_loading import import_string
        try:
            klass = import_string(klass_path)
        except ImportError as err:
            raise err
        return klass.__name__, klass

    else:
        raise Exception("Not Implemented!")

Finally we subclass DefaultNested by bypassing Foreignkey finder. The reason I didn't implement "resources B" under "resource A" pattern because when the total number resources increase fast, we have to handle relationship manually. In worst cases, we have Cn_2 possibilities!

Wish you can consider it seriously.

yiakwy avatar Mar 18 '16 10:03 yiakwy

Sorry. I really did not got it.

Are you pointing a problem or proposing a new kind of router? For the issue title, I guess it to be proposing a new kind of router.

alanjds avatar Mar 18 '16 14:03 alanjds

I am sorry that I didn't make it clear.

Here is my suggestion you can consider to enhance your nested router:

We can use either "Import_model" which receives hints from url pattern to route to children within a parent view

or we can use "Redirect to" which receives hints from url pattern to route to children view

While we allow multiple children been selected by using either selectors in query parameter or specifying not a single pricinple key.

in python we can do:

__code = "query_set.{method}({pk}={val})"
exec(__code.format(method=method, pk=query_key, val=pk))

Here is an example:

a url "^WeChat/>resources/(?P<resource_name>[a-z0-9_\s]+)$" comes in.

We will choose all the children nodes bounded (Foreign key selecting for example) to this parent type.

Ok, what should router do for it?

(1) composing url pattern before bounding to parent view method

(2) add a method to parent to "Redirect to" according to children type by converting url to query string : "Messages?media_type=wechat"

__code = """
# parent WeChatView
def {method_name}(self, _query_key, resource_name, **kwargs):

       def to_url_param(kwargs):
            args_proc = lambda o: \\
            '='.join(o[0],o[1])
            ob_map = map(args_proc, list(kwargs.items()))
            return list(ob_map)

       next_url_tpl = "^{{resource_name}}?" + '&'.join( ['{{ref}}={{val}}', ].extend(to_url_param(kwargs)) )
       self._logger.info('gen_url: %s', next_url_tpl)
       next_url = next_url_tpl.format(resource_name=resource_name, ref='{query_key}', val=_query_key)
       from django.http import HttpResponseRedirect
       HttpResponseRedirect(next_url)

_gen_method = get_{resource_name}{plural}_within_{prefix}(resource_name)
"""



In the new function of WeChatView, we can generate the above method and attache it the view itself. or

__code = parse(__code, plural=True) + """
setattr(viewset, _gen_method)
"""
exec(__code)

Then parent will no longer be responsible for rendering children.

Which means We Do Not Need To Register Children View explicitly

Hence in perspective of programmers, they just need to proivde three methods:

get_queryset get_object get_object_plural

in a Parent

yiakwy avatar Mar 19 '16 02:03 yiakwy

Here is an example:

`` Sat, 19, Mar, 2016 08:14:24 [INFO]:router.py, api.router, in line 282 >> DefaultDynamicQueryRouter _code_gen: def get_resource_from_account(self, _query_key, resource_name, **kwargs):

   def to_url_param(kwargs):
        args_proc = lambda o: \
        '='.join(o[0],o[1])
        ob_map = map(args_proc, list(kwargs.items()))
        return list(ob_map)

   next_url_tpl = "^{resource_name}?" + '&'.join( ['{ref}={val}', ].extend(to_url_param(kwargs)) )
   self._logger.info('gen_url: %s', next_url_tpl)
   next_url = next_url_tpl.format(resource_name=resource_name, ref='wechat_id', val=_query_key)
   from django.http import HttpResponseRedirect
   HttpResponseRedirect(next_url)

_gen_func_hook = get_resource_from_account setattr(viewset, 'get_resource_from_account', _gen_func_hook) ``

yiakwy avatar Mar 19 '16 08:03 yiakwy

I am having a BrainOverflowException into my head trying to get this. Halp!

@c17r and @ChristianKreuzberger, can you please give me a hand here?

alanjds avatar Mar 12 '17 16:03 alanjds

I would agree a full PR with tests would be easier to wrap our heads around and discuss instead of just snippets.

If my understanding of it is correct, I would say it's a little too "magical".

c17r avatar Mar 12 '17 16:03 c17r

@c17r @alanjds , A pull request has been submitted.

yiakwy avatar Mar 31 '17 08:03 yiakwy