├── .gitignore ├── .travis.yml ├── README.md ├── example ├── __init__.py ├── api │ ├── config │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── requirements.txt │ └── todolist │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py ├── mailchecker │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── mailer.py │ ├── manager.py │ ├── models.py │ └── test.py └── with_graphql │ ├── manage.py │ ├── todo │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py │ └── with_graphql │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── mservice_model ├── __init__.py ├── admin │ ├── __init__.py │ └── main.py ├── api.py ├── fields.py ├── forms.py ├── manager.py ├── models.py ├── options.py ├── queryset.py └── tests │ ├── base.py │ ├── response.py │ ├── test_admin.py │ ├── test_api.py │ ├── test_manager.py │ ├── test_models.py │ ├── test_queryset.py │ └── test_views.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | db.sqlite3 3 | gmail.storage 4 | .DS_Store 5 | client_secret.json 6 | .vscode/ 7 | tags 8 | source.sh 9 | .cache 10 | __pycache__ 11 | venv/ 12 | *.egg-info/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.5-dev" # 3.5 development branch 6 | - "3.6" 7 | - "3.6-dev" # 3.6 development branch 8 | - "3.7-dev" # 3.7 development branch 9 | # command to install dependencies 10 | install: 11 | - pip install -r requirements.txt 12 | 13 | # command to run tests 14 | script: 15 | - python example/with_graphql/manage.py test mservice_model.tests # or py.test for Python versions 3.5 and below -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### django-rest-admin 2 | 3 | This is a django app that attempts to get the django orm to hook up to restful services 4 | instead of the database. So instead of querying data from a database, you would be querying 5 | data from a rest api. 6 | 7 | 8 | In your `models.py` instead of inheriting from `django.db.models.Model`, you would 9 | inherit from `mservice_model.models.ServiceModel` 10 | 11 | A sample looks like This 12 | 13 | ``` 14 | from mservice_model.models import ServiceModel 15 | from .api import instance as request_api 16 | 17 | class BaseRequestTutor(ServiceModel): 18 | _service_api = request_api 19 | 20 | class Meta: 21 | ordering = ('id',) 22 | verbose_name = "Client Request Detail" 23 | 24 | def __init__(self, **kwargs): 25 | for key, value in kwargs.items(): 26 | setattr(self, key, value) 27 | 28 | def __repr__(self): 29 | return "".format(self.email) 30 | 31 | ``` 32 | 33 | where `request_api` is as follows 34 | 35 | ``` 36 | import json 37 | from mservice_model.api import Bunch, FetchHelpers, ServiceApi 38 | 39 | 40 | class FetchAPI(FetchHelpers): 41 | 42 | def get_object_by_id(self, request_id, cls=Bunch): 43 | data = self._fetch_data('GET', self._path( 44 | '/requests/{}'.format(request_id))) 45 | return [self._make_base_request(data, cls)] 46 | 47 | def get_values_list(self, *args, **kwargs): 48 | field_names = ",".join(args) 49 | params = {**kwargs, **{'field': field_names}} 50 | return self._fetch_data('GET', self._path('/requests/values_list'), params=params) 51 | 52 | def get_fields(self): 53 | return self._fetch_data('OPTIONS', self._path('/requests/'))['actions']['POST'] 54 | 55 | def get_all_objects(self, filter_by, order_by, cls): 56 | params = {**filter_by, **{'field_name': 'modified'}} 57 | if order_by: 58 | params.update(ordering=','.join(order_by)) 59 | print(order_by) 60 | data = self._fetch_data('GET', self._path('/requests/'), params=params) 61 | as_objects = [self._make_base_request(o, cls) for o in data['results']] 62 | total = data['count'] 63 | date_range = data.get('date_range') 64 | last_id = data.get('last_id') 65 | return as_objects, total, date_range, last_id 66 | 67 | def get_datetimes(self, field_name, kind, order, filter_query, **kwargs): 68 | params = dict(field_name=field_name, kind=kind, 69 | order=order, filter_query=json.dumps(filter_query)) 70 | return self._fetch_data('GET', self._path('/requests/datetimes'), params=params) 71 | 72 | def get_date_range(self, field_name): 73 | params = dict(field_name=field_name) 74 | return self._fetch_data('GET', self._path('/requests/date_range'), params=params) 75 | 76 | 77 | instance = ServiceApi(FetchAPI("http://192.168.56.101:8000")) 78 | 79 | ``` 80 | Take a look at the `todo` package to get a feel of how it would be implemented. 81 | 82 | Inspiration for this project 83 | https://www.youtube.com/watch?v=VgM0qmpHDiE 84 | 85 | TODO 86 | 87 | 1. Better Documentation 88 | 2. Tests 89 | 90 | Contributor: 91 | Oyeniyi biola -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/__init__.py -------------------------------------------------------------------------------- /example/api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/api/config/__init__.py -------------------------------------------------------------------------------- /example/api/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for api project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '+hew5wu7y$c62tzp#*77_-g+9jf4wymkz$hj1x8-ln0f&^ndt6' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'rest_framework', 39 | 'todolist.apps.TodolistConfig', 40 | 'django_extensions' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'config.urls' 54 | 55 | TEMPLATES = [{ 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, ] 68 | 69 | WSGI_APPLICATION = 'config.wsgi.application' 70 | 71 | # Database 72 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 73 | 74 | DATABASES = { 75 | 'default': { 76 | 'ENGINE': 'django.db.backends.sqlite3', 77 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 78 | } 79 | } 80 | 81 | # Password validation 82 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 83 | 84 | AUTH_PASSWORD_VALIDATORS = [ 85 | { 86 | 'NAME': 87 | 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 91 | 'django.contrib.auth.password_validation.MinimumLengthValidator', 92 | }, 93 | { 94 | 'NAME': 95 | 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 99 | 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 118 | 119 | STATIC_URL = '/static/' 120 | 121 | REST_FRAMEWORK = { 122 | 'DEFAULT_FILTER_BACKENDS': 123 | ('rest_framework_filters.backends.DjangoFilterBackend', ) 124 | } -------------------------------------------------------------------------------- /example/api/config/urls.py: -------------------------------------------------------------------------------- 1 | """api URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from todolist.views import router 19 | from rest_framework.documentation import include_docs_urls 20 | urlpatterns = [ 21 | url(r'^', include(router.urls)), 22 | url(r'^docs/', include_docs_urls(title='TodoService Documentation')), 23 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) 24 | ] 25 | -------------------------------------------------------------------------------- /example/api/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for api 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.10/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", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/api/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", "config.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /example/api/requirements.txt: -------------------------------------------------------------------------------- 1 | coreapi==2.3.0 2 | Django==2.0 3 | django-crispy-forms==1.6.1 4 | djangorestframework==3.7.3 5 | djangorestframework-filters==0.10.2 6 | Markdown==2.6.8 7 | -------------------------------------------------------------------------------- /example/api/todolist/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/api/todolist/__init__.py -------------------------------------------------------------------------------- /example/api/todolist/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /example/api/todolist/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodolistConfig(AppConfig): 5 | name = 'todolist' 6 | -------------------------------------------------------------------------------- /example/api/todolist/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.4 on 2017-03-14 15:24 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='TodoItem', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.TextField()), 24 | ('completed', models.BooleanField(default=False)), 25 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todos', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /example/api/todolist/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/api/todolist/migrations/__init__.py -------------------------------------------------------------------------------- /example/api/todolist/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from rest_framework import serializers 4 | 5 | 6 | # Create your models here. 7 | class TodoItem(models.Model): 8 | title = models.TextField() 9 | completed = models.BooleanField(default=False) 10 | owner = models.ForeignKey(User, null=True, related_name='todos', on_delete=models.CASCADE) 11 | 12 | def __repr__(self): 13 | return "" % self.title 14 | 15 | 16 | class TodoItemSerializer(serializers.ModelSerializer): 17 | class Meta: 18 | model = TodoItem 19 | fields = '__all__' 20 | 21 | class UserSerializer(serializers.ModelSerializer): 22 | class Meta: 23 | model = User 24 | fields = '__all__' -------------------------------------------------------------------------------- /example/api/todolist/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/api/todolist/views.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E1101 2 | from django.shortcuts import render 3 | from collections import OrderedDict 4 | from django.db import models 5 | from rest_framework import viewsets, routers, pagination 6 | from rest_framework_filters.backends import DjangoFilterBackend 7 | from rest_framework.filters import OrderingFilter 8 | import rest_framework_filters as filters 9 | from rest_framework.response import Response 10 | from .models import User, UserSerializer, TodoItem, TodoItemSerializer 11 | 12 | 13 | class StandardResultsSetPagination(pagination.PageNumberPagination): 14 | page_size = 100 15 | page_size_query_param = 'page_size' 16 | max_page_size = 1000 17 | 18 | def get_paginated_response(self, data, queryset, field): 19 | last_record = queryset.last() 20 | first_record = queryset.first() 21 | dict_val = [ 22 | ('count', self.page.paginator.count), 23 | ('next', self.get_next_link()), 24 | ('previous', self.get_previous_link()), 25 | ('results', data), 26 | ] 27 | if last_record: 28 | dict_val.append( ('last_id', last_record.pk), 29 | ) 30 | if first_record: 31 | dict_val.append( ('first_id', first_record.pk), 32 | ) 33 | 34 | response = OrderedDict(dict_val) 35 | model_fields = [x.name for x in queryset.model._meta.get_fields()] 36 | if field: 37 | if field in model_fields: 38 | date_range = queryset.aggregate( 39 | first=models.Min(field), last=models.Max(field)) 40 | response['date_range'] = date_range 41 | return Response(response) 42 | 43 | class BaseViewSet(viewsets.ModelViewSet): 44 | pagination_class = StandardResultsSetPagination 45 | filter_backends = (DjangoFilterBackend, OrderingFilter) 46 | ordering_fields = '__all__' 47 | 48 | def get_paginated_response(self, data): 49 | """ 50 | Return a paginated style `Response` object for the given output data. 51 | """ 52 | assert self.paginator is not None 53 | field_name = self.request.query_params.get('field_name') 54 | filted_query = self.filter_queryset(self.get_queryset()) 55 | return self.paginator.get_paginated_response(data, filted_query, field_name) 56 | 57 | class UserViewSet(BaseViewSet): 58 | queryset = User.objects.all() 59 | serializer_class = UserSerializer 60 | 61 | 62 | class TodoItemViewSet(BaseViewSet): 63 | queryset = TodoItem.objects.all() 64 | serializer_class = TodoItemSerializer 65 | 66 | 67 | router = routers.DefaultRouter() 68 | router.register(r'todos', TodoItemViewSet) 69 | router.register(r'users', UserViewSet) 70 | -------------------------------------------------------------------------------- /example/mailchecker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/mailchecker/__init__.py -------------------------------------------------------------------------------- /example/mailchecker/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Thread, Message 4 | from .forms import MessageForm, MessageInlineForm 5 | from mservice_model.admin import ServiceAdmin 6 | 7 | 8 | class MessageInline(admin.TabularInline): 9 | model = Message 10 | form = MessageInlineForm 11 | 12 | 13 | class MessageAdmin(ServiceAdmin): 14 | ordering = ('id', ) 15 | model = Message 16 | form = MessageForm 17 | 18 | 19 | class ThreadAdmin(ServiceAdmin): 20 | # inlines = [ 21 | # MessageInline, 22 | # ] 23 | fields = ('number_of_messages', ) 24 | list_display = ('id', 'to', 'number_of_messages') 25 | search_fields = ('id', ) 26 | ordering = ('id', ) 27 | actions = ['view_ids'] 28 | 29 | def view_ids(self, request, queryset): 30 | import pdb; pdb.set_trace() 31 | pass 32 | 33 | 34 | admin.site.register([Thread], ThreadAdmin) 35 | admin.site.register([Message], MessageAdmin) 36 | -------------------------------------------------------------------------------- /example/mailchecker/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Message 3 | 4 | class MessageForm(forms.ModelForm): 5 | sender = forms.EmailField() 6 | receiver = forms.EmailField() 7 | body = forms.Textarea() 8 | 9 | class Meta: 10 | fields = ('sender', 'receiver', 'body',) 11 | model = Message 12 | 13 | class MessageInlineForm(forms.ModelForm): 14 | sender = forms.EmailField() 15 | receiver = forms.EmailField() 16 | 17 | class Meta: 18 | fields = ('sender', 'receiver', 'body',) 19 | model = Message 20 | widgets = { 21 | 'sender': forms.Textarea(attrs={'cols': 20, 'rows': 2}), 22 | 'receiver': forms.Textarea(attrs={'cols': 20, 'rows': 2}), 23 | 'body': forms.Textarea(attrs={'cols': 80, 'rows': 2}), 24 | } 25 | 26 | def clean(self): 27 | cleaned_data = super(MessageInlineForm, self).clean() 28 | if self.instance and self.instance.id is not None: 29 | for key in ['sender', 'receiver']: 30 | try: 31 | del self.errors[key] 32 | except KeyError: 33 | pass 34 | return cleaned_data 35 | -------------------------------------------------------------------------------- /example/mailchecker/mailer.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import httplib2 3 | from apiclient.discovery import build 4 | from email.mime.text import MIMEText 5 | from mservice_model.api import ServiceApi, Bunch 6 | 7 | ME = 'me' 8 | 9 | 10 | class GmailApi(ServiceApi): 11 | def get_data(self, credentials=None, filter_by=None, cls=Bunch, **kwargs): 12 | """Returns a tuple of the fetched data and the total count""" 13 | messages = [] 14 | total = 0 15 | if cls.__name__ == 'Message': 16 | if 'pk' in filter_by: 17 | messages = get_message_by_id( 18 | credentials, filter_by['pk'], cls=cls) 19 | elif not 'thread' in filter_by: 20 | messages = [] 21 | else: 22 | messages, total = get_messages_by_thread_id( 23 | credentials, filter_by['thread'], cls=cls) 24 | 25 | if cls.__name__ == 'Thread': 26 | if 'id' in filter_by: 27 | messages = get_thread_by_id( 28 | credentials, filter_by['id'], cls=cls) 29 | 30 | else: 31 | messages, total = get_all_threads( 32 | credentials, filter_by, cls=cls) 33 | # import ipdb; ipdb.set_trace() 34 | # Can be a generator instead 35 | 36 | return messages, total 37 | 38 | 39 | def _get_gmail_service(credentials): 40 | http = httplib2.Http() 41 | http = credentials.authorize(http) 42 | return build('gmail', 'v1', http=http) 43 | 44 | 45 | def _make_message(msg, cls): 46 | try: 47 | parts = [p['body'] for p in msg['payload']['parts']] 48 | except KeyError: 49 | parts = [msg['payload']['body']] 50 | 51 | body = ''.join( 52 | base64.urlsafe_b64decode(p['data'].encode('utf-8')) for p in parts 53 | if 'data' in p) 54 | sender = [ 55 | h['value'] for h in msg['payload']['headers'] 56 | if h['name'].lower() in 'from' 57 | ][0] 58 | receiver = [ 59 | h['value'] for h in msg['payload']['headers'] 60 | if h['name'].lower() == 'to' 61 | ][0] 62 | return cls(id=msg['id'], 63 | thread_id=msg['threadId'], 64 | snippet=msg['snippet'], 65 | receiver=receiver, 66 | sender=sender, 67 | body=body) 68 | 69 | 70 | def send_message(credentials, 71 | frm, 72 | to, 73 | message_body, 74 | subject="Hello from Pycon", 75 | thread_id=None): 76 | gmail = _get_gmail_service(credentials) 77 | message = MIMEText(message_body) 78 | message['to'] = to 79 | message['from'] = frm 80 | message['subject'] = subject 81 | 82 | payload = {'raw': base64.b64encode(message.as_bytes()).decode('utf-8')} 83 | if thread_id: 84 | payload['threadId'] = thread_id 85 | return gmail.users().messages().send( 86 | userId=ME, 87 | body=payload, ).execute() 88 | 89 | 90 | def get_messages_by_thread_id(credentials, thread_id, cls=Bunch): 91 | gmail = _get_gmail_service(credentials) 92 | thread = gmail.users().threads().get(userId=ME, id=thread_id).execute() 93 | import ipdb 94 | ipdb.set_trace() 95 | total = 0 96 | return [_make_message(m, cls) for m in thread['messages']], total 97 | 98 | 99 | def get_all_threads(credentials, filter_by, cls=Bunch): 100 | to = (filter_by['to__icontains'] if 'to__icontains' in filter_by else None) 101 | 102 | gmail = _get_gmail_service(credentials) 103 | params = {'userId': ME, } 104 | if to: 105 | params['q'] = 'to:%s' % to 106 | threads = gmail.users().threads().list(**params).execute() 107 | total = threads['resultSizeEstimate'] 108 | if not threads or (to != None and threads['resultSizeEstimate'] is 0): 109 | return tuple(), total 110 | return tuple( 111 | cls(id=t['id'], number_of_messages=None, to=None) 112 | for t in threads['threads']), total 113 | 114 | 115 | def get_all_messages(credentials, cls=Bunch): 116 | gmail = _get_gmail_service(credentials) 117 | messages = gmail.users().messages().list(userId=ME, ).execute()['messages'] 118 | import ipdb 119 | ipdb.set_trace() 120 | total = 0 121 | return [_make_message(m, cls) for m in messages], total 122 | 123 | 124 | def get_thread_by_id(credentials, thread_id, cls=Bunch): 125 | gmail = _get_gmail_service(credentials) 126 | # import pdb; pdb.set_trace() 127 | thread = gmail.users().threads().get(userId=ME, id=thread_id).execute() 128 | return [ 129 | cls(id=thread['id'], 130 | to=None, 131 | number_of_messages=len(thread['messages'])) 132 | ] 133 | 134 | 135 | def get_message_by_id(credentials, message_id, cls=Bunch): 136 | gmail = _get_gmail_service(credentials) 137 | message = gmail.users().messages().get(userId=ME, id=message_id).execute() 138 | return [_make_message(message, cls)] 139 | 140 | -------------------------------------------------------------------------------- /example/mailchecker/manager.py: -------------------------------------------------------------------------------- 1 | from oauth2client.file import Storage 2 | from django.conf import settings 3 | from mservice_model.manager import ServiceManager 4 | from mservice_model.queryset import ServiceQuerySet 5 | 6 | 7 | class ThreadQuerySet(ServiceQuerySet): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | self.credentials = kwargs.pop('credentials') 11 | 12 | def params_for_fetching_data(self, **kwargs): 13 | return super().params_for_fetching_data(credentials=self.credentials, **kwargs) 14 | 15 | def _clone(self, *args, **kwargs): 16 | return super()._clone(credentials=self.credentials, **kwargs) 17 | 18 | def filter(self, *args, **kwargs): 19 | filter_args = self._get_filter_args(args, kwargs) 20 | if 'to__icontains' in filter_args: 21 | return ThreadQuerySet( 22 | model=self.model, 23 | credentials=self.credentials, 24 | mailer=self.mailer, 25 | filter_query={'to__icontains': filter_args['to__icontains']}) 26 | # import pdb; pdb.set_trace() 27 | return self 28 | 29 | 30 | class MessageQuerySet(ServiceQuerySet): 31 | def _create(self, frm, to, message_body, thread_id=None): 32 | return self.mailer.send_message( 33 | self.credentials, frm, to, message_body, thread_id=thread_id) 34 | 35 | def filter(self, *args, **kwargs): 36 | filter_args = self._get_filter_args(args, kwargs) 37 | if 'thread' in filter_args: 38 | 39 | try: 40 | tid = filter_args['thread'].id 41 | except AttributeError: 42 | tid = filter_args['thread'] 43 | 44 | return MessageQuerySet( 45 | model=self.model, 46 | credentials=self.credentials, 47 | mailer=self.mailer, 48 | filter_query={'thread': tid}) 49 | return self 50 | 51 | def get(self, *args, **kwargs): 52 | 53 | filter_args = self._get_filter_args(args, kwargs) 54 | if 'pk' not in filter_args: 55 | raise Exception("No PK found in Message GET") 56 | 57 | return MessageQuerySet( 58 | model=self.model, 59 | credentials=self.credentials, 60 | mailer=self.mailer, 61 | filter_query={'pk': filter_args['pk']})[0] 62 | 63 | 64 | class GmailManager(ServiceManager): 65 | def __init__(self, model, **kwargs): 66 | super(GmailManager, self).__init__(model, **kwargs) 67 | storage = Storage(settings.CREDENTIALS_PATH) 68 | self.credentials = storage.get() 69 | 70 | def get_queryset(self): 71 | return self.queryset( 72 | credentials=self.credentials, 73 | model=self.model, 74 | mailer=self.mailer, 75 | filter_query=self.initial_filter_query, ) 76 | 77 | 78 | class ThreadManager(GmailManager): 79 | queryset = ThreadQuerySet 80 | 81 | 82 | class MessageManager(GmailManager): 83 | queryset = MessageQuerySet 84 | -------------------------------------------------------------------------------- /example/mailchecker/models.py: -------------------------------------------------------------------------------- 1 | # from django.db.models import ForeignKey 2 | # from mservice_model.models import ServiceModel 3 | # from .manager import ThreadManager, MessageManager 4 | # from . import mailer 5 | # from django.db.models.fields import (AutoField, CharField, TextField) 6 | # from mservice_model.options import ServiceOptions 7 | 8 | 9 | # class GmailAutoField(AutoField): 10 | # def to_python(self, value): 11 | # return value 12 | 13 | 14 | # class Thread(ServiceModel): 15 | # _default_manager = ThreadManager 16 | # _service_api = mailer.GmailApi("h") 17 | 18 | # # _meta = ThreadOptions() 19 | # class Meta(ServiceOptions): 20 | # _service_fields = { 21 | # 'id': GmailAutoField(), 22 | # 'to': CharField(max_length=200), 23 | # 'number_of_messages': CharField(max_length=200), 24 | # } 25 | 26 | # def __init__(self, id=None, to=None, number_of_messages=None): 27 | # self.id = id 28 | # self.to = to 29 | # self.number_of_messages = number_of_messages 30 | 31 | # @property 32 | # def messages(self): 33 | # return Message.objects.filter(thread=self.id) 34 | 35 | # def __unicode__(self): 36 | # return "" % ( 37 | # self.id, "???" 38 | # if self.number_of_messages is None else self.number_of_messages) 39 | 40 | # def __repr__(self): 41 | # return self.__unicode__() 42 | 43 | # def save(self, *args, **kwargs): 44 | # pass 45 | 46 | 47 | # class Message(ServiceModel): 48 | # _default_manager = MessageManager 49 | # _service_api = mailer 50 | 51 | # class Meta(ServiceOptions): 52 | # _service_fields = { 53 | # 'id': GmailAutoField(), 54 | # 'receiver': CharField(max_length=200), 55 | # 'sender': CharField(max_length=200), 56 | # 'snippet': CharField(max_length=200), 57 | # 'body': TextField(), 58 | # } 59 | # # def additional_bind(self): 60 | # # # from .models import Thread, Message 61 | # # # self.thread = ForeignKey(Thread) 62 | # # # self.thread.contribute_to_class(Thread, 'thread') 63 | # # # self.concrete_model = Message 64 | # # # self._service_other_fields['thread'] = self.thread 65 | 66 | # def __init__(self, 67 | # id=None, 68 | # receiver=None, 69 | # sender=None, 70 | # snippet=None, 71 | # body=None, 72 | # thread=None, 73 | # thread_id=None): 74 | # self.id = id 75 | # self.receiver = receiver 76 | # self.sender = sender 77 | # self.snippet = snippet 78 | # self.body = body 79 | # self.thread_id = thread_id 80 | 81 | # from django.utils.encoding import smart_text 82 | # if self.body: 83 | # self.body = smart_text(self.body) 84 | # if self.snippet: 85 | # self.snippet = smart_text(self.snippet) 86 | 87 | # @property 88 | # def thread(self): 89 | # # return Thread.objects.get(id=self.thread_id) 90 | # return Thread.objects.get(id=self.id) 91 | 92 | # @thread.setter 93 | # def thread(self, value): 94 | # self.thread_id = value.id 95 | 96 | # def __unicode__(self): 97 | # from django.utils.encoding import smart_text 98 | # # return smart_text("" % (self.id, 99 | # # self.snippet[:30])) 100 | # return smart_text("" % self.id) 101 | 102 | # def __repr__(self): 103 | # return self.__unicode__() 104 | 105 | # def save(self, *args, **kwargs): 106 | 107 | # # Messages already save do not need re-sending 108 | # if self.id: 109 | # return 110 | 111 | # # Send message and fetch ID 112 | # result = self.objects.get_queryset()._create( 113 | # frm=self.sender, 114 | # to=self.receiver, 115 | # message_body=self.body, 116 | # thread_id=self.thread_id) 117 | 118 | # # Not all results are returned from the API, re-pull and set 119 | # # all fields (basically, reassigning the entire instance) 120 | # new_instance = self.objects.get(pk=result['id']) 121 | # for field_name in (f.name for f in self._meta.get_fields()): 122 | # setattr(self, field_name, getattr(new_instance, field_name)) 123 | 124 | 125 | # # Thread._meta._bind() 126 | # # Message._meta._bind() 127 | -------------------------------------------------------------------------------- /example/mailchecker/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from . import mailer 3 | 4 | from django.conf import settings 5 | from django.db.models import Q 6 | from oauth2client.file import Storage 7 | from .models import Thread, Message 8 | from .manager import ThreadQuerySet, MessageQuerySet 9 | from .mailer import Bunch 10 | from unittest import mock 11 | 12 | 13 | 14 | class ThreadTestCase(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.mailer = mock.MagicMock() 18 | self._old_mailer = Thread._default_manager.mailer 19 | 20 | Message._default_manager.mailer = self.mailer 21 | Thread._default_manager.mailer = self.mailer 22 | 23 | def tearDown(self): 24 | Message._default_manager.mailer = self._old_mailer 25 | Thread._default_manager.mailer = self._old_mailer 26 | 27 | def test_reverse_relation_lookup(self): 28 | self.mailer.get_data.return_value = [ 29 | Bunch(id=str(n)) for n in range(10) 30 | ], 10 31 | t = Thread(id='123123') 32 | # import pdb; pdb.set_trace() 33 | self.assertEqual(t.messages.count(), 10) 34 | 35 | 36 | class MessageTestCase(unittest.TestCase): 37 | 38 | def setUp(self): 39 | self.mailer = mock.MagicMock() 40 | self._old_mailer = Thread._default_manager.mailer 41 | 42 | Message._default_manager.mailer = self.mailer 43 | Thread._default_manager.mailer = self.mailer 44 | 45 | def tearDown(self): 46 | Message._default_manager.mailer = self._old_mailer 47 | Thread._default_manager.mailer = self._old_mailer 48 | 49 | def test_reverse_relation_works(self): 50 | self.mailer.get_data.return_value = [Message( 51 | id="00126" 52 | )], 0 53 | t = Message(id="00125") 54 | # import pdb; pdb.set_trace() 55 | self.assertEqual(t.thread.id, "00126") 56 | # import pdb; pdb.set_trace() 57 | self.assertEqual( 58 | self.mailer.get_data.call_args[0][1]['id'], 59 | '00125' 60 | ) 61 | 62 | 63 | class MessageQuerySetTestCase(unittest.TestCase): 64 | 65 | def setUp(self): 66 | storage = Storage(settings.CREDENTIALS_PATH) 67 | self.credentials = storage.get() 68 | self.mailer = mock.MagicMock() 69 | 70 | def test_message_with_filter(self): 71 | self.mailer.get_data.return_value = [ 72 | Bunch(id='1'), 73 | ], 0 74 | mqs = MessageQuerySet( 75 | model=Message, 76 | credentials=self.credentials, 77 | mailer=self.mailer 78 | ) 79 | self.assertEqual(mqs.filter(thread='1')[0].pk, '1') 80 | self.assertEqual( 81 | self.mailer.get_data.call_args[0][1]['thread'], 82 | '1' 83 | ) 84 | 85 | def test_message_with_id(self): 86 | self.mailer.get_data.return_value = [Bunch(id='1')], 0 87 | mqs = MessageQuerySet( 88 | model=Message, 89 | credentials=self.credentials, 90 | mailer=self.mailer 91 | ) 92 | self.assertEqual(mqs.get(pk='1843903').pk, '1') 93 | self.assertEqual( 94 | self.mailer.get_data.call_args[0][1]['pk'], 95 | '1843903' 96 | ) 97 | 98 | 99 | class ThreadQuerySetTestCase(unittest.TestCase): 100 | 101 | def setUp(self): 102 | self.mailer = mock.MagicMock() 103 | storage = Storage(settings.CREDENTIALS_PATH) 104 | self.credentials = storage.get() 105 | 106 | def test_queryset(self): 107 | self.mailer.get_data.return_value = [ 108 | Bunch(id='1'), 109 | Bunch(id='2'), 110 | Bunch(id='3'), 111 | ], 3 112 | 113 | tqs = ThreadQuerySet( 114 | model=Thread, 115 | credentials=self.credentials, 116 | mailer=self.mailer 117 | ) 118 | self.assertEqual(tqs.count(), 3) 119 | self.assertEqual(tqs[1].id, '2') 120 | self.assertTrue([ 121 | model._meta 122 | for model in tqs.all() 123 | ]) 124 | 125 | def test_queryset_get(self): 126 | self.mailer.get_data.return_value = [Bunch(id='target')], 1 127 | tqs = ThreadQuerySet( 128 | model=Thread, 129 | credentials=self.credentials, 130 | mailer=self.mailer 131 | ) 132 | self.assertEqual(tqs.get(id='target').id, 'target') 133 | self.assertEqual( 134 | self.mailer.get_data.call_args[0][1]['id'], 135 | 'target' 136 | ) 137 | 138 | def test_queryset_filter(self): 139 | self.mailer.get_data.return_value = [ 140 | Bunch(id='target1'), 141 | Bunch(id='target2'), 142 | ], 2 143 | tqs = ThreadQuerySet( 144 | model=Thread, 145 | credentials=self.credentials, 146 | mailer=self.mailer 147 | ) 148 | tqs2 = tqs.filter(to__icontains="daniel@gmail.com") 149 | self.assertNotEqual(tqs, tqs2) 150 | # import pdb; pdb.set_trace() 151 | # self.assertEqual( 152 | # self.mailer.get_data.call_args_list[2]['to__icontains'], 153 | # 'daniel@gmail.com' 154 | # ) 155 | 156 | self.assertEqual( 157 | [b.id for b in tqs2.all()], 158 | ['target1', 'target2'] 159 | ) 160 | 161 | def test_queryset_filter_Q(self): 162 | self.mailer.get_data.return_value = [ 163 | Bunch(id='target1'), 164 | Bunch(id='target2'), 165 | ], 2 166 | tqs = ThreadQuerySet( 167 | model=Thread, 168 | credentials=self.credentials, 169 | mailer=self.mailer 170 | ) 171 | query = Q(to__icontains="daniel@gmail.com") 172 | tqs2 = tqs.filter(query) 173 | self.assertEqual( 174 | [b.id for b in tqs2.all()], 175 | ['target1', 'target2'] 176 | ) 177 | # self.assertEqual( 178 | # self.mailer.get_all_threads.call_args_list[0][1]['to'], 179 | # 'daniel@gmail.com' 180 | # ) 181 | 182 | 183 | if __name__ == '__main__': 184 | unittest.main() 185 | -------------------------------------------------------------------------------- /example/with_graphql/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", "with_graphql.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /example/with_graphql/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/with_graphql/todo/__init__.py -------------------------------------------------------------------------------- /example/with_graphql/todo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from mservice_model.admin import ServiceAdmin 3 | from django.contrib.admin import sites 4 | from .models import TodoItem 5 | from django import forms 6 | # Register your models here. 7 | 8 | class TodoItemForm(forms.ModelForm): 9 | class Meta: 10 | fields = ['title','completed'] 11 | 12 | @admin.register(TodoItem) 13 | class TodoItemAdmin(ServiceAdmin): 14 | list_display = ['pk','title','completed'] 15 | form = TodoItemForm 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/with_graphql/todo/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from mservice_model.api import ServiceApi, FetchHelpers 3 | 4 | instance = ServiceApi('http://localhost:8000/todos/') -------------------------------------------------------------------------------- /example/with_graphql/todo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodoConfig(AppConfig): 5 | name = 'todo' 6 | -------------------------------------------------------------------------------- /example/with_graphql/todo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/with_graphql/todo/migrations/__init__.py -------------------------------------------------------------------------------- /example/with_graphql/todo/models.py: -------------------------------------------------------------------------------- 1 | from mservice_model.models import ServiceModel, model_factory 2 | from .api import instance as todo_api 3 | # Create your models here. 4 | 5 | class Mixin(object): 6 | def __repr__(self): 7 | return "" % self.id 8 | 9 | class TodoItem(ServiceModel): 10 | _service_api = todo_api 11 | 12 | class Meta: 13 | ordering = ('id',) 14 | verbose_name = "Todo Item" 15 | 16 | # TodoItem = model_factory(todo_api, "TodoItem", base_class=Mixin) 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/with_graphql/todo/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .models import TodoItem 3 | # Create your tests here. 4 | 5 | class TodoItemTestCase(TestCase): 6 | def test_can_fetch_all_todos(self): 7 | result = TodoItem.objects.all() 8 | self.assertGreater(len(result), 0) 9 | -------------------------------------------------------------------------------- /example/with_graphql/todo/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /example/with_graphql/with_graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/example/with_graphql/with_graphql/__init__.py -------------------------------------------------------------------------------- /example/with_graphql/with_graphql/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for with_graphql project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '%(ecw_ja*mis1a@5o_%f=d_(o$2u7s!xsiupw$xp=13vc7wul$' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_extensions', 41 | # 'todo', 42 | 'mservice_model', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'with_graphql.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'with_graphql.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | -------------------------------------------------------------------------------- /example/with_graphql/with_graphql/urls.py: -------------------------------------------------------------------------------- 1 | """with_graphql URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /example/with_graphql/with_graphql/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for with_graphql 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/2.0/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", "with_graphql.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /mservice_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/__init__.py -------------------------------------------------------------------------------- /mservice_model/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.core.paginator import Paginator, Page 3 | from django.utils.functional import cached_property 4 | 5 | 6 | 7 | class ServicePage(Page): 8 | pass 9 | 10 | 11 | class ServicePaginator(Paginator): 12 | @property 13 | def count(self): 14 | """ 15 | Returns the total number of objects, across all pages. 16 | """ 17 | # import ipdb; ipdb.set_trace() 18 | self.object_list._get_data() 19 | try: 20 | return self.object_list.total_count 21 | except (AttributeError, TypeError): 22 | # AttributeError if object_list has no count() method. 23 | # TypeError if object_list.count() requires arguments 24 | # (i.e. is of type list). 25 | return len(self.object_list) 26 | 27 | def page(self, number): 28 | """ 29 | Returns a Page object for the given 1-based page number. 30 | """ 31 | number = self.validate_number(number) 32 | new_object_list = self.object_list._get_data( 33 | page=number, per_page=self.per_page) 34 | return self._get_page(new_object_list, number, self) 35 | 36 | 37 | class ServiceAdmin(admin.ModelAdmin): 38 | list_per_page = 100 39 | paginator = ServicePaginator 40 | 41 | def get_changelist(self, request, **kwargs): 42 | """ 43 | Returns the ChangeList class for use on the changelist page. 44 | """ 45 | from .main import ServiceChangeList as ChangeList 46 | return ChangeList 47 | 48 | 49 | -------------------------------------------------------------------------------- /mservice_model/admin/main.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.options import (IS_POPUP_VAR, TO_FIELD_VAR, 2 | IncorrectLookupParameters) 3 | from django.contrib.admin.views.main import ChangeList 4 | from django.core.exceptions import (FieldDoesNotExist, ImproperlyConfigured, 5 | SuspiciousOperation) 6 | from django.core.paginator import InvalidPage 7 | 8 | 9 | class ServiceChangeList(ChangeList): 10 | def get_results(self, request): 11 | paginator = self.model_admin.get_paginator(request, self.queryset, 12 | self.list_per_page) 13 | # Get the number of objects, with admin filters applied. 14 | # import ipdb; ipdb.set_trace() 15 | 16 | result_count = paginator.count 17 | # import pdb; pdb.set_trace() 18 | # Get the total number of objects, with no admin filters applied. 19 | if self.model_admin.show_full_result_count: 20 | self.root_queryset.count() 21 | full_result_count = self.root_queryset.total_count 22 | else: 23 | full_result_count = None 24 | # import ipdb; ipdb.set_trace() 25 | can_show_all = result_count <= self.list_max_show_all 26 | multi_page = result_count > self.list_per_page 27 | 28 | # Get the list of objects to display on this page. 29 | if (self.show_all and can_show_all) or not multi_page: 30 | result_list = self.queryset._clone() 31 | else: 32 | try: 33 | result_list = paginator.page(self.page_num + 1).object_list 34 | except InvalidPage: 35 | raise IncorrectLookupParameters 36 | 37 | self.result_count = result_count 38 | self.show_full_result_count = self.model_admin.show_full_result_count 39 | # Admin actions are shown if there is at least one entry 40 | # or if entries are not counted because show_full_result_count is disabled 41 | self.show_admin_actions = not self.show_full_result_count or bool( 42 | full_result_count) 43 | self.full_result_count = full_result_count 44 | self.result_list = result_list 45 | self.can_show_all = can_show_all 46 | self.multi_page = multi_page 47 | self.paginator = paginator 48 | -------------------------------------------------------------------------------- /mservice_model/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import date, datetime 3 | from decimal import Decimal 4 | 5 | import maya 6 | import requests 7 | from django.db.models import fields 8 | from django.db.models.fields import FieldDoesNotExist 9 | from django.utils.functional import cached_property 10 | 11 | from mservice_model.fields import AutoField, ListField 12 | 13 | 14 | class MyEncoder(json.JSONEncoder): 15 | 16 | def default(self, obj): 17 | if isinstance(obj, datetime): 18 | return obj.isoformat() 19 | if isinstance(obj, Decimal): 20 | return str(obj) 21 | 22 | return json.JSONEncoder.default(self, obj) 23 | 24 | 25 | class Bunch(object): 26 | 27 | def __init__(self, *args, **kwargs): 28 | self.__dict__ = kwargs 29 | self.pk = self.id 30 | 31 | def __unicode__(self): 32 | return self.id 33 | 34 | def serializable_value(self, field_name): 35 | try: 36 | field = self._meta.get_field(field_name) 37 | except FieldDoesNotExist: 38 | return getattr(self, field_name) 39 | return getattr(self, field.attname) 40 | 41 | 42 | class FetchHelpers(object): 43 | """The api class to implement all the api calls responsible 44 | to get the admin interface to be functional""" 45 | 46 | def __init__(self, url): 47 | self.base_url = url 48 | 49 | def _make_base_request(self, data, cls): 50 | """Create an instance of the class passed from the 51 | response of the dictionary""" 52 | field_with_types = { 53 | k: v['type'] 54 | for k, v in self.fields.items() 55 | } 56 | options = { 57 | 'decimal': lambda x: Decimal(x) if x else None, 58 | 'datetime': lambda x: maya.parse(x).datetime() if x else None, 59 | } 60 | new_data = {} 61 | for key, value in data.items(): 62 | inst = field_with_types[key] 63 | try: 64 | result = options[inst](value) 65 | except KeyError: 66 | result = value 67 | new_data.update({key: result}) 68 | 69 | return cls(**new_data) 70 | 71 | def _fetch_data(self, method, url, **kwargs): 72 | method_map = { 73 | 'GET': requests.get, 74 | 'POST': requests.post, 75 | 'PUT': requests.put, 76 | 'DELETE': requests.delete, 77 | 'OPTIONS': requests.options 78 | } 79 | request = method_map.get(method) 80 | response = request(url, **kwargs) 81 | response.raise_for_status() 82 | return response.json() 83 | 84 | def _path(self, path=''): 85 | return self.base_url + path 86 | 87 | def create_data(self, data): 88 | """Makes a post api to end point to create record. 89 | if id exists, makes a put request instead""" 90 | d_id = data.pop('id',None) 91 | if d_id: 92 | response = self._fetch_data('PUT',self._path('{}/'.format(d_id)),json=data) 93 | else: 94 | response = self._fetch_data('POST',self._path(''),json=data) 95 | # import pdb; pdb.set_trace() 96 | return response 97 | 98 | def _populate_base_request_fields(self, data): 99 | result = {} 100 | options = { 101 | 'decimal': fields.DecimalField, 102 | 'datetime': fields.DateTimeField, 103 | 'integer': fields.IntegerField, 104 | 'string': fields.CharField, 105 | 'email': fields.EmailField, 106 | 'field': fields.CharField, 107 | 'list': ListField, 108 | 'boolean': fields.BooleanField 109 | } 110 | for key, value in data.items(): 111 | default_dict = { 112 | 'verbose_name': value['label'], 113 | 'null': value['required'], 114 | } 115 | if value['type'] in ["string", "email"]: 116 | if value.get('max_length'): 117 | default_dict.update(max_length=value['max_length']) 118 | if value['type'] == 'field': 119 | default_dict.update(max_length=70) 120 | if key == 'id': 121 | result[key] = AutoField() 122 | else: 123 | result[key] = options[value['type']](**default_dict) 124 | 125 | return result 126 | 127 | @cached_property 128 | def fields(self): 129 | return self.get_fields() 130 | 131 | def get_fields(self): 132 | """Makes an OPTIONS call to the djangorestframework endpoint""" 133 | fetched_data = self._fetch_data('OPTIONS', self._path('')) 134 | return fetched_data['actions']['POST'] 135 | 136 | def construct_model_fields(self): 137 | data = self.fields 138 | return self._populate_base_request_fields(data) 139 | 140 | def get_datetimes(self, field_name, kind, order, filter_query, **kwargs): 141 | """API logic to get the list of datetimes""" 142 | raise NotImplementedError 143 | 144 | def get_date_range(self, field_name): 145 | """API logic to implent the date ranges for `date_heirachy` in admin""" 146 | raise NotImplementedError 147 | 148 | def get_values_list(self, *args, **kwargs): 149 | """API logic to emulate returning queryset as a list of values just like 150 | django's orm .values_list. takes an optional parameter flat""" 151 | raise NotImplementedError 152 | 153 | def get_object_by_id(self, request_id, cls=Bunch): 154 | data = self._fetch_data('GET', self._path( 155 | '{}/'.format(request_id))) 156 | return [self._make_base_request(data, cls)] 157 | 158 | def get_all_objects(self, filter_by, order_by, cls): 159 | params = {**filter_by, **{'field_name': 'modified'}} 160 | if order_by: 161 | params.update(ordering=','.join(order_by)) 162 | print(order_by) 163 | data = self._fetch_data('GET', self._path(''), params=params) 164 | # import pdb; pdb.set_trace() 165 | as_objects = [self._make_base_request(o, cls) for o in data['results']] 166 | total = data['count'] 167 | date_range = data.get('date_range') 168 | last_id = data.get('last_id') 169 | return as_objects, total, date_range, last_id 170 | 171 | 172 | 173 | class ServiceApi(object): 174 | 175 | def __init__(self, api_instance, base_class=FetchHelpers): 176 | self.instance = FetchHelpers(api_instance) 177 | 178 | def get_data(self, filter_by, order_by, cls=Bunch, **kwargs): 179 | base_requests = [] 180 | total = 0 181 | date_range = None 182 | last_id = None 183 | new_filter_by = {**filter_by, **kwargs} 184 | print(new_filter_by) 185 | if 'pk' in filter_by or 'id' in filter_by: 186 | key = 'pk' 187 | if 'id' in filter_by: 188 | key = 'id' 189 | base_requests = self.instance.get_object_by_id(new_filter_by[key], 190 | cls) 191 | else: 192 | base_requests, total, date_range, last_id = self.instance.get_all_objects(new_filter_by, 193 | order_by, cls) 194 | date_range = self.serialize_date_range(date_range) 195 | return base_requests, total, date_range, last_id 196 | 197 | def create(self, cls=Bunch, **kwargs): 198 | """Make api call to create new record 199 | expects a dictionary that can be serialized to the class""" 200 | as_dict = self.to_serializable_dict(**kwargs) 201 | response = self.instance.create_data(as_dict) 202 | return self.instance._make_base_request(response, cls) 203 | 204 | def fetch_date_range(self, field_name): 205 | return self.instance.get_date_range(field_name) 206 | 207 | def serialize_date_range(self, date_range): 208 | result = date_range or {} 209 | if result: 210 | result.update(first=maya.when(result['first']).datetime()) 211 | result.update(last=maya.when(result['last']).datetime()) 212 | return result 213 | 214 | def aggregate(self, **kwargs): 215 | aliases = [x.default_alias for key, x in kwargs.items()] 216 | field_name = aliases[0].split('__')[0] 217 | result = self.fetch_date_range(field_name) 218 | if 'first' in kwargs.keys(): 219 | result.update(first=maya.when(result['first']).datetime()) 220 | if 'last' in kwargs.keys(): 221 | result.update(last=maya.when(result['last']).datetime()) 222 | return result 223 | 224 | def fetch_datetimes(self, *args, **kwargs): 225 | return self.instance.get_datetimes(*args, **kwargs) 226 | 227 | def datetimes(self, *args, **kwargs): 228 | value = [ 229 | maya.parse(x).datetime("Africa/Lagos") 230 | for x in self.fetch_datetimes(*args, **kwargs) 231 | ] 232 | return value 233 | 234 | def get_values_list(self, *args, **kwargs): 235 | return self.instance.get_values_list(*args, **kwargs) 236 | 237 | def to_serializable_dict(self, **kwargs): 238 | """Converts all values of kwargs to a 239 | serializable value in python""" 240 | as_string = json.dumps(kwargs, cls=MyEncoder) 241 | return json.loads(as_string) 242 | 243 | def initialize(self): 244 | """""" 245 | return self.instance.construct_model_fields() 246 | 247 | -------------------------------------------------------------------------------- /mservice_model/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django import forms 3 | 4 | 5 | class AutoField(models.fields.AutoField): 6 | def to_python(self, value): 7 | return value 8 | 9 | 10 | class ListFormField(forms.CharField): 11 | def to_python(self, value): 12 | """Normalize data to a list of strings.""" 13 | # Return an empty list if no input was given. 14 | if not value: 15 | return [] 16 | return value.split(',') 17 | 18 | def prepare_value(self, value): 19 | if isinstance(value, list): 20 | return ','.join(value) 21 | return value 22 | 23 | def validate(self, value): 24 | """Check if value consists only of valid emails.""" 25 | # Use the parent's handling of required fields, etc. 26 | super().validate(value) 27 | 28 | 29 | class ListField(models.Field): 30 | "Implements comma-separated storage of lists" 31 | 32 | def __init__(self, separator=",", *args, **kwargs): 33 | self.separator = separator 34 | self.internal_type = kwargs.pop('internal_type', 35 | 'CharField') # or TextField 36 | super(ListField, self).__init__(*args, **kwargs) 37 | 38 | def deconstruct(self): 39 | name, path, args, kwargs = super(ListField, self).deconstruct() 40 | # Only include kwarg if it's not the default 41 | if self.separator != ",": 42 | kwargs['separator'] = self.separator 43 | return name, path, args, kwargs 44 | 45 | def to_python(self, value): 46 | if isinstance(value, list): 47 | return value 48 | 49 | if value is None: 50 | return [] 51 | 52 | return value.split(self.separator) 53 | 54 | def value_to_string(self, obj): 55 | value = self.value_from_object(obj) 56 | 57 | return self.get_prep_value(value) 58 | 59 | def get_prep_value(self, value): 60 | return ','.join(value) 61 | 62 | def formfield(self, **kwargs): 63 | # This is a fairly standard way to set up some defaults 64 | # while letting the caller override them. 65 | defaults = {'form_class': ListFormField} 66 | defaults.update(kwargs) 67 | return super().formfield(**defaults) -------------------------------------------------------------------------------- /mservice_model/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ServiceForm(forms.ModelForm): 5 | # available_days = ListFormField(required=False) 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | for x in self.fields.keys(): 9 | self.fields[x].required = False -------------------------------------------------------------------------------- /mservice_model/manager.py: -------------------------------------------------------------------------------- 1 | 2 | class ServiceManager(object): 3 | mailer = None # The mailer component 4 | 5 | def __init__(self, model, **kwargs): 6 | self.model = model 7 | self.mailer = self.mailer or kwargs.get('mailer') 8 | self.initial_filter_query = kwargs.get('initial_filter_query', {}) 9 | 10 | def complex_filter(self, filter_obj): 11 | return self 12 | 13 | def order_by(self, *args, **kwargs): 14 | return self 15 | 16 | def using(self, *args, **kwargs): 17 | return self 18 | 19 | def iterator(self): 20 | return iter(self.get_queryset()) 21 | 22 | def all(self): 23 | return self.get_queryset().all() 24 | 25 | def first(self): 26 | return self.get_queryset().first() 27 | 28 | def last(self): 29 | return self.get_queryset().last() 30 | 31 | def count(self): 32 | queryset = self.get_queryset() 33 | queryset.all() 34 | return queryset.total_count 35 | 36 | def filter(self, *args, **kwargs): 37 | return self.get_queryset().filter(*args, **kwargs) 38 | 39 | def get_queryset(self): 40 | return self.queryset( 41 | model=self.model, 42 | mailer=self.mailer, 43 | filter_query=self.initial_filter_query, 44 | ) 45 | 46 | def get(self, *args, **kwargs): 47 | return self.get_queryset().get(*args, **kwargs) 48 | 49 | def create(self, **kwargs): 50 | return self.get_queryset().create(**kwargs) 51 | -------------------------------------------------------------------------------- /mservice_model/models.py: -------------------------------------------------------------------------------- 1 | from six import with_metaclass 2 | import inspect 3 | from django.db.models.fields import FieldDoesNotExist 4 | from django.db.models.base import ModelState 5 | from django.apps import apps 6 | from .options import ServiceOptions 7 | from .manager import ServiceManager 8 | from .queryset import ServiceQuerySet 9 | from django.forms import modelform_factory 10 | # from .options2 import ServiceOptions 11 | 12 | 13 | class BaseManager(ServiceManager): 14 | queryset = ServiceQuerySet 15 | 16 | 17 | class constructor(type): 18 | def __init__(cls, name, bases, clsdict): 19 | if len(cls.mro()) > 2: 20 | cls._meta._bind() 21 | super().__init__(name, bases, clsdict) 22 | 23 | def __new__(cls, name, bases, attrs): 24 | super_new = super(constructor, cls).__new__ 25 | parents = [b for b in bases if isinstance(b, constructor)] 26 | if not parents: 27 | return super_new(cls, name, bases, attrs) 28 | 29 | module = attrs.pop('__module__', None) 30 | new_class = super_new(cls, name, bases, attrs) 31 | attr_meta = attrs.pop('Meta', None) 32 | if not attr_meta: 33 | meta = getattr(new_class, 'Meta', ServiceOptions) 34 | else: 35 | params = {key:value for key, value in attr_meta.__dict__.items() if not key.startswith('__') and not callable(key)} 36 | SubClass = type(attr_meta.__name__, (ServiceOptions,), params) 37 | meta = SubClass 38 | base_meta = getattr(new_class, '_meta', None) 39 | 40 | app_label = None 41 | app_config = apps.get_containing_app_config(module) 42 | if getattr(meta, 'app_label', None) is None: 43 | if app_config is None: 44 | raise RuntimeError( 45 | "Model class %s.%s doesn't declare an explicit " 46 | "app_label and isn't in an application in " 47 | "INSTALLED_APPS." % (module, name)) 48 | else: 49 | app_label = app_config.label 50 | _service_fields = getattr(meta, '_service_fields',None) 51 | 52 | # import pdb 53 | # pdb.set_trace() 54 | 55 | dm = attrs.pop('_default_manager', BaseManager) 56 | service = attrs.pop('_service_api', None) 57 | if not _service_fields: 58 | _service_fields = service.initialize() 59 | # import ipdb; ipdb.set_trace() 60 | new_class.add_to_class( 61 | '_meta', meta( 62 | meta, app_label=app_label, the_class=new_class, 63 | _service_fields=_service_fields)) 64 | 65 | # import pdb; pdb.set_trace() 66 | if dm: 67 | new_class._default_manager = dm(new_class, mailer=service) 68 | new_class.objects = new_class._default_manager 69 | 70 | return new_class 71 | 72 | def add_to_class(cls, name, value): 73 | # We should call the contribute_to_class method only if it's bound 74 | if not inspect.isclass(value) and hasattr(value, 75 | 'contribute_to_class'): 76 | value.contribute_to_class(cls, name) 77 | else: 78 | setattr(cls, name, value) 79 | 80 | 81 | class ServiceModel(with_metaclass(constructor)): 82 | _deferred = False 83 | _state = ModelState() 84 | 85 | def __str__(self): 86 | return str(self.id) 87 | 88 | def serializable_value(self, field_name): 89 | try: 90 | field = self._meta.get_field(field_name) 91 | except FieldDoesNotExist: 92 | return getattr(self, field_name) 93 | return getattr(self, field.attname) 94 | 95 | @property 96 | def pk(self): 97 | if hasattr(self, 'id'): 98 | return self.id 99 | self.id = None 100 | return self.id 101 | 102 | # klass.objects = klass._default_manager 103 | 104 | def __eq__(self, other): 105 | if isinstance(other, ServiceModel): 106 | return self.pk == other.pk 107 | return False 108 | 109 | def full_clean(self, *args, **kwargs): 110 | pass 111 | 112 | def validate_unique(self, *args, **kwargs): 113 | pass 114 | 115 | def _get_unique_checks(self, *args, **kwargs): 116 | return ( 117 | [], 118 | [], ) 119 | 120 | def _get_pk_val(self): 121 | return None 122 | 123 | def save(self, *args, **kwargs): 124 | fields = [a.name for a in self._meta.get_fields()] 125 | data = {} 126 | for field in fields: 127 | data.update({field: getattr(self, field,None)}) 128 | result = self.objects.get_queryset().create(**data) 129 | for key in fields: 130 | setattr(self,key, getattr(result,key,None)) 131 | return self 132 | 133 | def __init__(self, **kwargs): 134 | self.id = None 135 | for key, value in kwargs.items(): 136 | setattr(self, key, value) 137 | 138 | 139 | 140 | def model_factory(service, name, base_class=None): 141 | BaseClass = base_class or object 142 | class SampleModel(ServiceModel,BaseClass): 143 | _service_api = service 144 | 145 | class Meta: 146 | app_label = name.lower() 147 | SampleModel.__name__ = name 148 | return SampleModel 149 | -------------------------------------------------------------------------------- /mservice_model/options.py: -------------------------------------------------------------------------------- 1 | from django.db.models.fields import (AutoField, CharField, FieldDoesNotExist, 2 | TextField) 3 | from django.db.models import ForeignKey 4 | from django.utils.functional import cached_property 5 | from django.utils.datastructures import ImmutableList, OrderedSet 6 | from django.apps import apps 7 | IMMUTABLE_WARNING = ( 8 | "The return type of '%s' should never be mutated. If you want to manipulate this list " 9 | "for your own use, make a copy first.") 10 | 11 | 12 | def make_immutable_fields_list(name, data): 13 | return ImmutableList(data, warning=IMMUTABLE_WARNING % name) 14 | 15 | 16 | class CachedPropertiesMixin(object): 17 | 18 | @cached_property 19 | def fields(self): 20 | """ 21 | Returns a list of all forward fields on the model and its parents, 22 | excluding ManyToManyFields. 23 | 24 | Private API intended only to be used by Django itself; get_fields() 25 | combined with filtering of field properties is the public API for 26 | obtaining this field list. 27 | """ 28 | # For legacy reasons, the fields property should only contain forward 29 | # fields that are not virtual or with a m2m cardinality. Therefore we 30 | # pass these three filters as filters to the generator. 31 | # The third lambda is a longwinded way of checking f.related_model - we don't 32 | # use that property directly because related_model is a cached property, 33 | # and all the models may not have been loaded yet; we don't want to cache 34 | # the string reference to the related_model. 35 | is_not_an_m2m_field = lambda f: not (f.is_relation and f.many_to_many) 36 | is_not_a_generic_relation = lambda f: not ( 37 | f.is_relation and f.one_to_many) 38 | is_not_a_generic_foreign_key = lambda f: not ( 39 | f.is_relation and f.many_to_one and not (hasattr(f.rel, 'to') and f.rel.to)) 40 | return make_immutable_fields_list( 41 | "fields", (f for f in self._get_fields(reverse=False) 42 | if is_not_an_m2m_field(f) and is_not_a_generic_relation( 43 | f) and is_not_a_generic_foreign_key(f))) 44 | 45 | @cached_property 46 | def concrete_fields(self): 47 | """ 48 | Returns a list of all concrete fields on the model and its parents. 49 | 50 | Private API intended only to be used by Django itself; get_fields() 51 | combined with filtering of field properties is the public API for 52 | obtaining this field list. 53 | """ 54 | try: 55 | return make_immutable_fields_list("concrete_fields", 56 | (f for f in self.fields 57 | if f.concrete)) 58 | except AttributeError: 59 | import ipdb 60 | ipdb.set_trace() 61 | 62 | @cached_property 63 | def local_concrete_fields(self): 64 | """ 65 | Returns a list of all concrete fields on the model. 66 | 67 | Private API intended only to be used by Django itself; get_fields() 68 | combined with filtering of field properties is the public API for 69 | obtaining this field list. 70 | """ 71 | return make_immutable_fields_list("local_concrete_fields", 72 | (f for f in self.local_fields 73 | if f.concrete)) 74 | 75 | # @raise_deprecation(suggested_alternative="get_fields()") 76 | def get_fields_with_model(self): 77 | return [self._map_model(f) for f in self.get_fields()] 78 | 79 | # @raise_deprecation(suggested_alternative="get_fields()") 80 | def get_concrete_fields_with_model(self): 81 | return [self._map_model(f) for f in self.concrete_fields] 82 | 83 | @cached_property 84 | def many_to_many(self): 85 | """ 86 | Returns a list of all many to many fields on the model and its parents. 87 | 88 | Private API intended only to be used by Django itself; get_fields() 89 | combined with filtering of field properties is the public API for 90 | obtaining this list. 91 | """ 92 | return make_immutable_fields_list( 93 | "many_to_many", (f for f in self._get_fields(reverse=False) 94 | if f.is_relation and f.many_to_many)) 95 | 96 | @cached_property 97 | def related_objects(self): 98 | """ 99 | Returns all related objects pointing to the current model. The related 100 | objects can come from a one-to-one, one-to-many, or many-to-many field 101 | relation type. 102 | 103 | Private API intended only to be used by Django itself; get_fields() 104 | combined with filtering of field properties is the public API for 105 | obtaining this field list. 106 | """ 107 | all_related_fields = self._get_fields( 108 | forward=False, reverse=True, include_hidden=True) 109 | return make_immutable_fields_list("related_objects", ( 110 | obj for obj in all_related_fields 111 | if not obj.hidden or obj.field.many_to_many)) 112 | 113 | 114 | class ServiceOptions(CachedPropertiesMixin): 115 | has_auto_field = True 116 | auto_created = False 117 | abstract = False 118 | swapped = False 119 | virtual_fields = [] 120 | apps = apps 121 | _service_pk_fields = 'id' 122 | related_fkey_lookups = [] 123 | default_related_name = None, 124 | 125 | def __init__(self, meta, **kwargs): 126 | self.meta = meta 127 | self._service_other_fields = {} 128 | self.app_label = kwargs.get('app_label', None) 129 | self._service_fields = kwargs.get('_service_fields', {}) 130 | model_name = kwargs.get('the_class', None) 131 | # import ipdb; ipdb.set_trace() 132 | if model_name: 133 | self.model_name = model_name.__name__ 134 | if not hasattr(self, 'verbose_name'): 135 | self.verbose_name = self.model_name 136 | self.verbose_name_plural = self.verbose_name + 's' 137 | # import ipdb; ipdb.set_trace() 138 | self.verbose_name_raw = self.model_name 139 | self.object_name = self.model_name.lower() 140 | # print(self.model_name) 141 | self.private_fields = [] 142 | 143 | def add_field(self, *args, **kwargs): 144 | pass 145 | 146 | def _bind(self): 147 | for field_name, field in self._service_fields.items(): 148 | setattr(self, field_name, field) 149 | field.set_attributes_from_name(field_name) 150 | self.pk = self._service_fields[self._service_pk_fields] 151 | self.additional_bind() 152 | 153 | def additional_bind(self): 154 | pass 155 | 156 | def get_fields(self, **kwargs): 157 | return self._get_fields() 158 | 159 | def _get_fields(self, reverse=True, forward=True): 160 | return tuple(field 161 | for field_name, field in sorted( 162 | list(self._service_fields.items()) + list( 163 | self._service_other_fields.items()))) 164 | 165 | def get_field(self, field_name): 166 | try: 167 | return self._service_fields[field_name] 168 | except KeyError: 169 | try: 170 | return self._service_other_fields[field_name] 171 | except KeyError: 172 | raise FieldDoesNotExist() 173 | 174 | @property 175 | def app_config(self): 176 | # Don't go through get_app_config to avoid triggering imports. 177 | return self.apps.app_configs.get(self.app_label) 178 | -------------------------------------------------------------------------------- /mservice_model/queryset.py: -------------------------------------------------------------------------------- 1 | class GmailQuery(object): 2 | select_related = False 3 | order_by = tuple() 4 | 5 | 6 | class ServiceQuerySet(object): 7 | """Generic queryset for services fetched over a remote api 8 | total_count is used to get the total counts of objects""" 9 | 10 | def using(self, db): 11 | return self 12 | 13 | def __init__(self, *args, **kwargs): 14 | self._cache = None 15 | self.total_count = 0 16 | self.ordered = True 17 | self.model = kwargs.pop('model') 18 | self.mailer = kwargs.pop('mailer', None) 19 | self.filter_query = kwargs.pop('filter_query', {}) 20 | self.query = GmailQuery() 21 | self.order_dict = () 22 | 23 | def order_by(self, *args, **kwargs): 24 | self.order_dict = args 25 | self._cache = None 26 | self._get_data() 27 | return self 28 | 29 | def aggregate(self, **kwargs): 30 | if self.date_range: 31 | return self.date_range 32 | return self.mailer.aggregate(**kwargs) 33 | 34 | def none(self, *args, **kwargs): 35 | cloned_query = self._clone() 36 | cloned_query.filter_query = {} 37 | return cloned_query 38 | 39 | def _variables(self): 40 | return dict(model=self.model, 41 | mailer=self.mailer, 42 | filter_query=self.filter_query) 43 | 44 | def _clone(self, *args, **kwargs): 45 | return self.__class__(**self._variables(), **kwargs) 46 | 47 | def first(self): 48 | return self[0] 49 | 50 | def last(self): 51 | return self[-1] 52 | 53 | def count(self): 54 | return len(self._get_data()) 55 | 56 | def get(self, *args, **kwargs): 57 | filter_args = self._get_filter_args(args, kwargs) 58 | if 'id' not in filter_args: 59 | raise Exception("No ID found in Thread GET") 60 | params = self._variables() 61 | params.update(filter_query={'id': filter_args['id']}) 62 | return ServiceQuerySet(**params)[0] 63 | 64 | def filter(self, *args, **kwargs): 65 | """Implement the filter function on the queryset""" 66 | filter_args = self._get_filter_args(args, kwargs) 67 | if filter_args: 68 | params = self._variables() 69 | params.update(filter_query=filter_args) 70 | return ServiceQuerySet(**params) 71 | return self 72 | 73 | def create(self, **kwargs): 74 | data = self.mailer.create(cls=self.model, **kwargs) 75 | self._cache = [ 76 | self._set_model_attrs(instance) for instance in [data] 77 | ] 78 | return self[0] 79 | 80 | def __getitem__(self, k): 81 | return self._get_data()[k] 82 | 83 | def __repr__(self): 84 | return repr(self._get_data()) 85 | 86 | def __iter__(self): 87 | return iter(self._get_data()) 88 | 89 | def all(self): 90 | return self._get_data() 91 | 92 | def _set_model_attrs(self, instance): 93 | instance._meta = self.model._meta 94 | instance._state = self.model._state 95 | return instance 96 | 97 | def _get_filter_args(self, args, kwargs): 98 | filter_args = kwargs if kwargs else {} 99 | if len(args) > 0: 100 | filter_args.update(dict(args[0].children)) 101 | return filter_args 102 | 103 | def __len__(self): 104 | return len(self._get_data()) 105 | 106 | def params_for_fetching_data(self, **kwargs): 107 | """parameters to be passed to _get_data function. should BufferError 108 | overidden by child classes""" 109 | return dict(filter_by=self.filter_query,order_by=self.order_dict,cls=self.model, **kwargs) 110 | 111 | def _get_data(self, **kwargs): 112 | """Get the query from the service api. 113 | register all the possible queries. in this cas 114 | field_query = ['id', 'to_contains']""" 115 | # import pdb; pdb.set_trace() 116 | if kwargs: 117 | self._cache = None 118 | if not self._cache: 119 | messages, self.total_count, self.date_range, self.last_id = self.mailer.get_data( 120 | **self.params_for_fetching_data(), **kwargs) 121 | self._cache = [ 122 | self._set_model_attrs(instance) for instance in messages 123 | ] 124 | # self._cache = map(self._set_model_attrs, all_threads) 125 | 126 | return self._cache 127 | 128 | def datetimes(self, field_name, kind, order='ASC', tzinfo=None): 129 | """ 130 | Returns a list of datetime objects representing all available 131 | datetimes for the given field_name, scoped to 'kind'. 132 | """ 133 | assert kind in ("year", "month", "day", "hour", "minute", "second"), \ 134 | "'kind' must be one of 'year', 'month', 'day', 'hour', 'minute' or 'second'." 135 | assert order in ('ASC', 'DESC'), \ 136 | "'order' must be either 'ASC' or 'DESC'." 137 | return self.mailer.datetimes(field_name, kind, order, self.filter_query) 138 | 139 | def distinct(self): 140 | return self 141 | 142 | def values_list(self, *args, **kwargs): 143 | return self.mailer.get_values_list(*args, **kwargs) -------------------------------------------------------------------------------- /mservice_model/tests/base.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/tests/base.py -------------------------------------------------------------------------------- /mservice_model/tests/response.py: -------------------------------------------------------------------------------- 1 | class MockRequest: 2 | 3 | def __init__(self, response, **kwargs): 4 | self.response = response 5 | self.overwrite = False 6 | if kwargs.get('overwrite'): 7 | self.overwrite = True 8 | self.status_code = kwargs.get('status_code', 200) 9 | 10 | @classmethod 11 | def raise_for_status(cls): 12 | pass 13 | 14 | def json(self): 15 | if self.overwrite: 16 | return self.response 17 | return {'data': self.response} 18 | 19 | 20 | option_response = { 21 | "actions": { 22 | "POST": { 23 | "id": { 24 | "type": "integer", 25 | "required": False, 26 | "read_only": True, 27 | "label": "ID" 28 | }, 29 | "title": { 30 | "type": "string", 31 | "required": True, 32 | "read_only": False, 33 | "label": "Title" 34 | }, 35 | "completed": { 36 | "type": "boolean", 37 | "required": False, 38 | "read_only": False, 39 | "label": "Completed" 40 | }, 41 | "owner": { 42 | "type": "field", 43 | "required": False, 44 | "read_only": False, 45 | "label": "Owner" 46 | } 47 | } 48 | } 49 | } 50 | 51 | get_all_response = { 52 | "count": 4, 53 | "next": None, 54 | "previous": None, 55 | "last_id": 4, 56 | "first_id": 1, 57 | "results": [ 58 | { 59 | "id": 1, 60 | "title": "Buckle my shoe", 61 | "completed": False, 62 | "owner": None 63 | }, 64 | { 65 | "id": 2, 66 | "title": "Knock at the door", 67 | "completed": False, 68 | "owner": None 69 | }, 70 | { 71 | "id": 3, 72 | "title": "Buy the new Ram", 73 | "completed": True, 74 | "owner": None 75 | }, 76 | { 77 | "id": 4, 78 | "title": "Implement restful Admin", 79 | "completed": False, 80 | "owner": None 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /mservice_model/tests/test_admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/tests/test_admin.py -------------------------------------------------------------------------------- /mservice_model/tests/test_api.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from mservice_model.api import ServiceApi,Bunch 3 | from unittest import mock 4 | from .response import MockRequest, option_response, get_all_response 5 | from .test_models import BaseTestCase 6 | 7 | 8 | class ServiceApiTestCase(BaseTestCase): 9 | def setUp(self): 10 | 11 | super().setUp() 12 | self.mock_options.return_value = MockRequest(option_response,overwrite=True) 13 | 14 | def test_correct_fields_are_returned(self): 15 | instance = ServiceApi(self.base_url) 16 | self.assertEqual(instance.instance.fields, option_response['actions']['POST']) 17 | 18 | def test_get_all_objects(self): 19 | self.mock_get.return_value = MockRequest(get_all_response, overwrite=True) 20 | instance = ServiceApi(self.base_url) 21 | response = instance.instance.get_all_objects({},None,Bunch) 22 | self.assertEqual(response[1], 4) 23 | self.assertEqual(response[3], 4) -------------------------------------------------------------------------------- /mservice_model/tests/test_manager.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/tests/test_manager.py -------------------------------------------------------------------------------- /mservice_model/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from mservice_model.models import ServiceModel, model_factory 4 | from mservice_model.api import ServiceApi 5 | from .response import MockRequest, option_response, get_all_response 6 | 7 | class BaseTestCase(unittest.TestCase): 8 | def setUp(self): 9 | self.base_url = 'http://localhost:2000/todos/' 10 | self.service = ServiceApi(self.base_url) 11 | self.patcher = mock.patch('mservice_model.api.requests') 12 | self.mock_request = self.patcher.start() 13 | self.mock_get = self.mock_request.get 14 | self.mock_post = self.mock_request.post 15 | self.mock_put = self.mock_request.put 16 | self.mock_options = self.mock_request.options 17 | self.mock_delete = self.mock_request.delete 18 | self.mock_options.return_value = MockRequest(option_response,overwrite=True) 19 | 20 | def tearDown(self): 21 | self.patcher.stop() 22 | 23 | 24 | class SampleModelTestCase(BaseTestCase): 25 | 26 | def test_can_create_instance_of_class(self): 27 | SampleModel = model_factory(self.service, name="Sample") 28 | self.assertEqual(SampleModel.Meta.app_label, 'sample') 29 | 30 | def test_mixin_class_methods_can_be_retrieved(self): 31 | class Mixin(object): 32 | def name(self): 33 | return "Biola" 34 | SampleModel = model_factory(self.service, "Sample", Mixin) 35 | self.assertEqual(SampleModel.name(""), "Biola") 36 | 37 | def test_field_types(self): 38 | SampleModel = model_factory(self.service, name="Sample") 39 | service_fields = SampleModel._meta._service_fields 40 | self.assertEqual(service_fields['id'].__class__.__name__, 'AutoField') 41 | self.assertEqual(service_fields['completed'].__class__.__name__, 'BooleanField') 42 | self.assertEqual(service_fields['title'].__class__.__name__, 'CharField') 43 | 44 | def test_count_method_in_queryset_returns_correct_value(self): 45 | self.mock_get.return_value = MockRequest(get_all_response, overwrite=True) 46 | SampleModel = model_factory(self.service, name="Sample") 47 | self.assertEqual(SampleModel.objects.count(), 4) 48 | 49 | def test_all_methods_returns_all_queryset_result(self): 50 | self.mock_get.return_value = MockRequest(get_all_response, overwrite=True) 51 | SampleModel = model_factory(self.service, name="Sample") 52 | result = SampleModel.objects.all() 53 | self.assertEqual(result[0].completed, False) 54 | self.assertEqual(result[0].id, 1) 55 | self.assertIsNone(result[0].owner) 56 | 57 | def test_get_method_returns_single_object_based_on_query(self): 58 | self.mock_get.return_value = MockRequest(get_all_response['results'][0], overwrite=True) 59 | SampleModel = model_factory(self.service, name="Sample") 60 | result = SampleModel.objects.get(id=1) 61 | self.mock_get.assert_called_once_with( 62 | "{}{}/".format(self.base_url, 1), 63 | ) 64 | self.assertEqual(result.id, 1) 65 | 66 | def test_can_add_an_item(self): 67 | self.mock_post.return_value = MockRequest({ 68 | "title":"Hello World","completed":True,"id":72,"owner":None 69 | },overwrite=True) 70 | SampleModel = model_factory(self.service, name="Sample") 71 | result = SampleModel.objects.create(title="Hello world",completed=True) 72 | self.assertEqual(result.pk, 72) 73 | self.assertEqual(result.owner,None) 74 | self.assertEqual(result.title, "Hello World") 75 | self.assertTrue(result.completed) 76 | 77 | 78 | def test_can_update_an_item(self): 79 | self.mock_get.return_value = MockRequest( 80 | get_all_response['results'][0], overwrite=True) 81 | self.mock_put.return_value = MockRequest( 82 | { 83 | "id": 1, 84 | "title": "Buckle my shoe", 85 | "completed": True, 86 | "owner": None 87 | },overwrite=True 88 | ) 89 | SampleModel = model_factory(self.service, name="Sample") 90 | result = SampleModel.objects.get(id=1) 91 | result.completed = True 92 | result.save() 93 | self.assertEqual(result.id, 1) 94 | self.assertTrue(result.completed) 95 | self.mock_put.assert_called_once_with( 96 | "{}{}/".format(self.base_url, 1), json={'completed': True, 'owner': None, 'title': 'Buckle my shoe'} 97 | ) 98 | 99 | def test_calling_save_on_a_new_record_does_not_raise_error(self): 100 | self.mock_post.return_value = MockRequest({ 101 | "title": "Aloiba", "completed": False, "id": 72, "owner": None 102 | }, overwrite=True) 103 | 104 | SampleModel = model_factory(self.service, name="Sample") 105 | record = SampleModel() 106 | record.title = "Aloiba" 107 | record.completed = False 108 | self.assertEqual(record.id, None) 109 | record.save() 110 | self.mock_post.assert_called_once_with( 111 | "{}".format(self.base_url),json={'title':"Aloiba",'completed':False, 'owner':None} 112 | ) 113 | self.assertEqual(record.title,"Aloiba") 114 | self.assertFalse(record.completed) 115 | self.assertEqual(record.id,72) 116 | 117 | 118 | -------------------------------------------------------------------------------- /mservice_model/tests/test_queryset.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/tests/test_queryset.py -------------------------------------------------------------------------------- /mservice_model/tests/test_views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gbozee/django-rest-admin/2d13f2411eddde18a964c6024d0cb79d521c8996/mservice_model/tests/test_views.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.0 2 | simplejson==3.10.0 3 | django_extensions 4 | -e git+https://github.com/gbozee/django-rest-admin.git@master#egg=mservice_model -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='mservice_model', 12 | version='0.1', 13 | packages=find_packages(), 14 | include_package_data=True, 15 | license='MIT License', # example license 16 | description='Getting the django admin to work with data from any source', 17 | long_description=README, 18 | url='https://www.example.com/', 19 | author='Biola Oyeniyi', 20 | author_email='gbozee@gmail.com', 21 | install_requires=[ 22 | 'maya','requests','simplejson' 23 | ], 24 | classifiers=[ 25 | 'Environment :: Web Environment', 26 | 'Framework :: Django', 27 | 'Framework :: Django :: X.Y', # replace "X.Y" as appropriate 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: MIT License', # example license 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | # Replace these appropriately if you are stuck on Python 2. 33 | 'Programming Language :: Python :: 3', 34 | 'Topic :: Internet :: WWW/HTTP', 35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 36 | ], 37 | ) 38 | --------------------------------------------------------------------------------