diff --git a/Complement.md b/Complement.md new file mode 100644 index 00000000..35bc266b --- /dev/null +++ b/Complement.md @@ -0,0 +1,61 @@ +Quick Start +----------- + +The historical approaches to "nested routers" can be devided into two categries: +1) First proposed in Microsoft .net asp framework, aka users explicitly specify the relationship between resources. Hence, +it also requires developers to implement their corresponding view methods to handle the relationship queries. +2) In RESTful scheme, we rarely concern about the relationship but refines the relationship in logics. i.e we have resources "A", then we define CRUD operations upon it using query key or looking up key. The relationships maintained by develpers' logics. + + +Hence this broughts redundancies for a fast and hight level templated programmes. Here I proposed another approach, as far as I concerned, no people really use it. Please update the historical approaches to let me know. + +The plan is : +> relationship is actually a tree based concept. We have foreign keys for children to find their parents, and what if we have some tools to help parents to list their children? +We also need "inferring engine" to deal with the problems like "A's Parent's child' uncle C, what is the relationship between A & C" (in progress, I am working AI problems) + +i.e +> a url like "parent/childB/blablabla" can be converted by engine as "childB/blablabla/?query\_key=constrained\_by\_`parent`" +The process is a finite states machine if we have the relationship graph. + +Hence it is the users' responsibility to specify the path from parent to children in viewsets like: +```python +class WebSiteViewSet(ViewSetMixin, + WebSiteViewRouter): + + verbose_key = 'website' + prefix_abbr = 'ws' + + affiliates = ['headline',] + +class HeadlineViewSet(ViewSetMixin, + HeadlineViewRouter): + + verbose_key = 'headline' + prefix_abbr = 'hl' +``` + +The programme should deduce a mehtod to convert the relationship so that it is totally methods already hanlded by children +I currently use "Redirect" method to land the proposal, and it happens inside server. In my last tests, it works. + +A user can simply specify the in relationship in two files: +1. viewset.py (illustrated above) +2. urls.py: +```python +from router import DefaultDynamicQueryRouter as DefaultNestedRouter +from viewset import WebSiteViewSet +nested_router = DefaultNestedRouter(is_method_attached=True) +nested_router.register(r'website', WebSiteViewSet) + +urlpatterns += patterns('', + url(r'^api/{v:}/'.format(v=VERSION), include(nested_router.urls)), +) +``` + + +Testing +======= +I use django test services, simply run +``` +python test_main.py +``` +You can checkout the result in tests/nested\_router.out diff --git a/rest_framework_nested/api/__init__.py b/rest_framework_nested/api/__init__.py new file mode 100644 index 00000000..28cf7fe9 --- /dev/null +++ b/rest_framework_nested/api/__init__.py @@ -0,0 +1 @@ +__author__ = 'wangyi' diff --git a/rest_framework_nested/api/router.py b/rest_framework_nested/api/router.py new file mode 100644 index 00000000..e062698c --- /dev/null +++ b/rest_framework_nested/api/router.py @@ -0,0 +1,577 @@ +from __future__ import unicode_literals + +__author__ = 'wangyi' + +from collections import OrderedDict, namedtuple +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import NoReverseMatch +from django.conf.urls import url + +from rest_framework.routers import SimpleRouter +from rest_framework.routers import flatten +from rest_framework.response import Response +from rest_framework import views +from rest_framework.reverse import reverse +from rest_framework.urlpatterns import format_suffix_patterns + +import logging +from utils.log import LoggerAdaptor +_logger = logging.getLogger("api.router") + +TYPE_RE = u"(?P<{type}>[a-z0-9_][a-z0-9\s_]+?)" +QUERY_KEY_RE = u"(?P<{query_key}>[a-z0-9_\s\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\x00-\xff]+)" # ^(?!accounts$) +RESOURCE_NAME_RE = u"(?P[^a-z0-9_\s\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\x00-\xff]+)" +RESOURCE_QK_RE = u"(?P[a-z0-9_\s\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\x00-\xff]+)" +FORMAT_RE = u"?\.(?P[a-z0-9]+)" + +MTCH_STAR = '^' +MTCH_END = '$' +TRAILING_SLASH = '/' + +import re +REL_RE = re.compile(r"{(resource_name)\}") + +Router = namedtuple('Router', ['url_regex_tpl', 'mapping', 'name_tpl', 'init_kwargs']) +NestedRouter = namedtuple('NestedRouter', ['url_regex_tpl', 'mapping', 'name_tpl', 'init_kwargs']) +DyDetailRouter = namedtuple('DyDetailRouter', ['url_regex_tpl', 'name_tpl', 'init_kwargs']) +DyListRouter = namedtuple('DyListRouter', ['url_regex_tpl', 'name_tpl', 'init_kwargs']) + + +class SimpleDynamicQueryRouter(SimpleRouter): + + logger = LoggerAdaptor("SimpleDynamicQueryRouter", _logger) + _attached = [] + + routers = [ + Router( # list + url_regex_tpl='{prefix}/', + mapping={ + 'get': 'get_{verbose_key}s', + }, + name_tpl='{prefix_abbr}_{verbose_key}(s)', # 'wct_accounts', must be in plural form + init_kwargs={} + ), + Router( + url_regex_tpl='{prefix}/{look_at}/', + mapping={ + 'get': 'get_{verbose_key}_by_{query_key}', + 'post': 'create_{verbose_key}_by_{query_key}', + 'put': 'update_{verbose_key}_by_{query_key}', + 'patch': 'update_partially_{verbose_key}_by_{query_key}', + 'delete': 'delete_{verbose_key}_by_{query_key}', + }, + name_tpl='{prefix_abbr}_{verbose_key}', + init_kwargs={} + ), + Router( # list + url_regex_tpl='{prefix}/resources/', + mapping={ + 'get': 'get_resources', + }, + name_tpl='{prefix_abbr}_resource(s)', + init_kwargs={} + ), + NestedRouter( # list + url_regex_tpl='{prefix}/{resource_name}/', + mapping={ + 'get': 'get_{resource_name}s_within_{prefix}', # plural = True + }, + name_tpl='{prefix_abbr}_{resource_name}_by_name', + init_kwargs={} + ), + NestedRouter( # detailed->list + url_regex_tpl='{prefix}/{look_at}/resources/', + mapping={ + 'get': 'get_resources_from_{verbose_key}', # plural = True + }, + name_tpl='{prefix_abbr}_resource(s)_from_{verbose_key}', + init_kwargs={} + ), + NestedRouter( + url_regex_tpl='{prefix}/{look_at}/{resource_name}/', + mapping={ + 'get': 'get_{resource_name}_from_{verbose_key}', + 'post': 'create_{resource_name}_from_{verbose_key}', + 'put': 'update_{resource_name}_from_{verbose_key}', + 'patch': 'update_partially_{resource_name}_from_{verbose_key}', + 'delete': 'delete_{resource_name}_from_{verbose_key}', + }, + name_tpl='{prefix_abbr}_{resource_name}_from_{verbose_key}', + init_kwargs={} + ), + DyDetailRouter( + url_regex_tpl='{prefix}/{look_at}/', + name_tpl='{method}-detail', + init_kwargs={} + ), + DyListRouter( + url_regex_tpl='{prefix}/', + name_tpl='{method}-list', + init_kwargs={} + ) + + ] + + def __init__(self, is_method_attached=True, resource_name=None): + self.is_method_attached = is_method_attached + self._resource_name = resource_name + self._attached = [] + self.clear() + self._verbose_key = None + self._query_key = None + self._prefix_abbr = None + self._resource_name = None + super(SimpleRouter, self).__init__() + + def clear(self): + self._verbose_key = None + self._query_key = None + self._prefix_abbr = None + self._resource_name = None + + def register(self, prefix, viewset, base_name=None): + if base_name is not None: + self._prefix_abbr = base_name + super(SimpleDynamicQueryRouter, self).register(prefix, viewset, base_name) + + def get_attr(self, nested_view_router, attr_name, default=None): + attr = getattr(nested_view_router, attr_name, default) + # if not attr: + # raise KeyError("{0} Not Defined!".format(attr_name)) + return attr + + def get_method_map(self, nested_view_router, method_map, **kwargs): + bounded_method = {} + + for http_action, methodname_tpl in method_map.items(): + methodname = methodname_tpl.format(**kwargs).lower() + + if hasattr(nested_view_router, methodname) or self.is_method_attached: + bounded_method[http_action] = methodname + self._attached.append({nested_view_router.queryset.model._meta.object_name: bounded_method}) + # self.logger.info('router._attached: %s', self._attached) + return bounded_method + + def get_verbose_key(self, nested_view_router): + if not self._verbose_key: + attr = self.get_attr(nested_view_router, 'verbose_key') + self._verbose_key = attr + return self._verbose_key + + def get_query_key(self, nested_view_router): + if not self._query_key: + attr = self.get_attr(nested_view_router, 'query_key') + self._query_key = attr + return self._query_key + + def get_prefix_abbr(self, nested_view_router): + if not self._prefix_abbr: + attr = self.get_attr(nested_view_router, 'prefix_abbr') + self._prefix_abbr = attr + return self._prefix_abbr + + def get_resource_name(self, nested_view_router): + if not self._resource_name: + # self._resource_name = "resource" + attr = self.get_attr(nested_view_router, 'affiliates', None) + self._resource_name = attr + return self._resource_name + + def get_look_at_regex(self, nested_view_router, look_at_prefix=''): + """ + ' Given a viewset, return the portion of URL regex that is used + to match against a single instance. + + Note that lookup_prefix is not used directly inside REST rest_framework + itself, but is required in order to nicely support nested router + implementations, such as drf-nested-routers. + + https://github.com/alanjds/drf-nested-routers ' quoted by django_rest + """ + query_key = self.get_attr(nested_view_router, 'query_key', 'pk') + look_at_regex = QUERY_KEY_RE.format(look_at_prefix=look_at_prefix, query_key=query_key) + return look_at_regex + + def get_routers(self, nested_view_router, **kwargs): + + # known actions + known_actions = flatten([route.mapping.values() for route in self.routes + if isinstance(route, Router)]) + # get methods attached to the view + detail_router, list_router = [], [] + # if @detail @list decorate the method, we reserve it for updating + for methodname in dir(nested_view_router): + attr = self.get_attr(nested_view_router, methodname) + http_methods = getattr(attr, "binding_to_methods", None) + detail = getattr(attr, 'detail', True) + if http_methods: + # user are using decorator to bind methods + # we need to modify these codes + if methodname in known_actions: + raise ImproperlyConfigured('method {0} has already been in router method mapping') + if detail: + detail_router.append(({http_action: methodname for http_action in http_methods}, methodname)) + else: + list_router.append(({http_action: methodname for http_action in http_methods}, methodname)) + + def _get_query_router(router): + return [Router( + url_regex_tpl=router.url_regex_tpl, + mapping=router.mapping, + name_tpl=router.name_tpl, + init_kwargs=router.init_kwargs + )] + + def _get_detail_router(router, detail_ret): + ret = [] + for _mapping, methodname in detail_ret: + + ret.append(DyDetailRouter( + url_regex_tpl=router.url_regex_tpl.format(methodname=methodname), + mapping=router.mapping.update(_mapping), + name_tpl=router.name_tpl.format(method=methodname), + init_kwargs=router.init_kwargs) + ) + return ret + + def _get_list_router(router, list_ret): + ret = [] + for _mapping, methodname in list_ret: + + ret.append(DyListRouter( + url_regex_tpl=router.url_regex_tpl.format(methodname=methodname), + mapping=router.mapping.update(_mapping), + name_tpl=router.name_tpl.format(method=methodname), + init_kwargs=router.init_kwargs) + ) + return ret + + def _compose_nested_router(router, names): + ret = [] + if names is None: + # do implementation here + pass + else: + for name in names: + _map = {} + + for k, v in router.mapping.items(): + _map[k] = REL_RE.sub(name, v) + + ret.append(NestedRouter( + url_regex_tpl=REL_RE.sub('(?P{name})'.format(name=name) + '/' + RESOURCE_QK_RE, router.url_regex_tpl), # !important + mapping=_map, + name_tpl=REL_RE.sub(name, router.name_tpl), + init_kwargs=router.init_kwargs)) + + return ret + + # return routers + ret = [] + + for router in self.routers: + # add method routing to every router + + if isinstance(router, NestedRouter): + ret += _compose_nested_router(router, kwargs.get('resource_name', None)) + elif isinstance(router, Router): + ret += _get_query_router(router) + elif isinstance(router, DyDetailRouter): + ret += _get_detail_router(router, detail_router) + elif isinstance(router, DyListRouter): + ret += _get_list_router(router, list_router) + # self.logger.info('ret: %s', ret) + return ret + + def get_urls(self): + + ret = [] + for prefix, nested_view_router, \ + basename in self.registry: + self.clear() + # get 'look_at' regex, verbose_key, prefix_abbr + look_at = self.get_look_at_regex(nested_view_router) + verbose_key = self.get_verbose_key(nested_view_router) + query_key = self.get_query_key(nested_view_router) + prefix_abbr = self.get_prefix_abbr(nested_view_router) or basename + resource_name = self.get_resource_name(nested_view_router) + # get routers related to the nested_view_router + _routers = self.get_routers(nested_view_router, resource_name=resource_name) + # loop through routers and compose url + + for router in _routers: + # get method mapping + # self.logger.info('router: %s', str(type(router))) + if isinstance(router, Router): + method_mapping = self.get_method_map(nested_view_router, router.mapping, + verbose_key=verbose_key, query_key=query_key, prefix=prefix) + if isinstance(router, NestedRouter): + method_mapping = self.get_method_map(nested_view_router, router.mapping, + verbose_key=verbose_key, query_key=query_key, prefix=prefix, resource_name=resource_name) + # form url regex + url_regex = self.compose_url_regex(router.url_regex_tpl, look_at, prefix, RESOURCE_NAME_RE) + # form name + name = self.compose_name(router.name_tpl, prefix_abbr, verbose_key, resource_name) + # produce view function + try: + view_func = nested_view_router.as_view(method_mapping, **router.init_kwargs) # ? + except TypeError as err: + self.logger.info(err) + self.logger.info('method_mapping: %s', method_mapping) + import traceback + traceback.print_exc() + # append to url list + ret.append(url(url_regex, view_func, name=name)) + + return ret + + def compose_url_regex(self, url_regex_tpl, look_at, prefix, resource_name_re): + # self.logger.info("look_at: %s", look_at) + url_regex = \ + url_regex_tpl.format(prefix=prefix, look_at=look_at, resource_name=resource_name_re) + \ + MTCH_END + return url_regex + + def compose_name(self, name_tpl, prefix_abbr, verbose_key, resource_name_re): + name = name_tpl.format(prefix_abbr=prefix_abbr, verbose_key=verbose_key, resource_name=resource_name_re) + return name + + +class RelationalDynamicQueryRouter(SimpleDynamicQueryRouter): + + logger = LoggerAdaptor("RelationalDynamicQueryRouter", _logger) + + def _process_view(self, viewset, methodname, query_key, prefix): + if methodname.upper() == 'GET': + self._process_get_view(self, viewset, methodname, query_key, prefix) + elif methodname.upper() == 'POST': + self._process_create_view(self, viewset, methodname, query_key, prefix) + elif methodname.upper() == 'PUT': + + self._process_update_view(self, viewset, methodname, query_key, prefix) + elif methodname.upper() == 'PATCH': + self._proces_partially_update(self, viewset, methodname, query_key, prefix) + elif methodname.upper() == 'DELETE': + self._process_delete_view(self, viewset, methodname, query_key, prefix) + + def _process_delete_view(self, viewset, methodname, query_key, prefix): + + __code = """ +def {method_name}(self, req, + {query_key}=None, + **kwargs): + raise Exception("Not Implemented: {method_name}") + +_gen_func_hook = {method_name} +setattr(viewset, '{method_name}', _gen_func_hook) + """ + viewset._logger = self.logger + __code = __code.format(method_name=methodname, query_key=query_key) + self.logger.info('_code_gen of %s: %s', viewset.__name__, __code) + try: + exec(__code) + except Exception as err: + raise(err) + + def _process_create_view(self, viewset, methodname, query_key, prefix): + + __code = """ +def {method_name}(self, req, + {query_key}=None, + resource_name=None, + res_query_key=None, + **kwargs): + raise Exception("Not Implemented: {method_name}") + +_gen_func_hook = {method_name} +setattr(viewset, '{method_name}', _gen_func_hook) + """ + viewset._logger = self.logger + __code = __code.format(method_name=methodname, query_key=query_key) + self.logger.info('_code_gen of %s: %s', viewset.__name__, __code) + try: + exec(__code) + except Exception as err: + raise(err) + + def _process_update_view(self, viewset, methodname, query_key, prefix): + + __code = """ +def {method_name}(self, req, + {query_key}=None, + resource_name=None, + res_query_key=None, + **kwargs): + raise Exception("Not Implemented: {method_name}") + +_gen_func_hook = {method_name} +setattr(viewset, '{method_name}', _gen_func_hook) + """ + viewset._logger = self.logger + __code = __code.format(method_name=methodname, query_key=query_key) + self.logger.info('_code_gen of %s: %s', viewset.__name__, __code) + try: + exec(__code) + except Exception as err: + raise(err) + + def _proces_partially_update(self, viewset, methodname, query_key, prefix): + + __code = """ +def {method_name}(self, req, + {query_key}=None, + resource_name=None, + res_query_key=None, + **kwargs): + raise Exception("Not Implemented: {method_name}") + +_gen_func_hook = {method_name} +setattr(viewset, '{method_name}', _gen_func_hook) + """ + viewset._logger = self.logger + __code = __code.format(method_name=methodname, query_key=query_key) + self.logger.info('_code_gen of %s: %s', viewset.__name__, __code) + try: + exec(__code) + except Exception as err: + raise(err) + + def _process_get_view(self, viewset, methodname, query_key, prefix): + + __code = """ +def {method_name}(self, req, + {query_key}=None, + resource_name=None, + res_query_key=None, + **kwargs): + + def to_url_param(kwargs): + args_proc = lambda o: u'='.join([o[0],o[1]]) + ob_map = map(args_proc, list(kwargs.items())) + return list(ob_map) + try: + + url_args = [u'{{ref}}={{val}}', ] + url_args.extend(to_url_param(kwargs)) + # self.logger.info(u"new_kwargs: %s", kwargs) + # self.logger.info(u"url_args: %s", url_args) + + from RESTful_api.urls import PREFIX + from api.views import parse + + WSGI_Req = req._request + SCHEME = 'http://' + HOST = WSGI_Req.META['HTTP_HOST']# protocle + REMOTE_ADDR = WSGI_Req.META['REMOTE_ADDR'] + PROTOCOL = WSGI_Req.META['SERVER_PROTOCOL'] + + RES_URL_tpl = u"{{resource_name}}/?" if not res_query_key \ + else '/'.join([u"{{resource_name}}", res_query_key]) + '/?' + next_url_tpl = SCHEME + HOST + '/' + PREFIX.lstrip('^') + RES_URL_tpl + '&'.join(url_args) + self.logger.info(u'gen_next_url_tpl: %s', next_url_tpl) + next_url = next_url_tpl.format(resource_name=parse(resource_name), + ref='{query_key}', val={query_key}) + self.logger.info(u'gen_next_url: %s', next_url) + from django.http import HttpResponseRedirect + return HttpResponseRedirect(next_url) + except Exception as err: + import traceback + traceback.print_exc() + raise(err) + + +_gen_func_hook = {method_name} +setattr(viewset, '{method_name}', _gen_func_hook) + """ + viewset._logger = self.logger + __code = __code.format(method_name=methodname, query_key=query_key) + self.logger.info('_code_gen of %s: %s', viewset.__name__, __code) + try: + exec(__code) + except Exception as err: + raise(err) + + def get_method_map(self, nested_view_router, method_map, **kwargs): + query_key = kwargs['query_key'] + prefix = kwargs['prefix'] + resource_name = kwargs.get('resource_name', None) + if prefix: + if '/' in prefix: + v = '_'.join(prefix.split('/')) + kwargs['prefix'] = v + bounded_method = super(RelationalDynamicQueryRouter, self).get_method_map(nested_view_router, method_map, **kwargs) + if not resource_name: + return bounded_method + if self.is_method_attached: + for http_action, methodname in bounded_method.items(): + self._process_view(nested_view_router, methodname, query_key, prefix) + return bounded_method + + +class DefaultDynamicQueryRouter(RelationalDynamicQueryRouter): # SimpleDynamicQueryRouter + + logger = LoggerAdaptor("DefaultDynamicQueryRouter", _logger) + + """ + The default router extends the SimpleRouter, but also adds in a default + API root view, and adds format suffix patterns to the URLs. + """ + include_root_view = True + include_format_suffixes = True + root_view_name = 'root' + + def get_api_root_view(self): + """ + Return a view to use as the API root. + """ + api_root_dict = OrderedDict() + for prefix, viewset, basename in self.registry: + prefix_abbr = self.get_attr(viewset, 'prefix_abbr') + verbose_key = self.get_attr(viewset, 'verbose_key') + api_root_dict[prefix] = '{prefix_abbr}_{verbose_key}(s)'.format(prefix_abbr=prefix_abbr, verbose_key=verbose_key) + + class APIRoot(views.APIView): + _ignore_model_permissions = True + + def get(self, request, *args, **kwargs): + ret = OrderedDict() + namespace = request.resolver_match.namespace + for key, url_name in api_root_dict.items(): + if namespace: + url_name = namespace + ':' + url_name + try: + ret[key] = reverse( + url_name, + args=args, + kwargs=kwargs, + request=request, + format=kwargs.get('format', None) + ) + except NoReverseMatch: + # Don't bail out if eg. no list routes exist, only detail routes. + continue + + return Response(ret) + + return APIRoot.as_view() + + def get_urls(self): + """ + Generate the list of URL patterns, including a default root view + for the API, and appending `.json` style format suffixes. + """ + urls = [] + + if self.include_root_view: + root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name) + urls.append(root_url) + + default_urls = super(DefaultDynamicQueryRouter, self).get_urls() + urls.extend(default_urls) + + if self.include_format_suffixes: + urls = format_suffix_patterns(urls) + + # self.logger.info("router.urls: %s", json.dumps([str(item) for item in urls], sort_keys=True, indent=4)) + # self.logger.info("attached methods: %s", json.dumps(self._attached, sort_keys=True, indent=4)) + return urls diff --git a/rest_framework_nested/api/tests.py b/rest_framework_nested/api/tests.py new file mode 100644 index 00000000..c1fbae65 --- /dev/null +++ b/rest_framework_nested/api/tests.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +__author__ = 'wangyi' + + +from django.test import SimpleTestCase +import json + +import logging +from utils.log import LoggerAdaptor +_logger = logging.getLogger("api.tests") + + +class DynamicQueryRouterTest(SimpleTestCase): + + logger = LoggerAdaptor("TestDynamicQueryRouter", _logger) + + def test_dynamic_query_router(self): + from router import DefaultDynamicQueryRouter as DefaultNestedRouter + from viewset import WebSiteViewSet + nested_router = DefaultNestedRouter(is_method_attached=True) + + nested_router.register(r'website', WebSiteViewSet) + + urls = nested_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(nested_router._attached, sort_keys=True, indent=4)) diff --git a/rest_framework_nested/api/utils/__init__.py b/rest_framework_nested/api/utils/__init__.py new file mode 100644 index 00000000..28cf7fe9 --- /dev/null +++ b/rest_framework_nested/api/utils/__init__.py @@ -0,0 +1 @@ +__author__ = 'wangyi' diff --git a/rest_framework_nested/api/utils/auth_utils.py b/rest_framework_nested/api/utils/auth_utils.py new file mode 100644 index 00000000..9f785702 --- /dev/null +++ b/rest_framework_nested/api/utils/auth_utils.py @@ -0,0 +1,36 @@ +__author__ = 'wangyi' + +import hmac +import hashlib +import base64 +from uuid import uuid4 +import random +import string + +from django.utils import timezone + + +def app_key_gen(): + return ''.join([random.SystemRandom().choice("{}".format(string.ascii_uppercase)) for i in range(16)]) + + +def app_secret_coder(api_secret, msg, algorithm="hmac-sha256"): + algo, hash_scheme = algorithm.split("-") + if algo == "hmac" and hash_scheme == "sha256": + digest_obj = hmac.new(api_secret, msg=msg, digestmod=hashlib.sha256).digest() + else: + raise Exception("(%s, %s) Not Be Supported Yet!" % (algo, hash_scheme)) + return base64.b64encode(digest_obj).decode() + + +def app_secret_gen(): + coder = hashlib.sha256() + salt = uuid4().hex + coder.update(timezone.now().strftime('%H%M%S') + salt) + return coder.hexdigest() + + +def check_sign_sim(sign_req, sign_serv): + # check algorithm + # decide cmp algorithm and whether to update + return sign_req == sign_serv diff --git a/rest_framework_nested/api/utils/datetime.py b/rest_framework_nested/api/utils/datetime.py new file mode 100644 index 00000000..c6cf01ce --- /dev/null +++ b/rest_framework_nested/api/utils/datetime.py @@ -0,0 +1,12 @@ +__author__ = 'wangyi' + +import time +import calendar + + +def to_seconds_from_datetime(dt): + return time.mktime(dt.timetuple()) + + +def to_seconds_from_datetime2(dt): + return calendar.timegm(dt.timetuple()) diff --git a/rest_framework_nested/api/utils/exceptions.py b/rest_framework_nested/api/utils/exceptions.py new file mode 100644 index 00000000..22e4cda8 --- /dev/null +++ b/rest_framework_nested/api/utils/exceptions.py @@ -0,0 +1,49 @@ +__author__ = 'wangyi' + +from rest_framework.exceptions import APIException +from rest_framework.views import exception_handler +from rest_framework.response import Response + + +def sys_exc_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + # Now add the HTTP status code to the response. + if response is not None: + pass + + return response + + +def handle_exc(ref, status_code, details=None): + cause = {'Cause': "<'%s'> Does Not Exist!" % ref, 'details': details} + rep = Response(data=cause, status=status_code) + return rep + + +class REST_APIException(APIException): + + default_details = "" + + def __init__(self, detail=None): + if detail is None: + self.detail = self.default_details + else: + self.detail = {'msg': detail, 'default': self.default_details} + + +class REST_API_INPUT_Excepiton(REST_APIException): + pass + + +class Cols_Not_Found(REST_API_INPUT_Excepiton): + + status_code = 5001 + default_details = "Cols Not Matched" + + +class BAD_SIGN(REST_API_INPUT_Excepiton): + + status_code = 5002 + default_details = "Sign Is Bad!" diff --git a/rest_framework_nested/api/utils/log.py b/rest_framework_nested/api/utils/log.py new file mode 100644 index 00000000..2a0dc37c --- /dev/null +++ b/rest_framework_nested/api/utils/log.py @@ -0,0 +1,17 @@ +__author__ = 'wangyi' + +import logging +# formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +# desired formmat e.g: +# [%(levelname)s] module_name.class_name %(asctime)s: + + +class LoggerAdaptor(logging.LoggerAdapter): + + def __init__(self, prefix, logger): + # super(self, App_LoggerAdaptor).__init__(logger, {}) + logging.LoggerAdapter.__init__(self, logger, {}) + self.prefix = prefix + + def process(self, msg, kwargs): + return "%s %s" % (self.prefix, msg), kwargs diff --git a/rest_framework_nested/api/utils/operator.py b/rest_framework_nested/api/utils/operator.py new file mode 100644 index 00000000..65bffcb5 --- /dev/null +++ b/rest_framework_nested/api/utils/operator.py @@ -0,0 +1,8 @@ +__author__ = 'wangyi' + + +def belong(left, right): + for i in left: + if i not in right: + return False + return True diff --git a/rest_framework_nested/api/utils/parser.py b/rest_framework_nested/api/utils/parser.py new file mode 100644 index 00000000..1b0bfe32 --- /dev/null +++ b/rest_framework_nested/api/utils/parser.py @@ -0,0 +1,83 @@ +__author__ = 'wangyi' + +import re +IDENTIFIER = r"[\w][\w\d_]*" +WHITE_CHARACTER = r"[ \n\t]" +# FIELDS = r"^(\+|-)?\((?P{ID}(?:,{WHITE}*{ID})*)\)".format(ID=IDENTIFIER, WHITE=WHITE_CHARACTER) +FIELDS = r"(?P{ID}(?:,{WHITE}*{ID})*)".format(ID=IDENTIFIER, WHITE=WHITE_CHARACTER) +FIELDS_LIMIT_RE = re.compile(FIELDS) +FIELDS_SIGN = r"(\+|-)?" +FIELDS_SIGN_RE = re.compile(r"^(\+|-)?") +FIELDS_OPEN_BRACKET_RE = re.compile(r"\(") +FIELDS_CLOSE_BRACKET_RE = re.compile(r"\)") +FIELDS_ARRAY = r"(?P{SIGN}{ID}(?:,{WHITE}*{SIGN}{ID})*)".format(SIGN=FIELDS_SIGN, ID=IDENTIFIER, WHITE=WHITE_CHARACTER) +FIELDS_ARRAY_RE = re.compile(FIELDS_ARRAY) + +# This function updated on 7th Nov 2016 +ret_array = None +sign = None + + +def filed_parse(fields_string): + sign = '+' + ret_array = {'+': [], '-': []} + + mtch = FIELDS_SIGN_RE.match(fields_string) + if mtch.group() is not '' and mtch.group() in (u'+', u'-'): + sign = mtch.group() + _, end = mtch.span() + mtched, new_string = _open_bracket(fields_string[end:]) + return sign, ret_array[sign] + + +def _open_bracket(raw_string): + mtch = FIELDS_OPEN_BRACKET_RE.match(raw_string) + if not mtch: + mtched, new_string = _array_figure(raw_string) + if new_string != '': + raise Exception("Fields in wrong format!") + array = mtched.split(',') + ret_array[sign].append((sign, array[0].strip())) + for i in array[1:]: + i = i.strip() + ret_array[i[0]].append(i[1:]) + else: + _, end = mtch.span() + mtched, new_string = _group_figure(raw_string[end:]) + _, next_new_string = _close_bracket(new_string) + if next_new_string != '': + raise Exception("Fields in wrong format") + array = mtched.split(',') + for i in array: + ret_array[sign].append(i) + + return mtched, '' + + +def _close_bracket(raw_string): + mtch = FIELDS_CLOSE_BRACKET_RE.match(raw_string) + if not mtch: + raise Exception("Fields not properly closed!") + _, end = mtch.span() + return mtch.group(), raw_string[end:] + + +def _group_figure(raw_string): + mtch = FIELDS_LIMIT_RE.match(raw_string) + if mtch is None: + raise Exception("Fields could not be None!") + _, end = mtch.span() + return mtch.group(), raw_string[end:] + + +def _array_figure(raw_string): + mtch = FIELDS_ARRAY_RE.match(raw_string) + if mtch is None: + raise Exception("Fields could not be None!") + _, end = mtch.span() + return mtch.group(), raw_string[end:] + + +if __name__ == "__main__": + fields = "+(1,2,3,45)" + print(filed_parse(fields)) diff --git a/rest_framework_nested/api/utils/tokenTab.py b/rest_framework_nested/api/utils/tokenTab.py new file mode 100644 index 00000000..ac469b53 --- /dev/null +++ b/rest_framework_nested/api/utils/tokenTab.py @@ -0,0 +1,6 @@ +__author__ = 'wangyi' + +ASCII_SPACE = ' ' +ASCII_ENDL = '\n' +ASCII_COMMA = ',' +ASCII_EQ = '=' diff --git a/rest_framework_nested/api/utils/v_utils.py b/rest_framework_nested/api/utils/v_utils.py new file mode 100644 index 00000000..5e14f32a --- /dev/null +++ b/rest_framework_nested/api/utils/v_utils.py @@ -0,0 +1,60 @@ +__author__ = 'wangyi' + +MODEL_PREFIX = 'third_party.models.' +SERIALIZER_PREFIX = 'third_party.serializer' + + +# magic method +def get_args_by_req(req, + prop_args=None, + expected_header_args=None, + f=None): + + ol = None + # header: req.META + + # req.[url_method] + method = req.method + try: + _full_args = req.__getattribute__(method) + except: + _full_args = {} + if prop_args is None: + ol = _full_args + else: + if isinstance(prop_args, str): + ol = {key: _full_args[key] for key in prop_args.split(',')} + else: + raise Exception("Not Implemented!") + + if f is not None: + ol = f(ol) + return ol + + +# help function for get_res_model +def to_kls(short_cut): + if short_cut.lower() == 'wechat': + return 'WeChatAccount' + if short_cut.lower() == 'messages': + return 'WeChatMSG' + return short_cut + + +def Import_factory(*args, **kwargs): + if kwargs == {}: + ob_map = map(to_kls, args) + if len(args) == 1: + kls_path = MODEL_PREFIX + '.'.join(ob_map) + else: + kls_path = '.'.join(ob_map) + + from django.utils.module_loading import import_string + try: + kls = import_string(kls_path) + except ImportError as err: + raise err + return kls.__name__, kls + + else: + raise Exception("Not Implemented!") diff --git a/rest_framework_nested/api/views.py b/rest_framework_nested/api/views.py new file mode 100644 index 00000000..f7462e41 --- /dev/null +++ b/rest_framework_nested/api/views.py @@ -0,0 +1,277 @@ +from __future__ import unicode_literals +__author__ = 'wangyi' + +import json +from ..third_party.models import WebSite, Headline +from utils import parser +from ..third_party.serializer import WebSiteSerializer, HeadlineSerializer, \ + SerializerManager +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework import generics, filters +from rest_framework import status +from rest_framework.permissions import IsAuthenticatedOrReadOnly + +from utils.v_utils import get_args_by_req +from utils.exceptions import handle_exc, BAD_SIGN, Cols_Not_Found + +import logging +from utils.log import LoggerAdaptor +_logger = logging.getLogger("api.tests") + + +class NestedViewRouter(generics.GenericAPIView): + + # authentication_classes = (SignatureAuth, TokenAuthentication, ) + + _LIMIT = 1000 + PAGINATION_DEF = 10 + + query_set = None + serializer_class = None + + logger = LoggerAdaptor("analyse_dynamic_runtime", _logger) + + filter_backends = (filters.SearchFilter, filters.OrderingFilter,) + exclude_from_selector_fields = (u'ordering', u'search', u'offset', u'limit', u'fields', + u'format', u'key', u'signature', u'url', u'date', u'request_target', + u'next_url') + sign, cols = None, None + query_key = 'pk' + + @property + def _req_method(self): + return self.request.method.lower() + + def dispatch(self, request, + *args, **kwargs): + """ + `.dispatch()` is pretty much the same as Django_Rest's regular dispatch, + but with extra hooks for filtering, extracting, and pagination. + """ + # substitute => initialize_request + # return a request in order to be used by viewset.initial_request + request = self._init_req(request) + self.headers = self.default_response_headers # deprecate? + + try: + self.initial(request, *args, **kwargs) + # Get the appropriate handler method + handler = getattr(self, self._req_method, self.http_method_not_allowed) \ + if self._req_method in self.http_method_names else \ + self.http_method_not_allowed + + # from dill.source import getsource + # self.logger.info(getsource(handler)) + # https://www.ibm.com/developerworks/cn/linux/l-cn-pythondebugger/ + self.logger.info(handler) + # pdb.set_trace() + # analyse source codes + response = handler(request, *args, **kwargs) + + except Exception as exc: + # pdb.pm() + response = self.handle_exception(exc) + + self.response = self.finalize_response(request, response, *args, **kwargs) + return self.response + + def _init_req(self, request): + self.query_params = get_args_by_req(request) + parser_context = self.get_parser_context(request) + + self.request = Request(request, parsers=self.get_parsers(), parser_context=parser_context, + authenticators=self.get_authenticators(), + negotiator=self.get_content_negotiator()) + return self.request + # hook to initialize_request + initialize_request = _init_req + + def only_cols(self, query_set, fields_string): + sign, cols = parser.field_parse(fields_string) + query_set = query_set.only(*cols) if sign == '+' else query_set.defer(*cols) + self.sign, self.cols = sign, cols + return query_set + + # override the original mehtod to perform magic filtering + def get_queryset(self, query_set=None, serializer_class=None): + + if query_set is None: + _qs = super(NestedViewRouter, self).get_queryset() + query_set = _qs + + query_set = self.filter_queryset(query_set) + selector_args = {} + + # filtering fields on serializer + def get_valid_fields(serializer_class): + valid_fields = [ + field.source or field_name + for field_name, field in serializer_class().fields.items() + if not getattr(field, 'write_only', False) and not field.source == '*' + ] + return valid_fields + + if serializer_class is None: + serializer_class = self.serializer_class + + valid_fields = get_valid_fields(serializer_class) + limit = self._LIMIT + for k, v in self.query_params.items(): + if k not in self.exclude_from_selector_fields and \ + k in valid_fields: + selector_args[k] = v + + elif k == u'offset': + limit = int(v) + limit + + elif k == u'fields': + try: + query_set = self.only_cols(query_set, v) + except Cols_Not_Found as err: + err.detail = """ + Available Fields: %s + """ % json.dump(valid_fields, sort_keys=True, indent=4) + raise err + except BAD_SIGN as err: + err.detail = """ + Available Signs: %s + """ % ('+', '-') + self._slicer = slice(0, limit) + query_set = query_set.filter(**selector_args) + return query_set + + def get_args(self): + return (self.args, self.kwargs) + + def get_object(self, req, pk): + selector_args = get_args_by_req(req, f=_extract_args) + query_set = self.get_queryset().filter(**selector_args) + label = self.query_key + selector_args[label] = pk + ob = query_set(**selector_args) + return ob + + def get_object_plural(self, req, pk=None): # plural + selector_args = get_args_by_req(req, f=_extract_args) + if pk is not None: + query_set = self.get_queryset().filter(**selector_args) + label = self.query_key + __code = "query_set=query_set.filter({pk}='{val}', **selector_args)" + exec(__code.format(pk=label, val=pk)) + query_set = query_set.__getitem__(self._slicer) + else: + query_set = self.get_queryset() \ + .filter(**selector_args) \ + .__getitem__(self._slicer) + # .order_by('-{query_key}'.format(query_key=self.query_key))\ + page = self.paginate_queryset(query_set) + return page + + def get_response(self, req, pk=None): + + def group(req): + page = self.get_object_plural(req) + repr = self.serializer(page, many=True) + if hasattr(repr, '__len__') and \ + len(repr) < self.PAGINATION_DEF: + return Response(data=repr) + else: + return self.get_paginated_response(data=repr) + + def ins(req, pk): + + query_set = self.get_object(req, pk) + repr = self.serializer(query_set) + return Response(repr) + + try: + if not pk: + rep = group(req) + else: + rep = ins(req, pk) + except Exception as err: + import traceback + traceback.print_exc() + return handle_exc(None, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + details=err.message) + return rep + + +def _extract_args(args): + black_list = NestedViewRouter.exclude_from_selector_fields + return {k: v for k, v in args.items() if k.lower() not in black_list} + + +class HeadlineViewRouter(NestedViewRouter): + + queryset = Headline.objects.all() + serializer_class = HeadlineSerializer + serializer = SerializerManager(HeadlineSerializer) + + ordering_fields = ('post_date', 'title', 'digest', 'score') + + search_fields = ('id', 'post_date', 'title', 'digest', 'website_id') + query_key = 'title' + + permission_classes = (IsAuthenticatedOrReadOnly,) + + def get_headlines(self, req): + return self.get_response(req) + + def get_headline_by_title(self, req, title): + return self.get_response(req, title) + + +class WebSiteViewRouter(NestedViewRouter): + + queryset = WebSite.objects.all() + serializer_class = WebSiteSerializer + serializer = SerializerManager(WebSiteSerializer) + + ordering_fields = ('id', 'name',) + + search_fields = ('id', 'name',) + query_key = 'name' + + permission_classes = (IsAuthenticatedOrReadOnly,) + + def get_websites(self, req): + return self.get_response(req) + + def get_website_by_name(self, req, name): + return self.get_response(req, name) + + def create_website_by_name(self, req, name, **param): + url = req.query_params.get("url", '') + tags = req.query_params.get("tags", '') + + if url is '' or tags is '': + return handle_exc(None, + status_code=status.HTTP_400_BAD_REQUEST, details="Not enough parameters") + + website = WebSite.objects.get(name=name) + if len(website) == 0: + website = WebSite(name=name, url=url, tags=tags) + website.save() + else: + website = website[0] + + serialized = WebSiteSerializer(website) + return Response(data=serialized.data, status=status.HTTP_201_CREATED, + headers={'Location': serialized.data.get('url', None)}) + + def update_partially_website_by_name(self, req, name, **param): + pass + + def delete_website_by_name(self, req, name): + wb = WebSite.objects.filter(name=name) + if len(wb) == 0: + return handle_exc(None, + status_code=status.HTTP_400_BAD_REQUEST, details="Not exist!") + data = WebSiteSerializer(wb, many=True).data + wb.delete() + return Response(data=data, status=status.HTTP_204_NO_CONTENT) + + update_website_by_name = get_website_by_name diff --git a/rest_framework_nested/api/viewset.py b/rest_framework_nested/api/viewset.py new file mode 100644 index 00000000..8aeaaa3e --- /dev/null +++ b/rest_framework_nested/api/viewset.py @@ -0,0 +1,20 @@ +__author__ = 'wangyi' + +from rest_framework.viewsets import ViewSetMixin +from views import WebSiteViewRouter, HeadlineViewRouter + + +class WebSiteViewSet(ViewSetMixin, + WebSiteViewRouter): + + verbose_key = 'website' + prefix_abbr = 'ws' + + affiliates = ['headline'] + + +class HeadlineViewSet(ViewSetMixin, + HeadlineViewRouter): + + verbose_key = 'headline' + prefix_abbr = 'hl' diff --git a/rest_framework_nested/runtests/settings.py b/rest_framework_nested/runtests/settings.py index 0bfd86c1..f91cc4af 100644 --- a/rest_framework_nested/runtests/settings.py +++ b/rest_framework_nested/runtests/settings.py @@ -99,7 +99,10 @@ # 'django.contrib.admindocs', # 'rest_framework.authtoken', 'rest_framework_nested', - 'rest_framework_nested.tests', + # This is not implemented by author, which will raise error by django commandline tool + # 'rest_framework_nested.tests', + # for test purpose and integration, we should separate data structure or db models + 'rest_framework_nested.third_party' ) # OAuth is optional and won't work if there is no oauth_provider & oauth2 @@ -159,8 +162,73 @@ # If we're running on the Jenkins server we want to archive the coverage reports as XML. import os +import sys if os.environ.get('HUDSON_URL', None): TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' TEST_OUTPUT_VERBOSE = True TEST_OUTPUT_DESCRIPTIONS = True TEST_OUTPUT_DIR = 'xmlrunner' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': u"%(asctime)s [%(levelname)s]:%(filename)s, %(name)s, in line %(lineno)s >> \n%(message)s".encode('utf-8'), + 'datefmt': "%a, %d, %b, %Y %H:%M:%S", # "%d/%b/%Y %H:%M:%S" + }, + 'simple': { + 'format': u'[%(levelname)s] %(filename)s %(lineno)s: %(message)s'.encode('utf-8') + }, + 'classic_formatter': { + 'format': u"%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s >> %(message)s".encode('utf-8'), + 'datefmt': "%a, %d, %b, %Y %H:%M:%S", + }, + 'default': { + 'format': u"%(asctime)s [%(levelname)s] [%(name)s:%(lineno)s] >> %(message)s".encode('utf-8'), + 'datefmt': "%d/%b/%Y %H:%M:%S", + } + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + 'formatter': 'verbose' + }, + 'email_info_tracer': { + 'level': 'ERROR', + 'class': 'logging.handlers.SMTPHandler', + 'formatter': 'verbose', + 'mailhost': '', + 'fromaddr': 'info_tracker_notify@weiboyi.com', + 'toaddrs': '', + 'subject': 'Info tracker API ERROR !', + 'credentials': ('', '') + }, + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'formatter': 'verbose', + 'filename': 'sys.log' + } + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'], + 'propagate': True, + 'level': 'INFO', + }, + 'api.tests': { + 'handlers': ['console'], + 'propagate': True, + 'level': 'INFO', + + }, + 'third_party.tests': { + 'handlers': ['console'], + 'propagate': True, + 'level': 'INFO', + }, + } +} diff --git a/rest_framework_nested/third_party/__init__.py b/rest_framework_nested/third_party/__init__.py new file mode 100644 index 00000000..28cf7fe9 --- /dev/null +++ b/rest_framework_nested/third_party/__init__.py @@ -0,0 +1 @@ +__author__ = 'wangyi' diff --git a/rest_framework_nested/third_party/migrations/0001_initial.py b/rest_framework_nested/third_party/migrations/0001_initial.py new file mode 100644 index 00000000..61b49395 --- /dev/null +++ b/rest_framework_nested/third_party/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Headline', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('url', models.URLField(max_length=256)), + ('post_date', models.DateField(null=True)), + ('digest', models.CharField(max_length=256, null=True)), + ('title', models.CharField(unique=True, max_length=64)), + ('score', models.IntegerField()), + ], + options={ + 'db_table': 'headline', + }, + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('username', models.CharField(unique=True, max_length=64)), + ('auth', models.OneToOneField(related_name='custom_info', verbose_name='User', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'user', + }, + ), + migrations.CreateModel( + name='UserWebSite', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('importance', models.IntegerField(null=True)), + ('user', models.ForeignKey(to='third_party.User')), + ], + options={ + 'db_table': 'user_website', + }, + ), + migrations.CreateModel( + name='WebSite', + fields=[ + ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True)), + ('url', models.URLField(max_length=256)), + ('name', models.CharField(unique=True, max_length=64)), + ('tags', models.CharField(max_length=128, null=True)), + ], + options={ + 'db_table': 'website', + }, + ), + migrations.AddField( + model_name='userwebsite', + name='website', + field=models.ForeignKey(to='third_party.WebSite'), + ), + migrations.AddField( + model_name='headline', + name='website_id', + field=models.ForeignKey(to='third_party.WebSite', db_column=b'website_id'), + ), + ] diff --git a/rest_framework_nested/third_party/migrations/__init__.py b/rest_framework_nested/third_party/migrations/__init__.py new file mode 100644 index 00000000..28cf7fe9 --- /dev/null +++ b/rest_framework_nested/third_party/migrations/__init__.py @@ -0,0 +1 @@ +__author__ = 'wangyi' diff --git a/rest_framework_nested/third_party/models.py b/rest_framework_nested/third_party/models.py new file mode 100644 index 00000000..6eb4b569 --- /dev/null +++ b/rest_framework_nested/third_party/models.py @@ -0,0 +1,49 @@ +__author__ = 'wangyi' + +from django.db import models +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + + +class WebSite(models.Model): + + id = models.AutoField(primary_key=True, auto_created=True, null=False) + url = models.URLField(max_length=256, null=False) + name = models.CharField(max_length=64, null=False, unique=True) + tags = models.CharField(max_length=128, null=True) + + class Meta: + db_table = "website" + + +class User(models.Model): + + auth = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='custom_info', + on_delete=models.CASCADE, verbose_name=_("User")) + username = models.CharField(max_length=64, null=False, unique=True) + + class Meta: + db_table = "user" + + +class UserWebSite(models.Model): + + user = models.ForeignKey(User, null=False) + website = models.ForeignKey(WebSite, null=False) + importance = models.IntegerField(null=True) + + class Meta: + db_table = "user_website" + + +class Headline(models.Model): + + url = models.URLField(max_length=256, null=False) + post_date = models.DateField(null=True) + digest = models.CharField(max_length=256, null=True) + title = models.CharField(max_length=64, null=False, unique=True) + website_id = models.ForeignKey(WebSite, db_column="website_id", null=False) + score = models.IntegerField(null=False) + + class Meta: + db_table = "headline" diff --git a/rest_framework_nested/third_party/serializer.py b/rest_framework_nested/third_party/serializer.py new file mode 100644 index 00000000..1c15cab2 --- /dev/null +++ b/rest_framework_nested/third_party/serializer.py @@ -0,0 +1,166 @@ +from __future__ import unicode_literals +__author__ = 'wangyi' + +from rest_framework import serializers +from models import User, WebSite, Headline, UserWebSite +import contextlib + +__all__ = ['WebSiteSerializer', 'UserSerializer', 'HeadlineSerializer', 'SerializerManager'] + + +class SerializerManager(object): + + def __init__(self, cls): + self.cls = cls + + def __get__(self, caller, caller_type=None): + self.owner = caller + return self + + @contextlib.contextmanager + def only(self): + try: + if hasattr(self.owner, 'cols') and self.owner.sign: + setattr(self.cls.Meta, '__sign', self.owner.sign) + setattr(self.cls.Meta, '__fields', self.owner.cols) + yield + except Exception as err: + import traceback + traceback.print_exc() + raise(err) + finally: + if hasattr(self.cls, '_fields') and\ + (hasattr(self.owner, 'cols') or hasattr(self.owner, 'sign')): + del self.cls._fields + + if hasattr(self.owner, 'cols'): + self.owner.cols = None + if hasattr(self.owner, 'sign'): + self.owner.sign = None + + def __call__(self, objects, **kwargs): + # self.cls.__sign, self.cls.__fields = + # self.owner.sign, self.owner.cols, + many = kwargs.pop('many', False) + if many: + with self.only(): + repr = self.cls(data=objects, many=True, + sign=self.owner.sign, + fields=self.owner.cols, + **kwargs) + repr.is_valid() + return repr.data + else: + return self.cls(objects, **kwargs).data + + +class DynamicFieldsModelSerializer(serializers.ModelSerializer): + + def __new__(cls, *args, **kwargs): + # We override this method in order to automagically create + # `ListSerializer` classes instead when `many=True` is set. + sign = kwargs.pop('sign', None) + fields = kwargs.pop('fields', None) + if kwargs.pop('many', False): + cls.drop_cols(sign, fields) + return cls.many_init(*args, **kwargs) + return super(DynamicFieldsModelSerializer, cls).__new__(cls, *args, **kwargs) + + @property + def data(self): + data = super(DynamicFieldsModelSerializer, self).data + + def walk_json(data, target_key): + descendant = [data] + + try: + while len(descendant) != 0: + curr = descendant.pop(0) + if curr.get(target_key) is not None: + raise StopIteration + + for key, val in curr.items(): + if isinstance(val, dict): + descendant.append(val) + except StopIteration: + return curr + else: + return None + + target = walk_json(data, 'password') + if target is not None: + target['password'] = "********" + return data + + def __init__(self, *args, **kwargs): + super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) + self._drop_cols() + + def _drop_cols(self): + if hasattr(self.Meta, '__sign') and \ + hasattr(self.Meta, '__fields'): + try: + # the following method doesn't work + sign = self.Meta.__sign + fields = self.Meta.__fields + except: + return + if fields is not None: + if sign \ + == u'+': + allowed = set(self.__fields) + existing = set(self.fields.keys()) + for field_name in existing - allowed: + self.fields.pop(field_name) + elif sign \ + == u'-': + for fields_name in self.__fields: + self.fields.pop(fields_name) + + @classmethod + def drop_cols(cls, sign=None, fields=None): + if fields is not None: + allowed = set(fields) + _cls_fields = cls().fields + if sign == u'+': + existing = set(_cls_fields.keys()) + for field_name in existing - allowed: + _cls_fields.pop(field_name) + elif sign == u'-': + for field_name in fields: + _cls_fields.pop(field_name) + cls._fields = _cls_fields + return \ + cls + + +class UserSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = User + fields = '__all__' + depth = 3 + + +class WebSiteSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = WebSite + fields = '__all__' + depth = 3 + + +class HeadlineSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = Headline + fields = '__all__' + depth = 3 + + +class UserWebsiteSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = UserWebSite + fields = '__all__' + depth = 3 diff --git a/sys.log b/sys.log new file mode 100644 index 00000000..e69de29b diff --git a/test_main.py b/test_main.py new file mode 100644 index 00000000..6ac93fb5 --- /dev/null +++ b/test_main.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +__author__ = 'wangyi' +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rest_framework_nested.runtests.settings") + sys.argv = ['test', 'test', 'rest_framework_nested.api.tests'] + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) \ No newline at end of file diff --git a/tests/nested_router.out b/tests/nested_router.out new file mode 100644 index 00000000..d50f6466 --- /dev/null +++ b/tests/nested_router.out @@ -0,0 +1,60 @@ +Creating test database for alias 'default'... +Fri, 31, Mar, 2017 08:23:30 [INFO]:tests.py, api.tests, in line 25 >> +TestDynamicQueryRouter router.urls: [ + "", + "[a-z0-9]+)/?$>", + "", + "[a-z0-9]+)/?$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)\\.(?P[a-z0-9]+)/?$>", + "", + "[a-z0-9]+)/?$>", + "headline)/(?P[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/$>", + "headline)/(?P[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)\\.(?P[a-z0-9]+)/?$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/resources/$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/resources\\.(?P[a-z0-9]+)/?$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/(?Pheadline)/(?P[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/$>", + "[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)/(?Pheadline)/(?P[a-z0-9_\\s\\w\uff1f]+|[\u4e00-\u9fa5\uff1f]+|[^\u0000-\u00ff]+)\\.(?P[a-z0-9]+)/?$>" +] +Fri, 31, Mar, 2017 08:23:30 [INFO]:tests.py, api.tests, in line 26 >> +TestDynamicQueryRouter attached methods: [ + { + "WebSite": { + "get": "get_websites" + } + }, + { + "WebSite": { + "delete": "delete_website_by_name", + "get": "get_website_by_name", + "patch": "update_partially_website_by_name", + "post": "create_website_by_name", + "put": "update_website_by_name" + } + }, + { + "WebSite": { + "get": "get_resources" + } + }, + { + "WebSite": { + "get": "get_headlines_within_website" + } + }, + { + "WebSite": { + "get": "get_resources_from_website" + } + }, + { + "WebSite": { + "delete": "delete_headline_from_website", + "get": "get_headline_from_website", + "patch": "update_partially_headline_from_website", + "post": "create_headline_from_website", + "put": "update_headline_from_website" + } + } +] +Destroying test database for alias 'default'...