├── example
├── contents
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── views.py
│ ├── models.py
│ └── admin.py
├── example
│ ├── __init__.py
│ ├── urls.py
│ ├── wsgi.py
│ └── settings.py
├── static
│ └── css
│ │ └── style.css
└── manage.py
├── admino
├── fields.py
├── forms.py
├── templatetags
│ ├── __init__.py
│ └── admino_tags.py
├── __init__.py
├── constants.py
├── templates
│ └── admin
│ │ ├── base_site.html
│ │ └── base.html
├── utils.py
├── views.py
├── serializers.py
└── sites.py
├── MANIFEST.in
├── setup.cfg
├── .gitignore
├── LICENSE
├── setup.py
└── README.md
/example/contents/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/static/css/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admino/fields.py:
--------------------------------------------------------------------------------
1 | from django import forms
--------------------------------------------------------------------------------
/example/contents/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admino/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 |
--------------------------------------------------------------------------------
/admino/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'erdemozkol'
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include README.rst
3 | include LICENSE
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | license-file = LICENSE
3 | description-file = README.md
4 |
--------------------------------------------------------------------------------
/example/contents/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/example/contents/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/admino/__init__.py:
--------------------------------------------------------------------------------
1 | from sites import site
2 |
3 | VERSION = (0, 0, 1)
4 | __version__ = '.'.join(map(str, VERSION))
5 |
6 |
--------------------------------------------------------------------------------
/admino/constants.py:
--------------------------------------------------------------------------------
1 | HTTP_METHOD_VIEWS = {
2 | "post": "create",
3 | "put": "update",
4 | "delete": "delete",
5 | }
6 |
--------------------------------------------------------------------------------
/admino/templatetags/admino_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.filter(takes_context=True)
8 | def media_clean(media, **kwargs):
9 | return media
--------------------------------------------------------------------------------
/admino/templates/admin/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 |
3 | {% block title %}Admino{% endblock %}
4 |
5 | {% block branding %}
6 |
7 | {% endblock %}
8 |
9 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/admino/templates/admin/base.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% block extrastyle %}
4 |
9 | {% endblock %}
10 |
11 | {% block extrahead %}
12 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/example/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.conf.urls import patterns, include, url
3 | from django.conf.urls.static import static
4 |
5 | from django.contrib import admin as django_admin
6 | import admino
7 |
8 | admino.site.activated()
9 |
10 | urlpatterns = [
11 | url(r'^admin/', admino.site.urls),
12 | url(r'^django-admin/', django_admin.site.urls),
13 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
14 |
--------------------------------------------------------------------------------
/example/example/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for example project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/admino/utils.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | from admino.serializers import FormSerializer
4 | from django.forms import BaseForm
5 | from django.utils.functional import Promise
6 | from django.utils.encoding import force_unicode
7 |
8 |
9 | def import_from_string(module_path):
10 | """
11 | Attempt to import a class from a string representation.
12 | """
13 | try:
14 | parts = module_path.split('.')
15 | module_path, class_name = '.'.join(parts[:-1]), parts[-1]
16 | module = importlib.import_module(module_path)
17 | return getattr(module, class_name)
18 | except (ImportError, AttributeError) as e:
19 | msg = 'Could not import "%s" for Admino setting' % module_path
20 | raise ImportError(msg)
21 |
--------------------------------------------------------------------------------
/example/contents/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.encoding import smart_unicode
3 |
4 |
5 | class BookType(models.Model):
6 | name = models.CharField(max_length=255)
7 | creation_date = models.DateTimeField(auto_now_add=True)
8 |
9 | def __unicode__(self):
10 | return smart_unicode(self.name)
11 |
12 |
13 | class Author(models.Model):
14 | name = models.CharField(max_length=255)
15 | image = models.ImageField(blank=True, null=True, upload_to="author/")
16 | creation_date = models.DateTimeField(auto_now_add=True)
17 |
18 | def __unicode__(self):
19 | return smart_unicode(self.name)
20 |
21 |
22 | class Book(models.Model):
23 | name = models.CharField(max_length=255)
24 | author = models.ForeignKey(Author)
25 | book_type = models.ManyToManyField(BookType)
26 |
27 | def __unicode__(self):
28 | return smart_unicode(self.name)
--------------------------------------------------------------------------------
/example/contents/admin.py:
--------------------------------------------------------------------------------
1 | from admino.sites import ModelAdmino
2 | from django.contrib import admin
3 | from models import Author, BookType, Book
4 |
5 |
6 | class AuthorAdmin(admin.ModelAdmin):
7 | list_display = ("name", "creation_date")
8 |
9 |
10 | class BookTypeAdmin(admin.ModelAdmin):
11 | list_display = ("name", "creation_date")
12 |
13 |
14 | class BookAdmin(admin.ModelAdmin):
15 | admin_type = "admino"
16 | list_display = ("name", "author", "title")
17 | list_display_links = ("name", "author")
18 | list_filter = ("author", "name")
19 |
20 | def title(self, obj):
21 | return "mr %s" % obj.name
22 |
23 |
24 | class TestAdminoClass(ModelAdmino):
25 | def api_get(self, request, *args, **kwargs):
26 | print "hello"
27 | return super(TestAdminoClass, self).api_get(request, *args, **kwargs)
28 |
29 |
30 | admin.site.register(Author, AuthorAdmin)
31 | admin.site.register(BookType, BookTypeAdmin)
32 | admin.site.register(Book, BookAdmin)
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | screendumps/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 | *.DS_Store
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | .idea/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | db.sqlite3
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 | .hypothesis/
50 | tmp.py
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 |
59 | # Sphinx documentation
60 | docs/_build/
61 |
62 | # PyBuilder
63 | target/
64 | notes/
65 | #Ipython Notebook
66 | .ipynb_checkpoints
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Erdem Ozkol
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 | VERSION = (0, 0, 1)
5 | __version__ = '.'.join(map(str, VERSION))
6 |
7 | readme_rst = os.path.join(os.path.dirname(__file__), 'README.rst')
8 |
9 | if os.path.exists(readme_rst):
10 | long_description = open(readme_rst).read()
11 | else:
12 | long_description = "Admino is a django package that provides a REST API for admin endpoints. It allows you to customize django admin panel."
13 |
14 | # allow setup.py to be run from any path
15 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
16 |
17 | setup(
18 | name='django-admino',
19 | version=__version__,
20 | description="Admino is a django package that provides a REST API for admin endpoints. It allows you to customize django admin panel.",
21 | long_description=long_description,
22 | author="Erdem Ozkol",
23 | author_email="info@admino.io",
24 | url="https://github.com/erdem/django-admino",
25 | license="MIT",
26 | platforms=["any"],
27 | packages=find_packages(exclude=("example", "env", "notes")),
28 | include_package_data=True,
29 | classifiers=[
30 | "Environment :: Web Environment",
31 | "Intended Audience :: Developers",
32 | "License :: OSI Approved :: MIT License",
33 | "Operating System :: OS Independent",
34 | "Framework :: Django",
35 | "Programming Language :: Python",
36 | ],
37 | )
38 |
39 |
--------------------------------------------------------------------------------
/example/contents/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.4 on 2016-03-25 14:56
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Author',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('name', models.CharField(max_length=255)),
22 | ('image', models.ImageField(blank=True, null=True, upload_to=b'author/')),
23 | ('creation_date', models.DateTimeField(auto_now_add=True)),
24 | ],
25 | ),
26 | migrations.CreateModel(
27 | name='Book',
28 | fields=[
29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30 | ('name', models.CharField(max_length=255)),
31 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contents.Author')),
32 | ],
33 | ),
34 | migrations.CreateModel(
35 | name='BookType',
36 | fields=[
37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38 | ('name', models.CharField(max_length=255)),
39 | ('creation_date', models.DateTimeField(auto_now_add=True)),
40 | ],
41 | ),
42 | migrations.AddField(
43 | model_name='book',
44 | name='book_type',
45 | field=models.ManyToManyField(to='contents.BookType'),
46 | ),
47 | ]
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Django Admino (Alpha)
2 |
3 | Admino is a django package that provides a REST API for admin endpoints. It allows you to customize django admin panel.
4 |
5 | http://admino.io
6 |
7 |
8 | ### Problem?
9 |
10 | Django admin is good solution for development tests and i/o, but django admin needs to be more customizable and extendable.
11 |
12 | ### Solution:
13 |
14 | if you want to implement a custom widget or ui for django admin, you may need a REST API to handle records.
15 |
16 | Admino added to new api views to django admin genericly. You don't need to change default admin files.
17 | Every API endpoint will generate your "ModelAdmin" configurations.
18 |
19 | ##### EXAMPLE:
20 |
21 | **Visible Books list page url:** /admin/books/book/?is_visible__exact=1
22 |
23 | 
24 |
25 | **Visible Books api url:** /admin/**api**/books/book/?is_visible__exact=1
26 |
27 | 
28 |
29 |
30 | **Book detail page url:** /admin/books/book/1/
31 |
32 | 
33 |
34 |
35 | **Book detail api url:** /admin/**api**/books/book/1/
36 |
37 | 
38 |
39 | #### Install
40 |
41 | pip install django-admino
42 |
43 | settings.py
44 |
45 | INSTALLED_APPS = [
46 | 'admino',
47 |
48 | 'django.contrib.admin',
49 | 'django.contrib.auth',
50 | 'django.contrib.contenttypes',
51 | 'django.contrib.sessions',
52 | 'django.contrib.messages',
53 | 'django.contrib.staticfiles',
54 |
55 | 'books',
56 | ]
57 |
58 | urls.py
59 |
60 | from django.contrib import admin
61 | import admino
62 |
63 | admin.site = admino.site.activated(admin.site)
64 |
65 | urlpatterns = [
66 | url(r'^admin/', admin.site.urls),
67 | ]
68 |
69 |
70 | Add custom admin Mixin class:
71 |
72 | settings.py
73 |
74 | ADMINO_MIXIN_CLASS = "app.module.AdminMixinClass"
75 |
76 |
--------------------------------------------------------------------------------
/admino/views.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from urllib import urlencode
3 |
4 | from admino.serializers import ModelAdminSerializer
5 | from django.core.urlresolvers import reverse_lazy
6 | from django.http import JsonResponse
7 | from django.views.generic import View
8 |
9 |
10 | class APIView(View):
11 |
12 | def json_response(self, data, *args, **kwargs):
13 | return JsonResponse(data, safe=False, *args, **kwargs)
14 |
15 |
16 | class ChangeListRetrieveAPIView(APIView):
17 |
18 | def get_api_next_url(self, request, cl):
19 | page_num = cl.page_num
20 | if page_num and page_num is not int or not cl.multi_page:
21 | return None
22 | info = self.model._meta.app_label, self.model._meta.model_name
23 | url = reverse_lazy("admin:%s_%s_api_list" % info)
24 | host = request.get_host()
25 | params = cl.params
26 | params["p"] = page_num + 1
27 | return "%s://%s%s?%s" % (request.scheme, host, url, urlencode(params))
28 |
29 | def get_api_previous_url(self, request, cl):
30 | page_num = cl.page_num
31 | if page_num == 0 or not cl.multi_page:
32 | return None
33 |
34 | info = self.model._meta.app_label, self.model._meta.model_name
35 | url = reverse_lazy("admin:%s_%s_api_list" % info)
36 | host = request.get_host()
37 | params = cl.params
38 | params["p"] = page_num - 1
39 | return "%s://%s%s?%s" % (request.scheme, host, url, urlencode(params))
40 |
41 | def get(self, request, model_admin, admin_cl, *args, **kwargs):
42 | self.model = admin_cl.model
43 | results = []
44 | for obj in admin_cl.result_list:
45 | results.append(model_admin.obj_as_dict(request, obj))
46 |
47 | data = OrderedDict()
48 | data["count"] = admin_cl.result_count
49 | data["next"] = self.get_api_next_url(request, admin_cl)
50 | data["previous"] = self.get_api_previous_url(request, admin_cl)
51 | data["results"] = results
52 | return self.json_response(data)
53 |
54 |
55 | class APIMetaView(APIView):
56 |
57 | def get(self, request, model_admin, *args, **kwargs):
58 | form = model_admin.get_form(request)
59 | data = ModelAdminSerializer(model_admin=model_admin, admin_form=form).data
60 | return self.json_response(data)
61 |
62 |
63 | class AdminDetailRetrieveAPIView(APIView):
64 |
65 | def get(self, request, model_admin, admin_cl, *args, **kwargs):
66 | return self.json_response("ok")
67 |
--------------------------------------------------------------------------------
/admino/serializers.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from django.forms.forms import DeclarativeFieldsMetaclass
3 |
4 | from django import forms
5 | from django.utils.encoding import force_unicode
6 | from django.utils.functional import Promise
7 |
8 |
9 | def obj_as_dict(o):
10 |
11 | if isinstance(o, DeclarativeFieldsMetaclass):
12 | o = FormSerializer(form=o).data
13 |
14 | if isinstance(o, forms.Field):
15 | o = FormFieldSerializer(field=o).data
16 |
17 | if isinstance(o, forms.Widget):
18 | o = FormWidgetSerializer(widget=o).data
19 |
20 | if isinstance(o, (list, tuple)):
21 | o = [obj_as_dict(x) for x in o]
22 |
23 | if isinstance(o, Promise):
24 | try:
25 | o = force_unicode(o)
26 | except:
27 | # Item could be a lazy tuple or list
28 | try:
29 | o = [obj_as_dict(x) for x in o]
30 | except:
31 | raise Exception('Unable to resolve lazy object %s' % o)
32 | if callable(o):
33 | o = o()
34 |
35 | if isinstance(o, dict):
36 | for k, v in o.items():
37 | o[k] = obj_as_dict(v)
38 |
39 | return o
40 |
41 |
42 | class BaseSerializer(object):
43 | @property
44 | def data(self):
45 | raise NotImplementedError
46 |
47 |
48 | class FormWidgetSerializer(BaseSerializer):
49 | def __init__(self, widget, *args, **kwargs):
50 | self.widget = widget
51 |
52 | @property
53 | def data(self):
54 | widget_data = OrderedDict()
55 | widget_data["type"] = self.widget.__class__.__name__
56 | return widget_data
57 |
58 |
59 | class FormFieldSerializer(BaseSerializer):
60 | def __init__(self, field, *args, **kwargs):
61 | self.field = field
62 |
63 | @property
64 | def data(self):
65 | field_data = OrderedDict()
66 | field_data["type"] = self.field.__class__.__name__
67 | field_data["widget"] = self.field.widget
68 | field_data["is_required"] = self.field.required
69 | return field_data
70 |
71 |
72 | class FormSerializer(BaseSerializer):
73 | def __init__(self, form, *args, **kwargs):
74 | self.form = form
75 |
76 | @property
77 | def data(self):
78 | form_fields = []
79 | for name, field in self.form.base_fields.items():
80 | d = dict()
81 | d[name] = field
82 | form_fields.append(d)
83 | return form_fields
84 |
85 |
86 | #todo check "formfield_overrides"
87 | MODEL_ADMIN_CLASS_ATTRIBUTES = (
88 | "raw_id_fields", "fields", "exclude", "fieldsets", "filter_vertical", "filter_horizontal", "radio_fields",
89 | "prepopulated_fields", "readonly_fields", "ordering", "view_on_site",
90 | "show_full_result_count", "list_display", "list_display_links", "list_filter", "list_per_page", "list_max_show_all",
91 | "list_editable", "search_fields", "date_hierarchy","save_as", "save_on_top")
92 |
93 |
94 | class ModelAdminSerializer(BaseSerializer):
95 | def __init__(self, model_admin, admin_form, *args, **kwargs):
96 | self.model_admin = model_admin
97 | self.admin_form = admin_form
98 |
99 | @property
100 | def data(self):
101 | data = OrderedDict()
102 | for attr in MODEL_ADMIN_CLASS_ATTRIBUTES:
103 | data[attr] = getattr(self.model_admin, attr, None)
104 | data["form"] = self.admin_form
105 | return obj_as_dict(data)
106 |
107 | def serialize_form(self):
108 | return FormSerializer(self.admin_form).data
109 |
--------------------------------------------------------------------------------
/example/example/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for example project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.9.4.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.9/ref/settings/
11 | """
12 |
13 | import os
14 | import sys
15 |
16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18 |
19 | SITE_PATH = os.path.abspath(os.path.dirname(__file__))
20 |
21 | PROJECT_PATH = os.path.normpath(os.path.join(SITE_PATH, '..', '..'))
22 | if PROJECT_PATH not in sys.path:
23 | sys.path.insert(0, PROJECT_PATH)
24 |
25 |
26 | # Quick-start development settings - unsuitable for production
27 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
28 |
29 | # SECURITY WARNING: keep the secret key used in production secret!
30 | SECRET_KEY = '^btn*&n^@6l87p)oe@s*rs57%@!amu845_90453r)2dsrq)g'
31 |
32 | # SECURITY WARNING: don't run with debug turned on in production!
33 | DEBUG = True
34 |
35 | ALLOWED_HOSTS = []
36 |
37 |
38 | # Application definition
39 |
40 | INSTALLED_APPS = [
41 | 'admino',
42 |
43 | 'django.contrib.admin',
44 | 'django.contrib.auth',
45 | 'django.contrib.contenttypes',
46 | 'django.contrib.sessions',
47 | 'django.contrib.messages',
48 | 'django.contrib.staticfiles',
49 |
50 | 'contents',
51 | ]
52 |
53 | MIDDLEWARE_CLASSES = [
54 | 'django.middleware.security.SecurityMiddleware',
55 | 'django.contrib.sessions.middleware.SessionMiddleware',
56 | 'django.middleware.common.CommonMiddleware',
57 | 'django.middleware.csrf.CsrfViewMiddleware',
58 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
59 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
60 | 'django.contrib.messages.middleware.MessageMiddleware',
61 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
62 | ]
63 |
64 | ROOT_URLCONF = 'example.urls'
65 |
66 | TEMPLATES = [
67 | {
68 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
69 | 'DIRS': [],
70 | 'APP_DIRS': True,
71 | 'OPTIONS': {
72 | 'context_processors': [
73 | 'django.template.context_processors.debug',
74 | 'django.template.context_processors.request',
75 | 'django.contrib.auth.context_processors.auth',
76 | 'django.contrib.messages.context_processors.messages',
77 | ],
78 | },
79 | },
80 | ]
81 |
82 | WSGI_APPLICATION = 'example.wsgi.application'
83 |
84 |
85 | # Database
86 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases
87 |
88 | DATABASES = {
89 | 'default': {
90 | 'ENGINE': 'django.db.backends.sqlite3',
91 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
92 | }
93 | }
94 |
95 |
96 | # Password validation
97 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
98 |
99 | AUTH_PASSWORD_VALIDATORS = [
100 | {
101 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
102 | },
103 | {
104 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
105 | },
106 | {
107 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
108 | },
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
111 | },
112 | ]
113 |
114 |
115 | # Internationalization
116 | # https://docs.djangoproject.com/en/1.9/topics/i18n/
117 |
118 | LANGUAGE_CODE = 'en-us'
119 |
120 | TIME_ZONE = 'UTC'
121 |
122 | USE_I18N = True
123 |
124 | USE_L10N = True
125 |
126 | USE_TZ = True
127 |
128 | # ADMINO_MIXIN_CLASS = "contents.admin.TestAdminoClass"
129 |
130 |
131 | # Static files (CSS, JavaScript, Images)
132 | # https://docs.djangoproject.com/en/1.9/howto/static-files/
133 |
134 | STATIC_ROOT = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'media/')
135 | STATIC_URL = '/static/'
136 | MEDIA_ROOT = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'media/uploads/')
137 | MEDIA_URL = '/uploads/'
138 |
--------------------------------------------------------------------------------
/admino/sites.py:
--------------------------------------------------------------------------------
1 | import json
2 | from functools import update_wrapper
3 |
4 | from admino.utils import import_from_string
5 | from admino.views import ChangeListRetrieveAPIView, APIMetaView
6 |
7 | from django import http
8 | from django.conf import settings
9 | from django.conf.urls import url, include
10 | from django.core.serializers.json import DjangoJSONEncoder
11 | from django.core import serializers
12 | from django.core.urlresolvers import reverse_lazy
13 | from django.http import HttpResponse
14 | from django.views.decorators.csrf import csrf_exempt
15 |
16 | from django.contrib.admin import actions
17 | from django.contrib.admin import site as django_site
18 | from django.contrib.admin import AdminSite as DjangoAdminSite, ModelAdmin as DjangoModelAdmin, autodiscover as django_admin_autodiscover
19 | from django.contrib.admin.options import IncorrectLookupParameters
20 |
21 | from .constants import HTTP_METHOD_VIEWS
22 |
23 |
24 | class AdminoMixin(DjangoModelAdmin):
25 | http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
26 |
27 | def get_api_urls(self):
28 |
29 | def wrap(view):
30 | def wrapper(*args, **kwargs):
31 | return self.admin_site.admin_view(view)(*args, **kwargs)
32 |
33 | wrapper.model_admin = self
34 | return update_wrapper(wrapper, view)
35 |
36 | info = self.model._meta.app_label, self.model._meta.model_name
37 |
38 | urlpatterns = [
39 | url(r'^$',
40 | wrap(self.admin_site.admin_view(self.dispatch)),
41 | name='%s_%s_api_list' % info),
42 | url(r'^(?P[-\d]+)/$',
43 | wrap(self.admin_site.admin_view(self.dispatch)),
44 | name='%s_%s_api_detail' % info),
45 | url(r'^meta/$',
46 | wrap(self.admin_site.admin_view(self.api_meta_view)),
47 | name='%s_%s_api_meta' % info),
48 | ]
49 | return urlpatterns
50 |
51 | def api_urls(self):
52 | return self.get_api_urls()
53 |
54 | api_urls = property(api_urls)
55 |
56 | def _allowed_methods(self):
57 | return [m.upper() for m in self.http_method_names if hasattr(self, "api_" + m)]
58 |
59 | def http_method_not_allowed(self, request, *args, **kwargs):
60 | if settings.DEBUG and self._allowed_methods():
61 | raise Exception("Only" + str(self._allowed_methods()))
62 | return http.HttpResponseNotAllowed(self._allowed_methods())
63 |
64 | @csrf_exempt
65 | def dispatch(self, request, *args, **kwargs):
66 | if not request.method.lower() in self.http_method_names:
67 | return self.http_method_not_allowed(request, *args, **kwargs)
68 |
69 | handler = self.http_method_not_allowed
70 | if request.method.lower() == "get":
71 | handler = getattr(self, "api_get", self.http_method_not_allowed)
72 |
73 | api_view_name = HTTP_METHOD_VIEWS.get(request.method.lower())
74 | if api_view_name:
75 | handler = getattr(self, "api_" + api_view_name, self.http_method_not_allowed)
76 |
77 | return handler(request, *args, **kwargs)
78 |
79 | def api_get(self, request, *args, **kwargs):
80 | if kwargs.get("pk"):
81 | return self.api_detail(request, *args, **kwargs)
82 | else:
83 | return self.api_list(request, *args, **kwargs)
84 |
85 | def get_admin_cl(self, request):
86 | ChangeList = self.get_changelist(request)
87 | try:
88 | cl = ChangeList(request, self.model, self.list_display,
89 | self.list_display_links, self.list_filter, self.date_hierarchy,
90 | self.search_fields, self.list_select_related, self.list_per_page,
91 | self.list_max_show_all, self.list_editable, self)
92 | return cl
93 |
94 | except IncorrectLookupParameters:
95 | raise Exception("IncorrectLookupParameters")
96 |
97 | def get_model_admin_field_names(self, request, obj):
98 | """
99 | This method return admin class readonly custom fields.
100 | Getting ModelAdmin list_display + readonly_fields
101 | """
102 | return set(list(self.get_readonly_fields(request, obj)) + list(self.get_list_display(request)))
103 |
104 | def serialize_objs(self, objs):
105 | data_objs = json.loads(serializers.serialize('json', objs))
106 | for data in data_objs:
107 | data.update(data["fields"])
108 | del data["fields"]
109 | return data_objs
110 |
111 | def serialize_obj(self, obj):
112 | return self.serialize_objs([obj])[0]
113 |
114 | def obj_as_dict(self, request, obj):
115 | data = self.serialize_obj(obj)
116 |
117 | # serialize model instance fields datas
118 | for field in obj._meta.get_fields():
119 | if field.is_relation and field.concrete:
120 | field_value = getattr(obj, field.name)
121 | if field_value:
122 | if field.many_to_many:
123 | data[field.name] = self.serialize_objs(field_value.all())
124 | elif field.many_to_one or field.one_to_one or field.one_to_many:
125 | data[field.name] = self.serialize_obj(field_value)
126 |
127 | # add custom admin class field to serialized bundle
128 | model_admin_fields = self.get_model_admin_field_names(request, obj)
129 | for field in model_admin_fields:
130 | if field in data:
131 | continue
132 |
133 | if hasattr(obj, field):
134 | f = getattr(obj, field)
135 | data[field] = unicode(f)
136 |
137 | if hasattr(self, field):
138 | field_method = getattr(self, field)
139 | if callable(field_method):
140 | data[field] = field_method(obj)
141 | else:
142 | data[field] = field_method
143 |
144 | info = self.model._meta.app_label, self.model._meta.model_name
145 | admin_detail_url = str(reverse_lazy("admin:%s_%s_change" % info, args=(obj.id,)))
146 | data["admin_detail_url"] = admin_detail_url
147 | return data
148 |
149 | def get_api_list_view_class(self):
150 | return ChangeListRetrieveAPIView
151 |
152 | def api_meta_view(self, request, *args, **kwargs):
153 | return APIMetaView().get(request, model_admin=self)
154 |
155 | def api_list(self, request, *args, **kwargs):
156 | cl = self.get_admin_cl(request)
157 | view_class = self.get_api_list_view_class()
158 | return view_class().get(request, model_admin=self, admin_cl=cl)
159 |
160 | def api_detail(self, request, *args, **kwargs):
161 | obj = self.get_object(request, object_id=kwargs.get("pk"))
162 | ModelForm = self.get_form(request, obj=obj)
163 | form = ModelForm(instance=obj)
164 | data = self.obj_as_dict(request, form.instance)
165 | return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), content_type='application/json')
166 |
167 | def api_create(self, request, *args, **kwargs):
168 | data = json.loads(request.body)
169 |
170 | ModelForm = self.get_form(request, obj=None)
171 | form = ModelForm(data=data, files=request.FILES)
172 | if form.is_valid():
173 | obj = form.save()
174 | data = self.obj_as_dict(request, obj)
175 | return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), content_type="application/json")
176 | else:
177 | errors = {
178 | "errors": json.loads(form.errors.as_json())
179 | }
180 | return HttpResponse(json.dumps(errors), status=400, content_type="application/json")
181 |
182 | def api_update(self, request, *args, **kwargs):
183 | return HttpResponse("put")
184 |
185 | def api_delete(self, request, *args, **kwargs):
186 | return HttpResponse("delete")
187 |
188 |
189 | class ModelAdmino(AdminoMixin, DjangoModelAdmin):
190 | admin_type = "admino"
191 |
192 |
193 | class AdminoSite(DjangoAdminSite):
194 |
195 | def __init__(self, django_site, name='admino'):
196 | self.django_site = django_site
197 | self._registry = {}
198 | self.name = name
199 | self._actions = {'delete_selected': actions.delete_selected}
200 | self._global_actions = self._actions.copy()
201 |
202 | def activated(self):
203 | django_admin_registered_apps = self.django_site._registry
204 | for model, admin_obj in django_admin_registered_apps.items():
205 | mixin_class = AdminoMixin
206 | if hasattr(settings, "ADMINO_MIXIN_CLASS"):
207 | module_path = getattr(settings, "ADMINO_MIXIN_CLASS")
208 | mixin_class = import_from_string(module_path)
209 | django_admin_class = admin_obj.__class__
210 | admino_class = type("ModelAdmino", (mixin_class, django_admin_class), {"admin_type": "admino"})
211 | admino_obj = admino_class(model, self)
212 | self._registry[model] = admino_obj
213 |
214 | django_admin_autodiscover()
215 | return self
216 |
217 | def get_urls(self):
218 | urlpatterns = super(AdminoSite, self).get_urls()
219 | valid_app_labels = []
220 | for model, model_admin in self._registry.items():
221 | api_urlpatterns = [
222 | url(r'^api/%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.api_urls)),
223 | ]
224 | urlpatterns = urlpatterns + api_urlpatterns
225 | if model._meta.app_label not in valid_app_labels:
226 | valid_app_labels.append(model._meta.app_label)
227 | return urlpatterns
228 |
229 | site = AdminoSite(django_site=django_site)
230 |
231 |
232 |
--------------------------------------------------------------------------------