├── .gitignore ├── README.rst ├── django_restify ├── __init__.py ├── restify.py ├── users.py └── views.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Cache 32 | 33 | *.pyc 34 | *~ 35 | 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear on external disk 41 | .Spotlight-V100 42 | .Trashes 43 | *_sqlite.db 44 | 45 | # Pain-Diary django 46 | db.* 47 | __pycache__ 48 | __pycache__ 49 | local.py -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-restify 2 | ============== 3 | 4 | :code:`django-restify` requires :code:`django-rest-framework`. It will create RESTFul endpoints for all models that are enabled in your project. 5 | 6 | Installation 7 | ============ 8 | 9 | .. code:: bash 10 | 11 | pip install django_restify 12 | 13 | 14 | Uses 15 | ==== 16 | 17 | 1. Install django-rest-framework and django_restify. 18 | 2. Activate :code:`django-rest-framework` and :code:`django_restify` by adding it in :code:`INSTALLED_APP` of your django settings 19 | 20 | .. code:: Python 21 | 22 | INSTALLED_APP = ( 23 | 'django.contrib.auth', 24 | (...), 25 | 'rest_framework', 26 | 'django_restify' 27 | ) 28 | 3. Configure :code:`urls.py`: 29 | 30 | Import :code:`django_restify.router:` 31 | 32 | .. code:: Python 33 | 34 | from django_restify.restify import router 35 | 36 | Configure URL: 37 | 38 | .. code:: Python 39 | 40 | urlpatterns = [ 41 | url(r'^admin/', include(admin.site.urls)), 42 | (.......), 43 | url(r'^api/v1/', include(router.urls)), 44 | ] 45 | 46 | Settings 47 | ======== 48 | 49 | .. code:: Python 50 | 51 | RESTIFY = { 52 | 'MODELS': [], 53 | 'IGNORE_LIST': [], 54 | 'USER_VIEWSET': '', 55 | 'NEW_USER_ACTIVE': True, 56 | 'SERIALIZERS': { 57 | 'model': {}, 58 | }, 59 | 'VIEWSETS': { 60 | 'entry': {}, 61 | } 62 | } 63 | 64 | 65 | MODELS 66 | ------ 67 | 68 | The list of models that you want create REST end-point. It will ingnore all other models and create end points models as listed in :code:`MODELS`. :code:`IGNORE_LIST` will get higher precedence over :code:`MODELS`. 69 | 70 | IGNORE_LIST 71 | ----------- 72 | 73 | The modules to be ignored (in list format). It can accept regular expression. For example, the default ignore list looks like: 74 | 75 | .. code:: Python 76 | 77 | ['^django*', '^api$', '^rest_framework*', '^auth*'] 78 | 79 | USER_VIEWSET 80 | ------------ 81 | 82 | To use custom viewset for user model 83 | 84 | NEW_USER_ACTIVE 85 | --------------- 86 | 87 | Set new registered user as active. 88 | 89 | SERIALIZERS 90 | ----------- 91 | 92 | To use custom serializers for a model. It should be in dictionary format e.g. :code:`{'model': 'package.serializers.ModelSerializer'}` 93 | 94 | VIEWSETS 95 | -------- 96 | 97 | To use custom viewsets for a model. It should be dictionary format e.g. :code:`{'model': 'package.viewsets.ModelViewSet'}` 98 | -------------------------------------------------------------------------------- /django_restify/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | 4 | def import_attr(package_path): 5 | """Returns the callable from string package path e.g. """ 6 | """in mymodule.views.myview, it loads myview callable""" 7 | 8 | mod_path, _, attr = package_path.rpartition('.') 9 | module = import_module(mod_path) 10 | attr = getattr(module, attr) 11 | 12 | return attr 13 | 14 | VERSION = '0.1.9' 15 | -------------------------------------------------------------------------------- /django_restify/restify.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | from importlib import import_module 4 | 5 | from django.apps import apps 6 | from django.conf import settings 7 | 8 | from rest_framework.routers import DefaultRouter 9 | 10 | from .views import Views 11 | 12 | 13 | def get_user_viewset(): 14 | restify_settings = getattr(settings, 'RESTIFY', {}) 15 | user_viewset = restify_settings.get('USER_VIEWSET', None) 16 | 17 | if user_viewset: 18 | user_module = import_module(user_viewset) 19 | else: 20 | user_module = import_module('django_restify.users') 21 | 22 | return getattr(user_module, 'UserViewSet') 23 | 24 | 25 | class Restify(object): 26 | 27 | def __init__(self): 28 | # get restify specific settings 29 | self.settings = getattr(settings, 'RESTIFY', {}) 30 | 31 | self.IGNORE_LIST = ['^django*', '^api$', '^rest_framework*', 32 | '^auth*'] + self.settings.get('IGNORE_LIST', []) 33 | self.router = None 34 | self.viewsets = {} # viewsets 35 | self.apps() 36 | 37 | def slugify(self, value): 38 | try: 39 | value = unicodedata.normalize('NFKD', value).encode( 40 | 'ascii', 'ignore').decode('ascii') 41 | except: 42 | pass 43 | value = re.sub('[^\w\s-]', '', value).strip().lower() 44 | 45 | return re.sub('[-\s]+', '-', value) 46 | 47 | def get_apps(self): 48 | return apps.app_configs 49 | 50 | def apps(self): 51 | all_apps = self.get_apps() 52 | MODELS = self.settings.get('MODELS', None) 53 | 54 | for app, app_config in all_apps.items(): 55 | # Check if user is in ignored list 56 | found = [ignore_pattern for ignore_pattern in self.IGNORE_LIST 57 | if re.findall(ignore_pattern, app_config.name)] 58 | if found: 59 | continue 60 | 61 | for model in app_config.get_models(): 62 | if MODELS and model._meta.model_name not in MODELS: 63 | continue 64 | 65 | url = self.slugify(model._meta.verbose_name_plural.title()) 66 | view = Views() 67 | viewset = view.get_viewsets(model) 68 | 69 | self.viewsets[url] = viewset 70 | 71 | def register(self): 72 | self.router = DefaultRouter() 73 | for url, viewset in self.viewsets.items(): 74 | self.router.register(url, viewset) 75 | 76 | # special case fo User model 77 | user_viewset = get_user_viewset() 78 | self.router.register('users', user_viewset) 79 | 80 | return self.router 81 | 82 | def router(self): 83 | return self.router 84 | 85 | restify = Restify() 86 | router = restify.register() 87 | -------------------------------------------------------------------------------- /django_restify/users.py: -------------------------------------------------------------------------------- 1 | 2 | from django.utils.translation import ugettext_lazy as _ 3 | from django.contrib.auth.models import User 4 | from django.conf import settings 5 | from django.db import IntegrityError 6 | 7 | from rest_framework import serializers 8 | from rest_framework import viewsets 9 | from rest_framework.response import Response 10 | from rest_framework import status 11 | from rest_framework.decorators import list_route 12 | 13 | 14 | class UserSerializers(serializers.ModelSerializer): 15 | class Meta: 16 | model = User 17 | extra_kwargs = {'password': {'write_only': True}} 18 | 19 | 20 | class UserViewSet(viewsets.ModelViewSet): 21 | queryset = User.objects.all() 22 | serializer_class = UserSerializers 23 | 24 | # apply filter on all fields 25 | filter_fields = [f for f in User._meta.get_all_field_names() 26 | if 'password' != f] 27 | 28 | def create(self, request, *args, **kwargs): 29 | error = { 30 | 'post': _('Post method is not supported.' 31 | 'Use either /users/register or /auth/register') 32 | } 33 | return Response(error, status=status.HTTP_400_BAD_REQUEST) 34 | 35 | @list_route(methods=['post']) 36 | def register(self, request): 37 | username = request.data.get('username', None) 38 | email = request.data.get('email', None) 39 | password = request.data.get('password', None) 40 | 41 | restify_settings = getattr(settings, "RESTIFY", {}) 42 | is_active = restify_settings.get('NEW_USER_ACTIVE', False) 43 | 44 | if not email or not username or not password: 45 | validation_message = _('This is requried field') 46 | content = { 47 | 'email': [validation_message], 48 | 'username': [validation_message], 49 | 'password': [validation_message] 50 | } 51 | return Response(content, status=status.HTTP_400_BAD_REQUEST) 52 | 53 | try: 54 | restify_settings = getattr(settings, "RESTIFY", {}) 55 | is_active = restify_settings.get('NEW_USER_ACTIVE', False) 56 | user = User.objects.create_user( 57 | username, 58 | email, 59 | password 60 | ) 61 | user.first_name = request.data.get('first_name', '') 62 | user.last_name = request.data.get('last_name', '') 63 | user.is_active = is_active 64 | user.save() 65 | except IntegrityError as e: 66 | message = {'error': e.message} 67 | return Response(message, status=status.HTTP_400_BAD_REQUEST) 68 | except Exception as e: 69 | message = {'error': _('Unknow error occured')} 70 | return Response(message, status=status.HTTP_400_BAD_REQUEST) 71 | 72 | user_searlized = UserSerializers(user) 73 | return Response(user_searlized.data, status=status.HTTP_201_CREATED) 74 | -------------------------------------------------------------------------------- /django_restify/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from rest_framework import serializers 4 | from rest_framework import viewsets 5 | 6 | from . import import_attr 7 | 8 | 9 | class Views(object): 10 | 11 | def __init__(self): 12 | self.restify_settings = getattr(settings, 'RESTIFY', {}) 13 | self.restify_settings_serializers = self.restify_settings.get( 14 | 'SERIALIZERS', {}) 15 | self.restify_settings_viewsets = self.restify_settings.get( 16 | 'VIEWSETS', {}) 17 | 18 | def create_serializer(self, model): 19 | """Creates serializers class dynamically for given model""" 20 | meta_class = type('Meta', (), {}) 21 | meta_class.model = model 22 | serializer_class = type( 23 | 'Serializer', (serializers.ModelSerializer, ), {}) 24 | serializer_class.Meta = meta_class 25 | 26 | return serializer_class 27 | 28 | def get_serializer(self, model): 29 | """Retunrs serializer class. Checks settings if custom serializer is defined. 30 | If defined return custom class, otherwise creates new one""" 31 | 32 | # Check if there's custom serializer class 33 | model_name = model._meta.model_name 34 | custom_serializer_class = self.restify_settings_serializers.get( 35 | model_name, None) 36 | 37 | if custom_serializer_class: 38 | serializer_class = import_attr(custom_serializer_class) 39 | else: 40 | serializer_class = self.create_serializer(model) 41 | 42 | return serializer_class 43 | 44 | def get_viewset(self, model): 45 | """Retunrs viewset class. Checks settings if custom vieset is defined. 46 | If defined return custom class, otherwise creates new one""" 47 | 48 | # Check if there's custom serializer class 49 | model_name = model._meta.model_name 50 | custom_viewset_class = self.restify_settings_viewsets.get( 51 | model_name, None) 52 | 53 | if custom_viewset_class: 54 | viewset_class = import_attr(custom_viewset_class) 55 | else: 56 | viewset_class = None 57 | 58 | return viewset_class 59 | 60 | def create_viewset(self, model, serializer_class): 61 | """Creates viewsets dynamically for model""" 62 | viewset_class = type('Viewset', (viewsets.ModelViewSet, ), {}) 63 | viewset_class.queryset = model.objects.all() 64 | 65 | viewset_class.serializer_class = serializer_class 66 | viewset_class.filter_fields = model._meta.get_all_field_names() 67 | 68 | return viewset_class 69 | 70 | def get_viewsets(self, model): 71 | """Creates and returns viewsets with serializer""" 72 | viewsets_class = self.get_viewset(model) 73 | 74 | if not viewsets_class: 75 | serializer_class = self.get_serializer(model) 76 | viewsets_class = self.create_viewset(model, serializer_class) 77 | 78 | return viewsets_class 79 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/en/latest/distributing.html 5 | https://github.com/pypa/sampleproject 6 | """ 7 | 8 | # Always prefer setuptools over distutils 9 | from setuptools import setup, find_packages 10 | # To use a consistent encoding 11 | from codecs import open 12 | from os import path 13 | 14 | here = path.abspath(path.dirname(__file__)) 15 | 16 | # Get the long description from the relevant file 17 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 18 | long_description = f.read() 19 | 20 | import django_restify 21 | setup( 22 | name='django_restify', 23 | 24 | # Versions should comply with PEP440. For a discussion on single-sourcing 25 | # the version across setup.py and the project code, see 26 | # https://packaging.python.org/en/latest/single_source_version.html 27 | version=django_restify.VERSION, 28 | 29 | description='Turn your Django project into RESTFul APIs in a minute.', 30 | long_description='', 31 | 32 | # The project's main homepage. 33 | url='https://github.com/s2krish/django-restify', 34 | 35 | # Author details 36 | author='Krish', 37 | author_email='krish@jaljale.com', 38 | 39 | # Choose your license 40 | # license='MIT', 41 | 42 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 43 | classifiers=[ 44 | # How mature is this project? Common values are 45 | # 3 - Alpha 46 | # 4 - Beta 47 | # 5 - Production/Stable 48 | 'Development Status :: 5 - Production/Stable', 49 | 'Environment :: Web Environment', 50 | 'Framework :: Django', 51 | 'Framework :: Django :: 1.8', 52 | 'Framework :: Django :: 1.9', 53 | 'Intended Audience :: Developers', 54 | 'License :: OSI Approved :: BSD License', 55 | 'Natural Language :: English', 56 | 'Operating System :: OS Independent', 57 | 'Programming Language :: Python', 58 | 'Programming Language :: Python :: 3', 59 | 'Topic :: Internet :: WWW/HTTP' 60 | ], 61 | 62 | 63 | 64 | 65 | # What does your project relate to? 66 | keywords=['django', 'restfulapi', 'rest apis'], 67 | 68 | # You can just specify the packages manually here if your project is 69 | # simple. Or you can use find_packages(). 70 | packages=['django_restify'], 71 | 72 | # List run-time dependencies here. These will be installed by pip when 73 | # your project is installed. For an analysis of "install_requires" vs pip's 74 | # requirements files see: 75 | # https://packaging.python.org/en/latest/requirements.html 76 | install_requires=['djangorestframework'], 77 | 78 | # List additional groups of dependencies here (e.g. development 79 | # dependencies). You can install these using the following syntax, 80 | # for example: 81 | # $ pip install -e .[dev,test] 82 | # extras_require={ 83 | # 'dev': ['check-manifest'], 84 | # 'test': ['coverage'], 85 | # }, 86 | 87 | # If there are data files included in your packages that need to be 88 | # installed, specify them here. If using Python 2.6 or less, then these 89 | # have to be included in MANIFEST.in as well. 90 | # package_data={ 91 | # 'sample': ['package_data.dat'], 92 | # }, 93 | 94 | # Although 'package_data' is the preferred approach, in some case you may 95 | # need to place data files outside of your packages. See: 96 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 97 | # In this case, 'data_file' will be installed into '/my_data' 98 | # data_files=[('my_data', ['data/data_file'])], 99 | 100 | # To provide executable scripts, use entry points in preference to the 101 | # "scripts" keyword. Entry points provide cross-platform support and allow 102 | # pip to create the appropriate form of executable for the target platform. 103 | # entry_points={ 104 | # 'console_scripts': [ 105 | # 'sample=sample:main', 106 | # ], 107 | # }, 108 | ) --------------------------------------------------------------------------------