├── examples ├── __init__.py ├── twitter_roa │ ├── __init__.py │ ├── urls.py │ ├── manage.py │ ├── admin.py │ ├── models.py │ ├── settings.py │ └── serializers.py ├── django_roa_client │ ├── __init__.py │ ├── views.py │ ├── urls.py │ ├── forms.py │ ├── manage.py │ ├── admin.py │ ├── serializers.py │ ├── settings.py │ ├── models.py │ └── tests.py ├── django_rest_framework │ ├── __init__.py │ ├── backend │ │ ├── backend │ │ │ ├── __init__.py │ │ │ ├── api │ │ │ │ ├── urls.py │ │ │ │ ├── __init__.py │ │ │ │ ├── models.py │ │ │ │ ├── views.py │ │ │ │ ├── serializers.py │ │ │ │ ├── fixtures │ │ │ │ │ └── initial_data.json │ │ │ │ └── mixins.py │ │ │ ├── wsgi.py │ │ │ ├── urls.py │ │ │ └── settings.py │ │ ├── load.sh │ │ └── manage.py │ ├── frontend │ │ ├── frontend │ │ │ ├── __init__.py │ │ │ ├── urls.py │ │ │ ├── wsgi.py │ │ │ ├── serializers.py │ │ │ ├── models.py │ │ │ ├── settings.py │ │ │ └── tests.py │ │ └── manage.py │ └── README.md └── django_roa_server │ ├── __init__.py │ ├── fixtures │ └── initial_data.json │ ├── manage.py │ ├── emitters.py │ ├── settings.py │ ├── models.py │ ├── urls.py │ └── handlers.py ├── django_roa ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── inspectresources.py ├── remoteauth │ ├── __init__.py │ ├── signals.py │ ├── serializers.py │ ├── backends.py │ └── models.py ├── models.py ├── db │ ├── middleware.py │ ├── admin.py │ ├── managers.py │ ├── __init__.py │ ├── exceptions.py │ ├── query.py │ └── models.py └── __init__.py ├── req └── tests.txt ├── .hgignore ├── .gitignore ├── THANKS ├── .travis.yml ├── .hgtags ├── setup.py ├── LICENSE ├── templates └── admin │ └── index.html ├── README.rst ├── CHANGELOG └── ez_setup.py /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_roa/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_roa/remoteauth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/twitter_roa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_roa_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_roa/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_rest_framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/urls.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_roa_server/__init__.py: -------------------------------------------------------------------------------- 1 | import emitters 2 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_roa/models.py: -------------------------------------------------------------------------------- 1 | # Otherwise Django do not consider this app for tests 2 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/load.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -f db.sqlite3 3 | python manage.py syncdb --noinput 4 | python manage.py runserver 5 | -------------------------------------------------------------------------------- /req/tests.txt: -------------------------------------------------------------------------------- 1 | django-filter==0.9.2 2 | django-piston==0.2.3 3 | djangorestframework==2.4.4 4 | http-parser==0.8.3 5 | restkit==4.2.2 6 | simplejson==3.6.5 7 | socketpool==0.5.3 8 | wsgiref==0.1.2 9 | -------------------------------------------------------------------------------- /django_roa/db/middleware.py: -------------------------------------------------------------------------------- 1 | from django_roa.db import set_roa_headers 2 | 3 | 4 | class ROAMiddleware(object): 5 | def process_request(self, request): 6 | # Set headers: 7 | set_roa_headers(request) 8 | -------------------------------------------------------------------------------- /examples/twitter_roa/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url, include 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | url(r'^admin/', include(admin.site.urls)), 8 | ) 9 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.pyc 4 | *~ 5 | *.db 6 | *.tmp 7 | *.DS_Store 8 | *.txt 9 | *.sqlite 10 | *.svn 11 | *.orig 12 | *.old 13 | *.swp 14 | settings.py 15 | manage.py 16 | django 17 | dist/* 18 | build/ 19 | django_roa.egg-info/PKG-INFO 20 | -------------------------------------------------------------------------------- /examples/django_roa_client/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from django_roa_client.forms import RemotePageWithRelationsForm 4 | 5 | def home(request): 6 | form = RemotePageWithRelationsForm() 7 | return HttpResponse(form.as_ul()) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | settings_local.py 5 | local_settings.py 6 | .idea/ 7 | *.sql 8 | *.db 9 | .cache 10 | /static/ 11 | .vagrant/ 12 | examples/django_rest_framework/backend/admin 13 | examples/django_rest_framework/backend/db.sqlite3 14 | -------------------------------------------------------------------------------- /examples/django_roa_client/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url, include 2 | from django.contrib import admin 3 | 4 | from django_roa_client.views import home 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | url(r'^admin/', include(admin.site.urls)), 10 | url(r'^$', home), 11 | ) 12 | -------------------------------------------------------------------------------- /django_roa/remoteauth/signals.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.signals import user_logged_out 2 | from django.dispatch import receiver 3 | 4 | 5 | @receiver(user_logged_out) 6 | def logged_out_handler(sender, **kwargs): 7 | """ 8 | User logout signal 9 | """ 10 | from .backends import reset_api_auth 11 | reset_api_auth() 12 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/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", "backend.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | # Examples: 8 | # url(r'^$', 'frontend.views.home', name='home'), 9 | # url(r'^blog/', include('blog.urls')), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | ) 13 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | Thank you guys! 2 | =============== 3 | 4 | My special thanks to Benoît Chesneau and Jesper Noehr for restkit and piston 5 | (respectively), their reactivity and thoughts on the project. 6 | 7 | Martin Skala made an impressive work on maintenance and improvements of the 8 | library in early 2012. This is highly rewarding and motivating as an 9 | Open-Source project. 10 | 11 | It has to be said. 12 | -------------------------------------------------------------------------------- /django_roa/db/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.options import ModelAdmin as DjangoModelAdmin 2 | from django.contrib.admin.options import StackedInline as DjangoStackedInline, \ 3 | TabularInline as DjangoTabularInline 4 | 5 | class ROAModelAdmin(DjangoModelAdmin): 6 | pass 7 | 8 | class ROAStackedInline(DjangoStackedInline): 9 | pass 10 | 11 | class ROATabularInline(DjangoTabularInline): 12 | pass 13 | -------------------------------------------------------------------------------- /django_roa/remoteauth/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Group, Permission, User 3 | 4 | 5 | class PermissionSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Permission 8 | 9 | 10 | class GroupSerializer(serializers.ModelSerializer): 11 | class Meta: 12 | model = Group 13 | 14 | 15 | class UserSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = User 18 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend 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.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for frontend 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.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "frontend.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /examples/django_roa_client/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django_roa_client.models import RemotePage, RemotePageWithRelations 3 | 4 | class TestForm(forms.Form): 5 | test_field = forms.CharField() 6 | remote_page = forms.ModelChoiceField(queryset=RemotePage.objects.all()) 7 | 8 | 9 | class RemotePageForm(forms.ModelForm): 10 | class Meta: 11 | model = RemotePage 12 | 13 | 14 | class RemotePageWithRelationsForm(forms.ModelForm): 15 | class Meta: 16 | model = RemotePageWithRelations 17 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Add django_roa path: 6 | django_roa_path = os.path.abspath(os.path.join( 7 | os.path.dirname(os.path.abspath(__file__)), 8 | '../../../' 9 | )) 10 | sys.path.insert(0, django_roa_path) 11 | 12 | 13 | if __name__ == "__main__": 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "frontend.settings") 15 | 16 | from django.core.management import execute_from_command_line 17 | 18 | execute_from_command_line(sys.argv) 19 | -------------------------------------------------------------------------------- /examples/django_rest_framework/README.md: -------------------------------------------------------------------------------- 1 | How to run Django Rest Framework example tests: 2 | =============================================== 3 | 4 | Launch two terminals 5 | 6 | Backend part 7 | ------------ 8 | 9 | In first terminal: 10 | 11 | $ cd examples/django_rest_framework/backend/ 12 | $ bash load.sh 13 | 14 | Frontend part 15 | ------------- 16 | 17 | In second terminal: 18 | 19 | $ cd examples/django_rest_framework/frontend/ 20 | $ python manage.py test frontend 21 | 22 | After each test command, please re-run backend load command. 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/django_roa_server/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "roa_user", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2008-12-27 10:03:16", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "sha1$436ec$7cd7f1c7057b3d4017267b53aabd35e3a29a80d6", 16 | "email": "roa_user@example.com", 17 | "date_joined": "2008-12-27 06:28:06" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /django_roa/management/commands/inspectresources.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | class Command(NoArgsCommand): 4 | help = "Introspects the models and outputs a representation of resources." 5 | 6 | requires_model_validation = False 7 | 8 | def handle_noargs(self, **options): 9 | from django.db.models import get_models 10 | for model in get_models(): 11 | if hasattr(model.objects, 'is_roa_manager'): 12 | print '%s (%s)' % (model.__name__, model.get_resource_url_list()) 13 | for field in model._meta.fields: 14 | print ' %s (%s)' % (field.attname, field.__class__.__name__) -------------------------------------------------------------------------------- /examples/twitter_roa/manage.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.insert(0, os.path.split(os.path.split(os.getcwd())[0])[0]) 3 | 4 | from django.core.management import execute_manager 5 | 6 | try: 7 | import settings # Assumed to be in the same directory. 8 | except ImportError: 9 | import sys 10 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /examples/django_roa_client/manage.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.insert(0, os.path.split(os.path.split(os.getcwd())[0])[0]) 3 | 4 | from django.core.management import execute_manager 5 | 6 | try: 7 | import settings # Assumed to be in the same directory. 8 | except ImportError: 9 | import sys 10 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /examples/django_roa_server/manage.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.insert(0, os.path.split(os.path.split(os.getcwd())[0])[0]) 3 | 4 | from django.core.management import execute_manager 5 | 6 | try: 7 | import settings # Assumed to be in the same directory. 8 | except ImportError: 9 | import sys 10 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | env: 8 | - DJANGO_VERSION=1.4.15 9 | - DJANGO_VERSION=1.5.10 10 | - DJANGO_VERSION=1.6.7 11 | - DJANGO_VERSION=1.7 12 | 13 | matrix: 14 | exclude: 15 | - python: "2.6" 16 | env: DJANGO_VERSION=1.7 17 | 18 | install: 19 | - pip install --download-cache=".pip_download_cache" -q Django==$DJANGO_VERSION 20 | - pip install --download-cache=".pip_download_cache" -r req/tests.txt 21 | - python setup.py -q install 22 | 23 | script: 24 | - cd examples/django_rest_framework/backend/ 25 | - bash load.sh & 26 | - sleep 4 # wait for runserver init 27 | - cd ../frontend/ 28 | - python manage.py test frontend 29 | -------------------------------------------------------------------------------- /django_roa/db/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models.manager import Manager 2 | 3 | from django_roa.db.query import RemoteQuerySet 4 | 5 | 6 | class ROAManager(Manager): 7 | """ 8 | Manager which access remote resources. 9 | """ 10 | use_for_related_fields = True 11 | is_roa_manager = True # ugly but useful because isinstance is evil 12 | 13 | def get_query_set(self): 14 | """ 15 | Returns a QuerySet which access remote resources. 16 | """ 17 | return RemoteQuerySet(self.model) 18 | 19 | def get_queryset(self): 20 | return RemoteQuerySet(self.model) 21 | 22 | def search(self, *args, **kwargs): 23 | return self.get_queryset().search(*args, **kwargs) 24 | -------------------------------------------------------------------------------- /examples/django_roa_client/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_roa import ModelAdmin 3 | from models import RemotePage, RemotePageWithManyFields, RemotePageWithCustomSlug 4 | 5 | class RemotePageAdmin(ModelAdmin): 6 | list_per_page = 2 7 | 8 | 9 | class RemotePageWithManyFieldsAdmin(ModelAdmin): 10 | list_display = ('__unicode__', 'date_field', 'char_field') 11 | list_filter = ('date_field',) 12 | 13 | 14 | class RemotePageWithCustomSlugAdmin(ModelAdmin): 15 | prepopulated_fields = {"slug": ("title",)} 16 | 17 | admin.site.register(RemotePage, RemotePageAdmin) 18 | admin.site.register(RemotePageWithManyFields, RemotePageWithManyFieldsAdmin) 19 | admin.site.register(RemotePageWithCustomSlug, RemotePageWithCustomSlugAdmin) 20 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Declare a model with all relationships 5 | 6 | 7 | class Account(models.Model): 8 | email = models.CharField(max_length=30) 9 | 10 | 11 | class Reporter(models.Model): 12 | account = models.OneToOneField(Account, related_name='reporter') 13 | first_name = models.CharField(max_length=30) 14 | last_name = models.CharField(max_length=30) 15 | 16 | 17 | class Article(models.Model): 18 | headline = models.CharField(max_length=100) 19 | pub_date = models.DateField() 20 | reporter = models.ForeignKey(Reporter, related_name='articles') 21 | 22 | 23 | class Tag(models.Model): 24 | label = models.CharField(max_length=30) 25 | articles = models.ManyToManyField(Article, related_name='tags') 26 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 81b112d277483c1f751debd70ba32febede2f865 0.1 2 | 195a60205a95afeca6f80432d701df8f219f5591 0.2 3 | efb28c5ea1f4791d88db6b71341ccf04bacf12b9 0.3 4 | a1d85028f3c09a39c2f4872fbac01d73388496ad 0.4 5 | 062ea60a4ac8f0c183522687de7888d7a3ba5c3a 0.5 6 | aded008eafcd9beb3732f00229514463129f7978 0.6 7 | 3472cf46793f94c079cea93298dda0d5baafa284 0.7 8 | 44ca10ae1a4970d2c2a57003f065f78514036486 0.8 9 | 382e0d19811c9398f6f6d1778164efc523dbd94f 0.9 10 | 7bdc4baad643054e20b04508da2948dcaf762b32 1.0 11 | f67107509ea1e070ece1dcf880c42d863e1789c6 1.1 12 | d6fa2e4e420bc1244103edddeeafde68c9b4e96d 1.2 13 | 284eed2d8a4be775bc463bdbdbb98ae5763130b7 1.3 14 | 3481b645c0579786880cbe26ad17a39ad9640dce 1.4 15 | 7eff422cf80858a22ce70e6b011713cd4a21f25a 1.5 16 | 903692084a61698bc90ba5dc7219201683de7dea 1.6 17 | e350e16c436456d13afe872bcdffb0d38848986b 1.7 18 | -------------------------------------------------------------------------------- /examples/twitter_roa/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_roa import ModelAdmin 3 | from models import Tweet, User 4 | 5 | class TweetAdmin(ModelAdmin): 6 | list_display = ('id', 'text', 'get_source_for_admin', 'get_user_link') 7 | 8 | def get_source_for_admin(self, item): 9 | return item.source 10 | get_source_for_admin.short_description = u'Source' 11 | get_source_for_admin.allow_tags = True 12 | 13 | def get_user_link(self, item): 14 | url = '../user/%s/' % (item.user.id) 15 | return '%s' % (url, item.user) 16 | get_user_link.short_description = u'User' 17 | get_user_link.allow_tags = True 18 | 19 | admin.site.register(Tweet, TweetAdmin) 20 | 21 | class UserAdmin(ModelAdmin): 22 | list_display = ('screen_name',) 23 | 24 | admin.site.register(User, UserAdmin) 25 | -------------------------------------------------------------------------------- /django_roa/db/__init__.py: -------------------------------------------------------------------------------- 1 | from threading import local 2 | from django.conf import settings 3 | 4 | 5 | ROA_SESSION_HEADERS_KEY = 'roa_session_headers_key' 6 | 7 | 8 | # Current thread access token: 9 | _roa_headers = local() 10 | 11 | 12 | def set_roa_headers(request, headers=None): 13 | 14 | if headers: 15 | request.session[ROA_SESSION_HEADERS_KEY] = headers 16 | elif ROA_SESSION_HEADERS_KEY in request.session: 17 | headers = request.session[ROA_SESSION_HEADERS_KEY] 18 | else: 19 | headers = {} 20 | 21 | # Save it into current thread 22 | _roa_headers.value = headers 23 | 24 | 25 | def get_roa_headers(): 26 | headers = getattr(settings, 'ROA_HEADERS', {}).copy() 27 | thread_headers = getattr(_roa_headers, 'value', {}) 28 | headers.update(thread_headers) 29 | return headers 30 | 31 | 32 | def reset_roa_headers(): 33 | if hasattr(_roa_headers, 'value'): 34 | del _roa_headers.value 35 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | from .api.views import AccountViewSet, ReporterViewSet, ArticleViewSet, \ 3 | TagViewSet, ReversedArticleViewSet, ReversedReporterViewSet, \ 4 | ReversedTagViewSet, ReversedAccountViewSet 5 | 6 | 7 | # API 8 | router = routers.DefaultRouter() 9 | router.register(r'accounts', AccountViewSet, base_name='account') 10 | router.register(r'reporters', ReporterViewSet, base_name='reporter') 11 | router.register(r'articles', ArticleViewSet, base_name='article') 12 | router.register(r'tags', TagViewSet, base_name='tag') 13 | router.register(r'reversed/tags', ReversedTagViewSet, base_name='reversedtag') 14 | router.register(r'reversed/articles', ReversedArticleViewSet, base_name='reversedarticle') 15 | router.register(r'reversed/reporters', ReversedReporterViewSet, base_name='reversedreporter') 16 | router.register(r'reversed/accounts', ReversedAccountViewSet, base_name='reversedaccount') 17 | 18 | urlpatterns = router.urls 19 | -------------------------------------------------------------------------------- /examples/django_roa_server/emitters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | from piston.emitters import Emitter, DjangoEmitter 6 | 7 | logger = logging.getLogger("django_roa_server") 8 | 9 | DEFAULT_CHARSET = getattr(settings, 'DEFAULT_CHARSET', 'utf-8') 10 | 11 | class ROADjangoEmitter(DjangoEmitter): 12 | """ 13 | ROA Django XML emitter. 14 | 15 | Just log the response with logging module. 16 | """ 17 | def render(self, request): 18 | response = super(ROADjangoEmitter, self).render(request, 'xml') 19 | logger.debug(u"Response:\n%s" % str(response).decode(DEFAULT_CHARSET)) 20 | return response 21 | 22 | Emitter.register('django', ROADjangoEmitter, 'application/xml; charset=utf-8') 23 | 24 | class CustomDjangoEmitter(DjangoEmitter): 25 | """ 26 | Custom Django XML emitter. 27 | 28 | Use a custom serializer. 29 | """ 30 | def render(self, request): 31 | response = super(CustomDjangoEmitter, self).render(request, 'custom') 32 | logger.debug(u"Response:\n%s" % response.decode(DEFAULT_CHARSET)) 33 | return response 34 | 35 | Emitter.register('custom', CustomDjangoEmitter, 'application/xml; charset=utf-8') 36 | -------------------------------------------------------------------------------- /examples/django_roa_client/serializers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.xmlutils import SimplerXMLGenerator 3 | 4 | from django.core.serializers.xml_serializer import Serializer as XMLSerializer, \ 5 | Deserializer as XMLDeserializer 6 | 7 | DEFAULT_CHARSET = getattr(settings, 'DEFAULT_CHARSET', 'utf-8') 8 | 9 | class Serializer(XMLSerializer): 10 | """ 11 | A test serializer which removes ``django-objects`` xml tag from default 12 | Django's xml serializer, adapt it to your own usage. 13 | """ 14 | 15 | def start_serialization(self): 16 | """ 17 | Start serialization -- open the XML document and the root element. 18 | """ 19 | self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", DEFAULT_CHARSET)) 20 | self.xml.startDocument() 21 | self.xml.startElement("django-test", {"version" : "1.0"}) 22 | 23 | def end_serialization(self): 24 | """ 25 | End serialization -- end the document. 26 | """ 27 | self.indent(0) 28 | self.xml.endElement("django-test") 29 | self.xml.endDocument() 30 | 31 | 32 | class Deserializer(XMLDeserializer): 33 | pass 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | try: 5 | from setuptools import setup, find_packages 6 | except ImportError: 7 | import ez_setup 8 | ez_setup.use_setuptools() 9 | from setuptools import setup, find_packages 10 | 11 | setup( 12 | name='django-roa', 13 | version='1.8.1', 14 | url='https://github.com/charles-vdulac/django-roa', 15 | download_url='https://github.com/charles-vdulac/django-roa/archive/master.zip', 16 | license='BSD', 17 | description="Turn your models into remote resources that you can access through Django's ORM.", 18 | author='David Larlet', 19 | author_email='david@larlet.fr', 20 | packages=find_packages(), 21 | include_package_data=True, 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: BSD License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Topic :: Internet :: WWW/HTTP', 31 | ], 32 | install_requires=[ 33 | 'Django', 34 | 'restkit', 35 | 'wsgiref', 36 | 'simplejson', 37 | 'djangorestframework' 38 | ], 39 | tests_require={ 40 | 'Piston-tests': ['django-piston'], 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /django_roa/remoteauth/backends.py: -------------------------------------------------------------------------------- 1 | from django_roa.remoteauth.models import User 2 | from django.contrib.auth.backends import ModelBackend 3 | 4 | 5 | class RemoteUserModelBackend(ModelBackend): 6 | """ 7 | Authenticates against django_roa.remoteauth.models.RemoteUser. 8 | """ 9 | def authenticate(self, username=None, password=None, **kwargs): 10 | try: 11 | user = User.objects.get(username=username) 12 | if user.check_password(password): 13 | return user 14 | except User.DoesNotExist: 15 | return None 16 | 17 | def get_group_permissions(self, user_obj, obj=None): 18 | """ 19 | Returns a set of permission strings that this user has through his/her 20 | groups. 21 | """ 22 | if not hasattr(user_obj, '_group_perm_cache'): 23 | # TODO: improve performances 24 | permissions = [u"%s.%s" % (p.content_type.app_label, p.codename) \ 25 | for group in user_obj.groups.all() \ 26 | for p in group.permissions.all()] 27 | user_obj._group_perm_cache = permissions 28 | return user_obj._group_perm_cache 29 | 30 | def get_user(self, user_id): 31 | try: 32 | return User.objects.get(pk=user_id) 33 | except User.DoesNotExist: 34 | return None 35 | -------------------------------------------------------------------------------- /examples/twitter_roa/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_roa import Model, Manager 4 | from django_roa.db.query import RemoteQuerySet 5 | 6 | class User(Model): 7 | name = models.CharField(max_length=255) 8 | screen_name = models.CharField(max_length=255) 9 | description = models.TextField() 10 | 11 | def __unicode__(self): 12 | return u'%s (%s)' % (self.name, self.screen_name) 13 | 14 | @staticmethod 15 | def get_resource_url_list(): 16 | return u'http://api.twitter.com/1/users/lookup.json?screen_name=twitterapi,twitter,twittersearch,twittermedia,twittermobile' 17 | 18 | def get_resource_url_count(self): 19 | return User.get_resource_url_list() 20 | 21 | 22 | class FakeCountRemoteQuerySet(RemoteQuerySet): 23 | def count(self): 24 | """ 25 | Because trying to count the whole number of tweets is stupid. 26 | """ 27 | return 20 28 | 29 | 30 | class TweetManager(Manager): 31 | def get_query_set(self): 32 | return FakeCountRemoteQuerySet(self.model) 33 | 34 | 35 | class Tweet(Model): 36 | text = models.TextField() 37 | source = models.CharField(max_length=50) 38 | user = models.ForeignKey(User) 39 | 40 | objects = TweetManager() 41 | 42 | def __unicode__(self): 43 | return u'%s (%s)' % (self.text, self.id) 44 | 45 | @staticmethod 46 | def get_resource_url_list(): 47 | return u'http://api.twitter.com/1/statuses/public_timeline.json' 48 | -------------------------------------------------------------------------------- /django_roa/__init__.py: -------------------------------------------------------------------------------- 1 | # Depends on settings for flexibility 2 | from django.conf import settings 3 | from django.db.models import signals, Model as DjangoModel 4 | from django.db.models.manager import Manager as DjangoManager 5 | from django.contrib.admin.options import ModelAdmin as DjangoModelAdmin 6 | from django.contrib.admin.options import StackedInline as DjangoStackedInline, \ 7 | TabularInline as DjangoTabularInline 8 | 9 | from django_roa.db.models import ROAModel 10 | from django_roa.db.managers import ROAManager 11 | from django_roa.db.admin import ROAModelAdmin, ROAStackedInline, ROATabularInline 12 | 13 | ROA_MODELS = getattr(settings, "ROA_MODELS", False) 14 | Model = ROA_MODELS and ROAModel or DjangoModel 15 | Manager = ROA_MODELS and ROAManager or DjangoManager 16 | ModelAdmin = ROA_MODELS and ROAModelAdmin or DjangoModelAdmin 17 | StackedInline = ROA_MODELS and ROAStackedInline or DjangoStackedInline 18 | TabularInline = ROA_MODELS and ROATabularInline or DjangoTabularInline 19 | 20 | def ensure_roa_manager(sender, **kwargs): 21 | cls = sender 22 | manager = getattr(cls, '_default_manager', None) 23 | if (not manager and not cls._meta.abstract) \ 24 | or (ROA_MODELS and hasattr(cls, 'get_resource_url_list') \ 25 | and not hasattr(manager, 'is_roa_manager')): 26 | # Create the default manager, if needed. 27 | cls.add_to_class('objects', Manager()) 28 | cls.add_to_class('_default_manager', Manager()) 29 | 30 | signals.class_prepared.connect(ensure_roa_manager) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, David Larlet, http://larlet.com 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Welldev, Webrise nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ''AS IS'' AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/views.py: -------------------------------------------------------------------------------- 1 | from .mixins import ModelViewSet 2 | from .models import Account, Reporter, Article, Tag 3 | from .serializers import AccountSerializer, ReporterSerializer, ArticleSerializer, \ 4 | TagSerializer, ReversedReporterSerializer, ReversedArticleSerializer, \ 5 | ReversedTagSerializer, ReversedAccountSerializer 6 | 7 | 8 | # Filters and permissions are declared in settings 9 | 10 | 11 | class AccountViewSet(ModelViewSet): 12 | queryset = Account.objects.all() 13 | serializer_class = AccountSerializer 14 | 15 | 16 | class ReporterViewSet(ModelViewSet): 17 | queryset = Reporter.objects.all() 18 | serializer_class = ReporterSerializer 19 | 20 | 21 | class ArticleViewSet(ModelViewSet): 22 | queryset = Article.objects.all() 23 | serializer_class = ArticleSerializer 24 | 25 | 26 | class TagViewSet(ModelViewSet): 27 | queryset = Tag.objects.all() 28 | serializer_class = TagSerializer 29 | # Do not paginate the tags, for unpaginated testing 30 | paginate_by = None 31 | 32 | 33 | # Reversed 34 | 35 | 36 | class ReversedTagViewSet(ModelViewSet): 37 | queryset = Tag.objects.all() 38 | serializer_class = ReversedTagSerializer 39 | 40 | 41 | class ReversedArticleViewSet(ModelViewSet): 42 | queryset = Article.objects.all() 43 | serializer_class = ReversedArticleSerializer 44 | 45 | 46 | class ReversedReporterViewSet(ModelViewSet): 47 | queryset = Reporter.objects.all() 48 | serializer_class = ReversedReporterSerializer 49 | 50 | 51 | class ReversedAccountViewSet(ModelViewSet): 52 | queryset = Account.objects.all() 53 | serializer_class = ReversedAccountSerializer 54 | -------------------------------------------------------------------------------- /examples/django_roa_server/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | ROOT_PATH = os.path.dirname(__file__) 3 | 4 | TEMPLATE_DEBUG = DEBUG = True 5 | MANAGERS = ADMINS = () 6 | DATABASE_ENGINE = 'sqlite3' 7 | DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite') 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': os.path.join(ROOT_PATH, 'testdb.sqlite'), 12 | } 13 | } 14 | 15 | TIME_ZONE = 'America/Chicago' 16 | LANGUAGE_CODE = 'en-us' 17 | SITE_ID = 1 18 | USE_I18N = True 19 | MEDIA_ROOT = '' 20 | MEDIA_URL = '' 21 | ADMIN_MEDIA_PREFIX = '/media/' 22 | SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk' 23 | TEMPLATE_LOADERS = ( 24 | 'django.template.loaders.filesystem.load_template_source', 25 | 'django.template.loaders.app_directories.load_template_source', 26 | ) 27 | TEMPLATE_CONTEXT_PROCESSORS = ( 28 | "django.core.context_processors.auth", 29 | "django.core.context_processors.debug", 30 | "django.core.context_processors.i18n", 31 | "django.core.context_processors.request", 32 | ) 33 | MIDDLEWARE_CLASSES = ( 34 | 'django.middleware.common.CommonMiddleware', 35 | 'django.contrib.sessions.middleware.SessionMiddleware', 36 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 37 | ) 38 | ROOT_URLCONF = 'urls' 39 | TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),) 40 | INSTALLED_APPS = ( 41 | 'django_roa', 42 | 'django_roa_server', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.sessions', 46 | #'django.contrib.sites', 47 | #'django.contrib.admin', 48 | ) 49 | SERIALIZATION_MODULES = { 50 | 'custom' : 'examples.django_roa_client.serializers', 51 | } 52 | PISTON_DISPLAY_ERRORS = False 53 | 54 | ## Logging settings 55 | import logging 56 | logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(message)s") 57 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Account, Reporter, Article, Tag 3 | 4 | 5 | # 6 | # Model serializers 7 | # Typical relationships applications 8 | # 9 | 10 | 11 | class AccountSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Account 14 | fields = ('id', 'email') 15 | 16 | 17 | class ReporterSerializer(serializers.ModelSerializer): 18 | account = AccountSerializer() 19 | class Meta: 20 | model = Reporter 21 | fields = ('id', 'account', 'first_name', 'last_name') 22 | 23 | 24 | class ArticleSerializer(serializers.ModelSerializer): 25 | reporter = ReporterSerializer() 26 | class Meta: 27 | model = Article 28 | fields = ('id', 'headline', 'pub_date', 'reporter') 29 | 30 | 31 | class TagSerializer(serializers.ModelSerializer): 32 | articles = ArticleSerializer(many=True) 33 | class Meta: 34 | model = Tag 35 | fields = ('id', 'label', 'articles') 36 | 37 | 38 | # 39 | # Model serializers 40 | # Reversed relationships applications 41 | # 42 | 43 | 44 | class ReversedTagSerializer(serializers.ModelSerializer): 45 | class Meta: 46 | model = Tag 47 | fields = ('id', 'label') 48 | 49 | 50 | class ReversedArticleSerializer(serializers.ModelSerializer): 51 | tags = ReversedTagSerializer(many=True) 52 | class Meta: 53 | model = Article 54 | fields = ('id', 'headline', 'pub_date', 'tags') 55 | 56 | 57 | class ReversedReporterSerializer(serializers.ModelSerializer): 58 | articles = ReversedArticleSerializer() 59 | 60 | class Meta: 61 | model = Reporter 62 | fields = ('id', 'first_name', 'last_name', 'articles') 63 | 64 | 65 | class ReversedAccountSerializer(serializers.ModelSerializer): 66 | reporter = ReversedReporterSerializer() 67 | class Meta: 68 | model = Account 69 | fields = ('id', 'email', 'reporter') 70 | 71 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Account, Reporter, Article, Tag 3 | 4 | 5 | # 6 | # Model serializers 7 | # Typical relationships applications 8 | # 9 | 10 | 11 | class AccountSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Account 14 | fields = ('id', 'email') 15 | 16 | 17 | class ReporterSerializer(serializers.ModelSerializer): 18 | account = AccountSerializer() 19 | class Meta: 20 | model = Reporter 21 | fields = ('id', 'account', 'first_name', 'last_name') 22 | 23 | 24 | class ArticleSerializer(serializers.ModelSerializer): 25 | reporter = ReporterSerializer() 26 | class Meta: 27 | model = Article 28 | fields = ('id', 'headline', 'pub_date', 'reporter') 29 | 30 | 31 | class TagSerializer(serializers.ModelSerializer): 32 | articles = ArticleSerializer(many=True) 33 | class Meta: 34 | model = Tag 35 | fields = ('id', 'label', 'articles') 36 | 37 | 38 | # 39 | # Model serializers 40 | # Reversed relationships applications 41 | # 42 | 43 | 44 | class ReversedTagSerializer(serializers.ModelSerializer): 45 | class Meta: 46 | model = Tag 47 | fields = ('id', 'label') 48 | 49 | 50 | class ReversedArticleSerializer(serializers.ModelSerializer): 51 | tags = ReversedTagSerializer(many=True) 52 | class Meta: 53 | model = Article 54 | fields = ('id', 'headline', 'pub_date', 'tags') 55 | 56 | 57 | class ReversedReporterSerializer(serializers.ModelSerializer): 58 | articles = ReversedArticleSerializer() 59 | 60 | class Meta: 61 | model = Reporter 62 | fields = ('id', 'first_name', 'last_name', 'articles') 63 | 64 | 65 | class ReversedAccountSerializer(serializers.ModelSerializer): 66 | reporter = ReversedReporterSerializer() 67 | class Meta: 68 | model = Account 69 | fields = ('id', 'email', 'reporter') 70 | 71 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "api.account", 4 | "pk": 1, 5 | "fields": { 6 | "email": "john@example.com" 7 | } 8 | }, 9 | { 10 | "model": "api.account", 11 | "pk": 2, 12 | "fields": { 13 | "email": "paul@example.com" 14 | } 15 | }, 16 | { 17 | "model": "api.reporter", 18 | "pk": 1, 19 | "fields": { 20 | "first_name": "John", 21 | "last_name": "Smith", 22 | "account": 1 23 | } 24 | }, 25 | { 26 | "model": "api.reporter", 27 | "pk": 2, 28 | "fields": { 29 | "first_name": "Paul", 30 | "last_name": "Jones", 31 | "account": 2 32 | } 33 | }, 34 | { 35 | "model": "api.article", 36 | "pk": 1, 37 | "fields": { 38 | "headline": "John's first story", 39 | "pub_date": "2013-01-04", 40 | "reporter": 1 41 | } 42 | }, 43 | { 44 | "model": "api.article", 45 | "pk": 2, 46 | "fields": { 47 | "headline": "John's second story", 48 | "pub_date": "2013-01-31", 49 | "reporter": 1 50 | } 51 | }, 52 | { 53 | "model": "api.article", 54 | "pk": 3, 55 | "fields": { 56 | "headline": "Paul's story", 57 | "pub_date": "2013-02-15", 58 | "reporter": 2 59 | } 60 | }, 61 | { 62 | "model": "api.tag", 63 | "pk": 1, 64 | "fields": { 65 | "label": "news", 66 | "articles": [1, 2, 3] 67 | } 68 | }, 69 | { 70 | "model": "api.tag", 71 | "pk": 2, 72 | "fields": { 73 | "label": "january", 74 | "articles": [1,2] 75 | } 76 | }, 77 | { 78 | "model": "api.tag", 79 | "pk": 3, 80 | "fields": { 81 | "label": "february", 82 | "articles": [3] 83 | } 84 | } 85 | ] 86 | -------------------------------------------------------------------------------- /templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrastyle %}{% endblock %} 5 | 6 | {% block coltype %}colMS{% endblock %} 7 | 8 | {% block bodyclass %}dashboard{% endblock %} 9 | 10 | {% block breadcrumbs %}{% endblock %} 11 | 12 | {% block content %} 13 |
14 | 15 | {% if app_list %} 16 | {% for app in app_list %} 17 |
18 | 19 | 20 | {% for model in app.models %} 21 | 22 | {% if model.perms.change %} 23 | 24 | {% else %} 25 | 26 | {% endif %} 27 | 28 | {% if model.perms.add %} 29 | 30 | {% else %} 31 | 32 | {% endif %} 33 | 34 | {% if model.perms.change %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 | 40 | {% endfor %} 41 |
{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}
{{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
42 |
43 | {% endfor %} 44 | {% else %} 45 |

{% trans "You don't have permission to edit anything." %}

46 | {% endif %} 47 |
48 | {% endblock %} 49 | 50 | {% block sidebar %} 51 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_roa import Model as ROAModel 3 | 4 | 5 | class CommonROAModel(ROAModel): 6 | 7 | class Meta: 8 | abstract = True 9 | 10 | @classmethod 11 | def get_resource_url_list(cls): 12 | return u'http://127.0.0.1:8000/%s/' % (cls.api_base_name) 13 | 14 | def get_resource_url_count(self): 15 | return self.get_resource_url_list() 16 | 17 | 18 | # Declare backend models 19 | 20 | 21 | class Account(CommonROAModel): 22 | id = models.IntegerField(primary_key=True) # don't forget it ! 23 | email = models.CharField(max_length=30) 24 | 25 | api_base_name = 'accounts' 26 | 27 | @classmethod 28 | def serializer(cls): 29 | from .serializers import AccountSerializer 30 | return AccountSerializer 31 | 32 | 33 | class Reporter(CommonROAModel): 34 | id = models.IntegerField(primary_key=True) # don't forget it ! 35 | account = models.OneToOneField(Account) 36 | first_name = models.CharField(max_length=30) 37 | last_name = models.CharField(max_length=30) 38 | 39 | api_base_name = 'reporters' 40 | 41 | @classmethod 42 | def serializer(cls): 43 | from .serializers import ReporterSerializer 44 | return ReporterSerializer 45 | 46 | 47 | class Article(CommonROAModel): 48 | id = models.IntegerField(primary_key=True) # don't forget it ! 49 | headline = models.CharField(max_length=100) 50 | pub_date = models.DateField() 51 | reporter = models.ForeignKey(Reporter, related_name='articles') 52 | 53 | api_base_name = 'articles' 54 | 55 | @classmethod 56 | def serializer(cls): 57 | from .serializers import ArticleSerializer 58 | return ArticleSerializer 59 | 60 | 61 | class Tag(CommonROAModel): 62 | id = models.IntegerField(primary_key=True) # don't forget it ! 63 | label = models.CharField(max_length=30) 64 | articles = models.ManyToManyField(Article, related_name='tags') 65 | 66 | api_base_name = 'tags' 67 | 68 | @classmethod 69 | def serializer(cls): 70 | from .serializers import TagSerializer 71 | return TagSerializer 72 | -------------------------------------------------------------------------------- /examples/twitter_roa/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | ROOT_PATH = os.path.dirname(__file__) 3 | 4 | TEMPLATE_DEBUG = DEBUG = True 5 | MANAGERS = ADMINS = () 6 | DATABASE_ENGINE = 'sqlite3' 7 | DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite') 8 | 9 | TIME_ZONE = 'America/Chicago' 10 | LANGUAGE_CODE = 'en-us' 11 | SITE_ID = 1 12 | USE_I18N = True 13 | MEDIA_ROOT = '' 14 | MEDIA_URL = '' 15 | ADMIN_MEDIA_PREFIX = '/media/' 16 | SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk' 17 | TEMPLATE_LOADERS = ( 18 | 'django.template.loaders.filesystem.load_template_source', 19 | 'django.template.loaders.app_directories.load_template_source', 20 | ) 21 | TEMPLATE_CONTEXT_PROCESSORS = ( 22 | "django.core.context_processors.auth", 23 | "django.core.context_processors.debug", 24 | "django.core.context_processors.i18n", 25 | "django.core.context_processors.request", 26 | ) 27 | MIDDLEWARE_CLASSES = ( 28 | 'django.middleware.common.CommonMiddleware', 29 | 'django.contrib.sessions.middleware.SessionMiddleware', 30 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 31 | ) 32 | ROOT_URLCONF = 'urls' 33 | TEMPLATE_DIRS = (os.path.join(ROOT_PATH, '../../templates'),) 34 | INSTALLED_APPS = ( 35 | 'django_roa', 36 | 'twitter_roa', 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | ) 42 | SESSION_ENGINE = "django.contrib.sessions.backends.file" 43 | SERIALIZATION_MODULES = { 44 | 'twitter' : 'examples.twitter_roa.serializers', 45 | } 46 | 47 | ## ROA custom settings 48 | ROA_MODELS = True # set to False if you'd like to develop/test locally 49 | ROA_FORMAT = 'twitter' # json or xml 50 | ROA_DJANGO_ERRORS = True # useful to ease debugging if you use test server 51 | 52 | ROA_URL_OVERRIDES_DETAIL = { 53 | 'twitter_roa.tweet': lambda o: u'http://api.twitter.com/1/statuses/show/%s.json' % o.id, 54 | 'twitter_roa.user': lambda o: u'http://api.twitter.com/1/users/show.json?user_id=%s' % o.id, 55 | } 56 | ROA_ARGS_NAMES_MAPPING = { 57 | 'filter_id__exact': 'user_id', 58 | } 59 | ROA_CUSTOM_ARGS = { 60 | 'include_entities': 'false', 61 | 'skip_status': 'true', 62 | } 63 | 64 | ## Logging settings 65 | import logging 66 | logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(message)s") 67 | -------------------------------------------------------------------------------- /examples/twitter_roa/serializers.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | from StringIO import StringIO 3 | 4 | from django.conf import settings 5 | from django.db import models 6 | from django.utils.encoding import smart_unicode 7 | from django.core.serializers import base 8 | from django.core.serializers.json import Serializer as JSONSerializer 9 | from django.core.serializers.python import _get_model 10 | 11 | DEFAULT_CHARSET = getattr(settings, 'DEFAULT_CHARSET', 'utf-8') 12 | 13 | class Serializer(JSONSerializer): 14 | pass 15 | 16 | 17 | def Deserializer(stream_or_string, **options): 18 | """ 19 | Deserialize a stream or string of JSON data. 20 | """ 21 | if isinstance(stream_or_string, basestring): 22 | stream = StringIO(stream_or_string) 23 | else: 24 | stream = stream_or_string 25 | models.get_apps() 26 | object_list = simplejson.load(stream) 27 | if not isinstance(object_list, list): 28 | object_list = [object_list] 29 | for obj in object_list: 30 | # Look up the model and starting build a dict of data for it. 31 | if 'screen_name' in obj: 32 | Model = _get_model('twitter_roa.user') 33 | else: 34 | Model = _get_model("twitter_roa.tweet") 35 | data = {} 36 | m2m_data = {} 37 | 38 | # Handle each field 39 | for (field_name, field_value) in obj.iteritems(): 40 | if isinstance(field_value, str): 41 | field_value = smart_unicode( 42 | field_value, 43 | options.get("encoding", DEFAULT_CHARSET), 44 | strings_only=True) 45 | 46 | try: 47 | field = Model._meta.get_field(field_name) 48 | except models.FieldDoesNotExist: 49 | continue 50 | 51 | # Handle FK fields 52 | if field.rel and isinstance(field.rel, models.ManyToOneRel): 53 | if field_value is not None: 54 | data[field.attname] = field.rel.to._meta.\ 55 | get_field(field.rel.field_name).\ 56 | to_python(field_value['id']) 57 | else: 58 | data[field.attname] = None 59 | 60 | # Handle all other fields 61 | else: 62 | data[field.name] = field.to_python(field_value) 63 | yield base.DeserializedObject(Model(**data), m2m_data) 64 | -------------------------------------------------------------------------------- /django_roa/db/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.html import strip_tags 3 | from django.utils.text import unescape_entities 4 | from django.utils.encoding import force_unicode 5 | 6 | ROA_DJANGO_ERRORS = getattr(settings, 'ROA_DJANGO_ERRORS', False) 7 | 8 | class ROAException(Exception): 9 | def __init__(self, exception): 10 | 11 | status_int = hasattr(exception, 'status_int') 12 | status_code = hasattr(exception, 'status_code') 13 | if status_code: 14 | self.status_code = exception.status_code 15 | elif status_int: 16 | self.status_code = exception.status_int 17 | else: 18 | self.status_code = "XXX" 19 | 20 | if hasattr(exception, 'message'): 21 | self.msg = force_unicode(exception.message) 22 | else: 23 | self.msg = force_unicode(exception) 24 | 25 | def __str__(self): 26 | if ROA_DJANGO_ERRORS and '' in self.msg: 27 | return self.parse_django_error() 28 | return self.msg 29 | 30 | def parse_django_error(self): 31 | """Extract the summary part of a Django HTML error.""" 32 | try: 33 | summary = self.msg.split(u'\n
\n ', 1)[1]\ 34 | .split(u'Python Executable:', 1)[0] 35 | traceback = self.msg.split(u'\n\nTraceback:', 1)[1]\ 36 | .split(u'', 1)[0] 37 | except IndexError: 38 | return self.msg 39 | result = [] 40 | title = None 41 | for line in strip_tags(summary).split('\n'): 42 | line_content = unescape_entities(line.strip()) 43 | if line_content: 44 | if line_content.endswith(':'): 45 | title = line_content 46 | elif title is None: 47 | title = "%s:" % line_content 48 | else: 49 | result.append("%s %s\n" % (title, line_content)) 50 | result.append("Status code: %s" % self.status_code) 51 | indent, indent2 = u' ', u' ' 52 | return u"%(summary)s %(traceback)s".strip() % { 53 | 'summary': indent.join(force_unicode(line) for line in result), 54 | 'traceback': indent2.join(force_unicode(line+"\n") \ 55 | for line in traceback.split('\n')), 56 | } 57 | 58 | 59 | class ROANotImplementedYetException(Exception): 60 | pass 61 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for frontend project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'a+q*2e&q#_w=rivx@ky8+833lx%xwtlz3-)jw@gd2()b(bmf3v' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'django_roa', 40 | 'frontend', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'frontend.urls' 53 | 54 | WSGI_APPLICATION = 'frontend.wsgi.application' 55 | 56 | 57 | # Database 58 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'django.db.backends.sqlite3', 63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 64 | } 65 | } 66 | 67 | # Internationalization 68 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 69 | 70 | LANGUAGE_CODE = 'en-us' 71 | 72 | TIME_ZONE = 'UTC' 73 | 74 | USE_I18N = True 75 | 76 | USE_L10N = True 77 | 78 | USE_TZ = True 79 | 80 | 81 | # Static files (CSS, JavaScript, Images) 82 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 83 | 84 | STATIC_URL = '/static/' 85 | 86 | 87 | # 88 | # ROA 89 | # 90 | 91 | ROA_MODELS = True # set to False if you'd like to develop/test locally 92 | ROA_FORMAT = 'json' # json or xml 93 | ROA_DJANGO_ERRORS = True # useful to ease debugging if you use test server 94 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '^586q83uc(6v0y$z@u(8h2sr&$+qdlw0a3q&+(4=u^5jvu5r#6' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'rest_framework', 40 | 'backend.api', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'backend.urls' 53 | 54 | WSGI_APPLICATION = 'backend.wsgi.application' 55 | 56 | 57 | # Database 58 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'django.db.backends.sqlite3', 63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 64 | } 65 | } 66 | 67 | # Internationalization 68 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 69 | 70 | LANGUAGE_CODE = 'en-us' 71 | 72 | TIME_ZONE = 'UTC' 73 | 74 | USE_I18N = True 75 | 76 | USE_L10N = True 77 | 78 | USE_TZ = True 79 | 80 | 81 | # Static files (CSS, JavaScript, Images) 82 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 83 | 84 | STATIC_URL = '/static/' 85 | 86 | # 87 | # DRF: 88 | # 89 | 90 | REST_FRAMEWORK = { 91 | # 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), 92 | 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',), 93 | 'PAGINATE_BY': 20 94 | } 95 | -------------------------------------------------------------------------------- /examples/django_rest_framework/backend/backend/api/mixins.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from django.db import models 3 | from functools import reduce 4 | import operator 5 | 6 | 7 | class FilterByKeyMixin(object): 8 | """ 9 | Custom viewset: add filter_key feature 10 | """ 11 | search_param = 'search' 12 | order_param = 'order_by' 13 | filter_param_prefix = 'filter_' 14 | 15 | def construct_search(self, field_name): 16 | if field_name.startswith('^'): 17 | return "%s__istartswith" % field_name[1:] 18 | elif field_name.startswith('='): 19 | return "%s__iexact" % field_name[1:] 20 | elif field_name.startswith('@'): 21 | return "%s__search" % field_name[1:] 22 | else: 23 | return "%s__icontains" % field_name 24 | 25 | def get_search_terms(self, request): 26 | """ 27 | Search terms are set by a ?search=... query parameter, 28 | and may be comma and/or whitespace delimited. 29 | """ 30 | params = request.QUERY_PARAMS.get(self.search_param, '') 31 | return params.replace(',', ' ').split() 32 | 33 | def filter_queryset(self, queryset): 34 | """ 35 | Parses QUERY_PARAMS and apply them 36 | """ 37 | 38 | for param in self.request.QUERY_PARAMS.keys(): 39 | # filter ? 40 | if param.startswith(self.filter_param_prefix): 41 | key_ = param.split(self.filter_param_prefix)[1] 42 | value_ = self.request.QUERY_PARAMS[param] 43 | if value_ is not None: 44 | queryset = queryset.filter(**{'%s' % (key_): value_}) 45 | 46 | # order by ? 47 | elif param == self.order_param: 48 | value_ = self.request.QUERY_PARAMS[param] 49 | if value_ is not None: 50 | queryset = queryset.order_by(value_) 51 | 52 | # search ? 53 | elif param == self.search_param: 54 | search_fields = getattr(self, 'search_fields', None) 55 | if search_fields: 56 | orm_lookups = [self.construct_search(str(search_field)) for search_field in search_fields] 57 | 58 | for search_term in self.get_search_terms(self.request): 59 | or_queries = [models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups] 60 | queryset = queryset.filter(reduce(operator.or_, or_queries)) 61 | 62 | limit_start = self.request.QUERY_PARAMS.get('limit_start', None) 63 | limit_stop = self.request.QUERY_PARAMS.get('limit_stop', None) 64 | 65 | if limit_stop is not None: 66 | limit_start = int(limit_start) if limit_start is not None else 0 67 | limit_stop = int(limit_stop) 68 | queryset = queryset[limit_start:limit_stop] 69 | 70 | elif limit_start is not None: 71 | limit_start = int(limit_start) 72 | queryset = queryset[limit_start:] 73 | 74 | return queryset 75 | 76 | 77 | class ModelViewSet(FilterByKeyMixin, viewsets.ModelViewSet): 78 | pass 79 | -------------------------------------------------------------------------------- /examples/django_roa_client/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | ROOT_PATH = os.path.dirname(__file__) 3 | 4 | TEMPLATE_DEBUG = DEBUG = True 5 | MANAGERS = ADMINS = () 6 | 7 | DATABASE_ENGINE = 'sqlite3' 8 | DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite') 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | 'NAME': os.path.join(ROOT_PATH, 'testdb.sqlite'), 13 | } 14 | } 15 | 16 | TIME_ZONE = 'America/Chicago' 17 | LANGUAGE_CODE = 'en-us' 18 | SITE_ID = 1 19 | USE_I18N = True 20 | MEDIA_ROOT = '' 21 | MEDIA_URL = '' 22 | ADMIN_MEDIA_PREFIX = '/media/' 23 | SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk' 24 | TEMPLATE_LOADERS = ( 25 | 'django.template.loaders.filesystem.load_template_source', 26 | 'django.template.loaders.app_directories.load_template_source', 27 | ) 28 | TEMPLATE_CONTEXT_PROCESSORS = ( 29 | "django.core.context_processors.auth", 30 | "django.core.context_processors.debug", 31 | "django.core.context_processors.i18n", 32 | "django.core.context_processors.request", 33 | ) 34 | MIDDLEWARE_CLASSES = ( 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | ) 39 | ROOT_URLCONF = 'urls' 40 | TEMPLATE_DIRS = (os.path.join(ROOT_PATH, '../../templates'),) 41 | INSTALLED_APPS = ( 42 | 'django_roa', 43 | 'django_roa.remoteauth', 44 | 'django_roa_client', 45 | 'django.contrib.auth', 46 | 'django.contrib.admin', 47 | 'django.contrib.contenttypes', 48 | 'django.contrib.sessions', 49 | ) 50 | AUTHENTICATION_BACKENDS = ( 51 | 'django_roa.remoteauth.backends.RemoteUserModelBackend', 52 | ) 53 | SESSION_ENGINE = "django.contrib.sessions.backends.file" 54 | SERIALIZATION_MODULES = { 55 | 'django': 'examples.django_roa_client.serializers', 56 | } 57 | 58 | ## ROA custom settings 59 | ROA_MODELS = True # set to False if you'd like to develop/test locally 60 | ROA_FORMAT = 'django' # json or xml 61 | # specify the headers sent to the ws from restkit 62 | ROA_HEADERS = { 63 | 'Content-Type': 'application/x-www-form-urlencoded', 64 | } 65 | ROA_DJANGO_ERRORS = True # useful to ease debugging if you use test server 66 | 67 | ROA_URL_OVERRIDES_LIST = { 68 | 'django_roa_client.remotepagewithoverriddenurls': u'http://127.0.0.1:8081/django_roa_server/remotepagewithoverriddenurls/', 69 | } 70 | ROA_URL_OVERRIDES_DETAIL = { 71 | 'django_roa_client.remotepagewithoverriddenurls': lambda o: u"%s%s-%s/" % (o.get_resource_url_list(), o.id, o.slug), 72 | } 73 | ROA_MODEL_NAME_MAPPING = ( 74 | # local name: remote name 75 | ('django_roa_client.', 'django_roa_server.'), 76 | ('remoteauth.', 'auth.'), 77 | ) 78 | ROA_ARGS_NAMES_MAPPING = { 79 | 'ORDER_BY': 'order', 80 | } 81 | ROA_MODEL_CREATE_MAPPING = { 82 | 'package.somemodel': ['attribute1', 'attribute2', 'attribute3', 'attribute4'], 83 | } 84 | ROA_MODEL_UPDATE_MAPPING = { 85 | 'package.somemodel': ['attribute2', 'attribute3'], 86 | } 87 | # Enable HTTP authentication through django-piston 88 | from restkit import BasicAuth 89 | ROA_FILTERS = [ BasicAuth('django-roa', 'roa'), ] 90 | # Disable authentication through django-piston 91 | #ROA_FILTERS = [] 92 | 93 | ## Logging settings 94 | import logging 95 | logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(message)s") 96 | -------------------------------------------------------------------------------- /examples/django_roa_server/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class RemotePage(models.Model): 4 | title = models.CharField(max_length=50, blank=True, null=True) 5 | 6 | def __unicode__(self): 7 | return u'%s (%s)' % (self.title, self.pk) 8 | 9 | 10 | class RemotePageWithManyFields(models.Model): 11 | #auto_field = models.AutoField(primary_key=True) 12 | char_field = models.CharField(max_length=50, blank=True, null=True) 13 | date_field = models.DateField(blank=True, null=True) 14 | datetime_field = models.DateTimeField(blank=True, null=True) 15 | decimal_field = models.DecimalField(decimal_places=3, max_digits=5, blank=True, null=True) 16 | email_field = models.EmailField(blank=True, null=True) 17 | filepath_field = models.FilePathField(blank=True, null=True) 18 | float_field = models.FloatField(blank=True, null=True) 19 | integer_field = models.IntegerField(blank=True, null=True) 20 | ipaddress_field = models.IPAddressField(blank=True, null=True) 21 | positiveinteger_field = models.PositiveIntegerField(blank=True, null=True) 22 | positivesmallinteger_field = models.PositiveSmallIntegerField(blank=True, null=True) 23 | slug_field = models.SlugField(blank=True, null=True) 24 | smallinteger_field = models.SmallIntegerField(blank=True, null=True) 25 | text_field = models.TextField(blank=True, null=True) 26 | time_field = models.TimeField(blank=True, null=True) 27 | url_field = models.URLField(blank=True, null=True) 28 | xml_field = models.TextField(blank=True, null=True) 29 | 30 | file_field = models.FileField(upload_to="files", blank=True, null=True) 31 | image_field = models.ImageField(upload_to="images", blank=True, null=True) 32 | 33 | def __unicode__(self): 34 | return u'%s (%s)' % (self.__class__.__name__, self.pk) 35 | 36 | 37 | class RemotePageWithBooleanFields(models.Model): 38 | boolean_field = models.BooleanField() 39 | null_boolean_field = models.NullBooleanField() 40 | 41 | 42 | class RemotePageWithCustomSlug(models.Model): 43 | title = models.CharField(max_length=50) 44 | slug = models.SlugField() 45 | 46 | def __unicode__(self): 47 | return u'%s (%s)' % (self.title, self.pk) 48 | 49 | 50 | class RemotePageWithOverriddenUrls(models.Model): 51 | title = models.CharField(max_length=50) 52 | slug = models.SlugField() 53 | 54 | def __unicode__(self): 55 | return u'%s (%s)' % (self.title, self.pk) 56 | 57 | 58 | class RemotePageWithRelationsThrough(models.Model): 59 | title = models.CharField(max_length=50) 60 | remote_page_with_relations = models.ForeignKey("RemotePageWithRelations", blank=True, null=True) 61 | remote_page_with_many_fields = models.ForeignKey("RemotePageWithManyFields", blank=True, null=True) 62 | 63 | def __unicode__(self): 64 | return u'%s (%s)' % (self.title, self.pk) 65 | 66 | 67 | class RemotePageWithRelations(models.Model): 68 | title = models.CharField(max_length=50) 69 | remote_page = models.ForeignKey(RemotePage, blank=True, null=True) 70 | remote_page_fields = models.ManyToManyField(RemotePageWithManyFields, through=RemotePageWithRelationsThrough, blank=True, null=True) 71 | 72 | def __unicode__(self): 73 | return u'%s (%s)' % (self.title, self.pk) 74 | 75 | 76 | class RemotePageWithNamedRelations(models.Model): 77 | title = models.CharField(max_length=50) 78 | first_page = models.ForeignKey(RemotePage, blank=True, null=True, related_name="from_first") 79 | last_page = models.ForeignKey(RemotePage, blank=True, null=True, related_name="from_last") 80 | 81 | def __unicode__(self): 82 | return u'%s (%s)' % (self.title, self.pk) 83 | 84 | 85 | class RemotePageWithCustomPrimaryKey(models.Model): 86 | auto_field = models.AutoField(primary_key=True) 87 | title = models.CharField(max_length=50) 88 | 89 | def __unicode__(self): 90 | return u'%s (%s)' % (self.title, self.pk) 91 | -------------------------------------------------------------------------------- /django_roa/remoteauth/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django.utils import timezone 3 | from django.core import validators 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.contrib.auth.models import Permission as DjangoPermission, \ 6 | User as DjangoUser, Group as DjangoGroup, \ 7 | UserManager as DjangoUserManager 8 | try: 9 | from django.contrib.auth.models import Message as DjangoMessage 10 | except ImportError: 11 | DjangoMessage = None 12 | from django.utils.translation import ugettext_lazy as _ 13 | from django.db import models 14 | 15 | from django_roa import Model, Manager 16 | 17 | 18 | class Permission(Model, DjangoPermission): 19 | """ 20 | Inherits methods from Django's Permission model. 21 | """ 22 | name = models.CharField(_('name'), max_length=50) 23 | # non-standard related_name added to avoid clashes 24 | content_type = models.ForeignKey(ContentType, related_name="permissions") 25 | codename = models.CharField(_('codename'), max_length=100) 26 | 27 | @staticmethod 28 | def get_resource_url_list(): 29 | return u'http://127.0.0.1:8081/auth/permission/' 30 | 31 | @classmethod 32 | def serializer(cls): 33 | from .serializers import PermissionSerializer 34 | return PermissionSerializer 35 | 36 | 37 | class Group(Model, DjangoGroup): 38 | """ 39 | Inherits methods from Django's Group model. 40 | """ 41 | id = models.IntegerField(primary_key=True) 42 | name = models.CharField(_('name'), max_length=80, unique=True) 43 | permissions = models.ManyToManyField(Permission, 44 | verbose_name=_('permissions'), blank=True) 45 | 46 | @staticmethod 47 | def get_resource_url_list(): 48 | return u'http://127.0.0.1:8081/auth/group/' 49 | 50 | @classmethod 51 | def serializer(cls): 52 | from .serializers import GroupSerializer 53 | return GroupSerializer 54 | 55 | 56 | class UserManager(Manager, DjangoUserManager): 57 | """ 58 | Inherits methods from Django's UserManager manager. 59 | """ 60 | 61 | 62 | class User(Model, DjangoUser): 63 | """ 64 | Inherits methods from Django's User model. 65 | """ 66 | id = models.IntegerField(primary_key=True) 67 | username = models.CharField(_('username'), max_length=30, unique=True, 68 | help_text=_('Required. 30 characters or fewer. Letters, numbers and ' 69 | '@/./+/-/_ characters'), 70 | validators=[ 71 | validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') 72 | ]) 73 | first_name = models.CharField(_('first name'), max_length=30, blank=True) 74 | last_name = models.CharField(_('last name'), max_length=30, blank=True) 75 | email = models.EmailField(_('email address'), blank=True) 76 | is_staff = models.BooleanField(_('staff status'), default=False, 77 | help_text=_('Designates whether the user can log into this admin ' 78 | 'site.')) 79 | is_active = models.BooleanField(_('active'), default=True, 80 | help_text=_('Designates whether this user should be treated as ' 81 | 'active. Unselect this instead of deleting accounts.')) 82 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 83 | 84 | password = models.CharField(_('password'), max_length=128) 85 | last_login = models.DateTimeField(_('last login'), default=timezone.now) 86 | 87 | is_superuser = models.BooleanField(_('superuser status'), default=False, 88 | help_text=_('Designates that this user has all permissions without ' 89 | 'explicitly assigning them.')) 90 | groups = models.ManyToManyField(Group, verbose_name=_('groups'), 91 | blank=True, help_text=_('The groups this user belongs to. A user will ' 92 | 'get all permissions granted to each of ' 93 | 'his/her group.'), 94 | related_name="user_set", related_query_name="user") 95 | user_permissions = models.ManyToManyField(Permission, 96 | verbose_name=_('user permissions'), blank=True, 97 | help_text=_('Specific permissions for this user.'), 98 | related_name="user_set", related_query_name="user") 99 | 100 | objects = UserManager() 101 | 102 | @staticmethod 103 | def get_resource_url_list(): 104 | return u'http://127.0.0.1:8081/auth/user/' 105 | 106 | @classmethod 107 | def serializer(cls): 108 | from .serializers import UserSerializer 109 | return UserSerializer 110 | 111 | 112 | Message = None 113 | 114 | if DjangoMessage: 115 | class Message(Model, DjangoMessage): 116 | """ 117 | Inherits methods from Django's Message model. 118 | """ 119 | user = models.ForeignKey(User) 120 | message = models.TextField(_('message')) 121 | 122 | @staticmethod 123 | def get_resource_url_list(): 124 | return u'http://127.0.0.1:8081/auth/message/' 125 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | Django-ROA (Resource Oriented Architecture) 3 | =========================================== 4 | 5 | **Use Django's ORM to model remote API resources.** 6 | 7 | Fork of original `David Larlet Django ROA lib `_. 8 | Now ROA works directly with an API like `Django Rest Framework `_ 9 | 10 | **How does it works:** 11 | Each time a request is passed to the database, the request is intercepted and transformed to an HTTP request to the remote server with the right 12 | method (GET, POST, PUT or DELETE) given the get_resource_url_* methods specified in the model's definition. 13 | 14 | 15 | Documentation 16 | ============= 17 | 18 | Initial documentation: 19 | 20 | - `Wiki home `_ 21 | - `Getting started with Django-ROA `_ 22 | - `Developing with Django-ROA `_ 23 | 24 | 25 | Supported versions 26 | ================== 27 | 28 | .. image:: https://travis-ci.org/charles-vdulac/django-roa.png?branch=master 29 | :target: https://travis-ci.org/charles-vdulac/django-roa 30 | :alt: Build Status 31 | 32 | - Django 1.4, 1.5, 1.6, 1.7 33 | - Python 2.6, 2.7 34 | 35 | 36 | Installation 37 | ============ 38 | 39 | .. code:: bash 40 | 41 | $ pip install -e git+https://github.com/charles-vdulac/django-roa.git@master#egg=django_roa 42 | 43 | 44 | Fork getting started 45 | ==================== 46 | 47 | If you have an API output like this (typical DRF output): 48 | 49 | .. code:: python 50 | 51 | # GET http://api.example.com/articles/ 52 | # HTTP 200 OK 53 | # Content-Type: application/json 54 | # Vary: Accept 55 | # Allow: GET, POST, HEAD, OPTIONS 56 | 57 | { 58 | "count": 3, 59 | "next": null, 60 | "previous": null, 61 | "results": [ 62 | { 63 | "id": 1, 64 | "headline": "John's first story", 65 | "pub_date": "2013-01-04", 66 | "reporter": { 67 | "id": 1, 68 | "account": { 69 | "id": 1, 70 | "email": "john@example.com" 71 | }, 72 | "first_name": "John", 73 | "last_name": "Smith" 74 | } 75 | }, 76 | ... 77 | ] 78 | } 79 | 80 | Your code will look like this: 81 | 82 | .. code:: python 83 | 84 | from django.db import models 85 | from django_roa import Model as ROAModel 86 | 87 | class Article(ROAModel): 88 | id = models.IntegerField(primary_key=True) # don't forget it ! 89 | headline = models.CharField(max_length=100) 90 | pub_date = models.DateField() 91 | reporter = models.ForeignKey(Reporter, related_name='articles') 92 | 93 | api_base_name = 'articles' 94 | 95 | @classmethod 96 | def serializer(cls): 97 | from .serializers import ArticleSerializer 98 | return ArticleSerializer 99 | 100 | @classmethod 101 | def get_resource_url_list(cls): 102 | return u'http://api.example.com/{base_name}/'.format( 103 | base_name=cls.api_base_name, 104 | ) 105 | 106 | def get_resource_url_count(self): 107 | return self.get_resource_url_list() 108 | 109 | .. code:: python 110 | 111 | from rest_framework import serializers 112 | from .models import Article 113 | 114 | class ArticleSerializer(serializers.ModelSerializer): 115 | reporter = ReporterSerializer() 116 | class Meta: 117 | model = Article 118 | fields = ('id', 'headline', 'pub_date', 'reporter') 119 | 120 | Refer to `tests `_ for full example. 121 | 122 | Running tests 123 | ============= 124 | 125 | - Initial tests: read `documentation `_ 126 | - Fork tests: read `README `_ 127 | 128 | 129 | Caveats 130 | ======= 131 | 132 | For the moment, the library doesn't work in this case: 133 | 134 | One to one (reversed) 135 | --------------------- 136 | 137 | .. code:: python 138 | 139 | class Reporter(CommonROAModel): 140 | account = models.OneToOneField(Account) 141 | ... 142 | 143 | with fixtures: 144 | 145 | .. code:: json 146 | 147 | { 148 | "model": "api.reporter", 149 | "pk": 1, 150 | "fields": { 151 | "first_name": "John", 152 | "last_name": "Smith", 153 | "account": 1 154 | } 155 | }, 156 | { 157 | "model": "api.account", 158 | "pk": 1, 159 | "fields": { 160 | "email": "john@example.com" 161 | } 162 | }, 163 | 164 | This works: 165 | 166 | .. code:: python 167 | 168 | reporter = Reporter.objects.get(id=1) 169 | assertEqual(reporter.account.id, 1) 170 | assertEqual(reporter.account.email, 'john@example.com') 171 | 172 | But not this way: 173 | 174 | .. code:: python 175 | 176 | account = Account.objects.get(id=1) 177 | assertEqual(account.reporter.id, 1) 178 | assertEqual(account.reporter.first_name, "John") 179 | 180 | 181 | HTTPS certificate pinning 182 | ========================= 183 | 184 | You can pass ssl args (see `ssl.wrap_socket()`) via the `ROA_SSL_ARGS` of your 185 | ``settings.py``. 186 | 187 | 188 | To pin the server certificate, save the public certificate(s) you want to 189 | pin in *pinned-ca.pem* and add the following to your *settings.py* : 190 | 191 | .. code:: python 192 | 193 | from os.path import dirname, join 194 | ROA_SSL_ARGS = { 195 | 'ca_certs': join(dirname(dirname(__file__)), 'pinned-ca.pem'), 196 | 'cert_reqs': True 197 | } 198 | -------------------------------------------------------------------------------- /examples/django_roa_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import url, patterns 2 | 3 | from piston.resource import Resource 4 | from piston.authentication import HttpBasicAuthentication 5 | 6 | from django_roa_server.handlers import RemotePageHandler, \ 7 | RemotePageWithManyFieldsHandler, RemotePageWithBooleanFieldsHandler, \ 8 | RemotePageWithCustomSlugHandler, RemotePageWithOverriddenUrlsHandler, \ 9 | RemotePageWithRelationsHandler, UserHandler, MessageHandler, \ 10 | PermissionHandler, GroupHandler, RemotePageCountHandler, \ 11 | RemotePageWithManyFieldsCountHandler, RemotePageWithBooleanFieldsCountHandler, \ 12 | RemotePageWithCustomSlugCountHandler, RemotePageWithOverriddenUrlsCountHandler, \ 13 | RemotePageWithRelationsHandler, RemotePageWithNamedRelationsHandler, \ 14 | RemotePageWithNamedRelationsCountHandler, RemotePageWithRelationsThroughHandler, \ 15 | RemotePageWithCustomPrimaryKeyHandler, RemotePageWithCustomPrimaryKeyCountHandler, \ 16 | RemotePageWithCustomPrimaryKeyCount2Handler 17 | 18 | # Enable HTTP authentication through django-piston 19 | ad = { 'authentication': HttpBasicAuthentication( 20 | realm="django-roa-server", 21 | auth_func = lambda username, password: username == 'django-roa' and password == 'roa' 22 | )} 23 | # Disable authentication through django-piston 24 | #ad = { 'authentication': None} 25 | 26 | remote_pages = Resource(handler=RemotePageHandler, **ad) 27 | remote_pages_count = Resource(handler=RemotePageCountHandler, **ad) 28 | 29 | remote_pages_with_many_fields = Resource(handler=RemotePageWithManyFieldsHandler, **ad) 30 | remote_pages_with_many_fields_count = Resource(handler=RemotePageWithManyFieldsCountHandler, **ad) 31 | 32 | remote_pages_with_boolean_fields = Resource(handler=RemotePageWithBooleanFieldsHandler, **ad) 33 | remote_pages_with_boolean_fields_count = Resource(handler=RemotePageWithBooleanFieldsCountHandler, **ad) 34 | 35 | remote_pages_with_custom_slug = Resource(handler=RemotePageWithCustomSlugHandler, **ad) 36 | remote_pages_with_custom_slug_count = Resource(handler=RemotePageWithCustomSlugCountHandler, **ad) 37 | 38 | remote_pages_with_custom_primary_key = Resource(handler=RemotePageWithCustomPrimaryKeyHandler, **ad) 39 | remote_pages_with_custom_primary_key_count = Resource(handler=RemotePageWithCustomPrimaryKeyCountHandler, **ad) 40 | remote_pages_with_custom_primary_key_count2 = Resource(handler=RemotePageWithCustomPrimaryKeyCount2Handler, **ad) 41 | 42 | remote_pages_with_overridden_urls = Resource(handler=RemotePageWithOverriddenUrlsHandler, **ad) 43 | remote_pages_with_overridden_urls_count = Resource(handler=RemotePageWithOverriddenUrlsCountHandler, **ad) 44 | 45 | remote_pages_with_relations = Resource(handler=RemotePageWithRelationsHandler, **ad) 46 | remote_pages_with_relations_count = Resource(handler=RemotePageWithRelationsHandler, **ad) 47 | 48 | remote_pages_with_relations_through = Resource(handler=RemotePageWithRelationsThroughHandler, **ad) 49 | remote_pages_with_relations_through_count = Resource(handler=RemotePageWithRelationsThroughHandler, **ad) 50 | 51 | remote_pages_with_named_relations = Resource(handler=RemotePageWithNamedRelationsHandler, **ad) 52 | remote_pages_with_named_relations_count = Resource(handler=RemotePageWithNamedRelationsCountHandler, **ad) 53 | 54 | users = Resource(handler=UserHandler, **ad) 55 | messages = None 56 | if MessageHandler: 57 | messages = Resource(handler=MessageHandler, **ad) 58 | permissions = Resource(handler=PermissionHandler, **ad) 59 | groups = Resource(handler=GroupHandler, **ad) 60 | 61 | urlpatterns = patterns('', 62 | # Remote pages counts 63 | url(r'^django_roa_server/remotepage/count/$', remote_pages_count), 64 | url(r'^django_roa_server/remotepagewithmanyfields/count/$', remote_pages_with_many_fields_count), 65 | url(r'^django_roa_server/remotepagewithbooleanfields/count/$', remote_pages_with_boolean_fields_count), 66 | url(r'^django_roa_server/remotepagewithcustomslug/count/$', remote_pages_with_custom_slug_count), 67 | url(r'^django_roa_server/remotepagewithcustomprimarykey/count/$', remote_pages_with_custom_primary_key_count), 68 | url(r'^django_roa_server/remotepagewithcustomprimarykey/count2/$', remote_pages_with_custom_primary_key_count2), 69 | url(r'^django_roa_server/remotepagewithoverriddenurls/count/$', remote_pages_with_overridden_urls_count), 70 | url(r'^django_roa_server/remotepagewithrelations/count/$', remote_pages_with_relations_count), 71 | url(r'^django_roa_server/remotepagewithrelationsthrough/count/$', remote_pages_with_relations_through_count), 72 | url(r'^django_roa_server/remotepagewithnamedrelations/count/$', remote_pages_with_named_relations_count), 73 | url(r'^django_roa_server/remotepagewithproxy/count/$', remote_pages_count), 74 | 75 | # Remote pages 76 | url(r'^django_roa_server/remotepage/?(?P\d+)?/?$', remote_pages), 77 | url(r'^django_roa_server/remotepagewithmanyfields/?(?P\d+)?/?$', remote_pages_with_many_fields), 78 | url(r'^django_roa_server/remotepagewithbooleanfields/?(?P\d+)?/?$', remote_pages_with_boolean_fields), 79 | url(r'^django_roa_server/remotepagewithcustomslug/?(?P[-\w]+)?/?$', remote_pages_with_custom_slug), 80 | url(r'^django_roa_server/remotepagewithcustomprimarykey/?(?P[-\w]+)?/?$', remote_pages_with_custom_primary_key), 81 | url(r'^django_roa_server/remotepagewithoverriddenurls/?(?P[-\w]+)?/?$', remote_pages_with_overridden_urls), 82 | url(r'^django_roa_server/remotepagewithrelations/?(?P\d+)?/?$', remote_pages_with_relations), 83 | url(r'^django_roa_server/remotepagewithrelationsthrough/?(?P\d+)?/?$', remote_pages_with_relations_through), 84 | url(r'^django_roa_server/remotepagewithnamedrelations/?(?P\d+)?/?$', remote_pages_with_named_relations), 85 | url(r'^django_roa_server/remotepagewithproxy/?(?P\d+)?/?$', remote_pages), 86 | 87 | # Auth application 88 | url(r'^auth/user/?(?P\d+)?/?$', users), 89 | url(r'^auth/message/?(?P\d+)?/?$', messages), 90 | url(r'^auth/permission/?(?P\d+)?/?$', permissions), 91 | url(r'^auth/group/?(?P\d+)?/?$', groups), 92 | ) 93 | -------------------------------------------------------------------------------- /examples/django_rest_framework/frontend/frontend/tests.py: -------------------------------------------------------------------------------- 1 | from django.utils.timezone import now 2 | from rest_framework.test import APITestCase 3 | from django_roa.db.exceptions import ROAException 4 | from .models import Account, Article, Tag, Reporter 5 | 6 | 7 | class ROATestCase(APITestCase): 8 | 9 | def test_all(self): 10 | 11 | # 12 | # Get 13 | # 14 | 15 | account = Account.objects.get(id=1) 16 | self.assertEqual(account.id, 1) 17 | self.assertEqual(account.email, 'john@example.com') 18 | 19 | account = Account.objects.get(email='paul@example.com') 20 | self.assertEqual(account.id, 2) 21 | self.assertEqual(account.email, 'paul@example.com') 22 | 23 | # All 24 | accounts = Account.objects.all() 25 | self.assertEqual(accounts.count(), 2) 26 | self.assertEqual(accounts[0].id, 1) 27 | self.assertEqual(accounts[0].email, 'john@example.com') 28 | self.assertEqual(accounts[1].id, 2) 29 | self.assertEqual(accounts[1].email, 'paul@example.com') 30 | 31 | # filter 32 | accounts = Account.objects.filter(email='paul@example.com') 33 | self.assertEqual(accounts.count(), 1) 34 | self.assertEqual(accounts[0].id, 2) 35 | self.assertEqual(accounts[0].email, 'paul@example.com') 36 | 37 | # 38 | # Relationships 39 | # Typical relationships applications 40 | # 41 | 42 | # One to One 43 | reporter = Reporter.objects.get(id=1) 44 | self.assertEqual(reporter.first_name, "John") 45 | self.assertEqual(reporter.account.id, 1) 46 | self.assertEqual(reporter.account.email, 'john@example.com') 47 | 48 | # One to one reversed: 49 | # TODO: doesn't work 50 | # account = Account.objects.get(id=1) 51 | # self.assertEqual(account.reporter.id, 1) 52 | # self.assertEqual(account.reporter.first_name, "John") 53 | 54 | # One to Many 55 | articles = reporter.articles.all() 56 | self.assertEqual(articles.count(), 2) 57 | self.assertEqual(articles[0].id, 1) 58 | self.assertEqual(articles[0].headline, "John's first story") 59 | self.assertEqual(articles[1].id, 2) 60 | self.assertEqual(articles[1].headline, "John's second story") 61 | 62 | # Many to one 63 | article = Article.objects.get(id=1) 64 | self.assertEqual(article.reporter.id, 1) 65 | self.assertEqual(article.reporter.first_name, "John") 66 | 67 | # Many to Many 68 | tags = articles[0].tags.all() 69 | self.assertEqual(tags.count(), 2) 70 | self.assertEqual(tags[0].id, 1) 71 | self.assertEqual(tags[0].label, 'news') 72 | self.assertEqual(tags[1].id, 2) 73 | self.assertEqual(tags[1].label, 'january') 74 | 75 | # Many to Many reversed 76 | tag = Tag.objects.get(id=1) 77 | self.assertEqual(tag.label, 'news') 78 | articles = tag.articles.all() 79 | self.assertEqual(articles.count(), 3) 80 | self.assertEqual(articles[0].id, 1) 81 | self.assertEqual(articles[0].headline, "John's first story") 82 | self.assertEqual(articles[1].id, 2) 83 | self.assertEqual(articles[1].headline, "John's second story") 84 | self.assertEqual(articles[2].id, 3) 85 | self.assertEqual(articles[2].headline, "Paul's story") 86 | 87 | # 88 | # Create 89 | # 90 | 91 | new_account = Account() 92 | new_account.email = 'peter@example.com' 93 | new_account.save() 94 | 95 | self.assertIsNotNone(new_account.id) 96 | 97 | accounts = Account.objects.all() 98 | self.assertEqual(accounts.count(), 3) 99 | self.assertEqual(accounts[0].email, 'john@example.com') 100 | self.assertEqual(accounts[1].email, 'paul@example.com') 101 | self.assertEqual(accounts[2].email, 'peter@example.com') 102 | 103 | # 104 | # Delete 105 | # 106 | 107 | new_account.delete() 108 | self.assertIsNone(new_account.pk) 109 | 110 | accounts = Account.objects.all() 111 | self.assertEqual(accounts.count(), 2) 112 | 113 | # 114 | # Create with relationship 115 | # 116 | 117 | count_accounts = Account.objects.count() 118 | count_reporters = Reporter.objects.count() 119 | count_articles = Article.objects.count() 120 | 121 | new_account = Account() 122 | new_account.email = 'james@example.com' 123 | new_account.save() 124 | 125 | new_reporter = Reporter() 126 | new_reporter.account = new_account 127 | new_reporter.first_name = 'James' 128 | new_reporter.last_name = 'Doe' 129 | new_reporter.save() 130 | 131 | new_article = Article() 132 | new_article.headline = "James's story" 133 | new_article.reporter = new_reporter 134 | self.assertRaises(ROAException, new_article.save) # pub_date is required 135 | new_article.pub_date = now().date() 136 | new_article.save() 137 | 138 | # There are 3 lines where new_account is save: 139 | # new_account.save() 140 | # new_reporter.save() 141 | # new_article.save() 142 | # But only one instance must be save into db. 143 | # TODO: goal: only one record. 144 | self.assertEqual(Account.objects.count(), count_accounts + 3) 145 | # Same case: 146 | self.assertEqual(Reporter.objects.count(), count_reporters + 2) 147 | self.assertEqual(Article.objects.count(), count_articles + 1) 148 | 149 | article = Article.objects.get(headline="James's story") 150 | self.assertEqual(article.reporter.first_name, 'James') 151 | self.assertEqual(article.reporter.account.email, 'james@example.com') 152 | 153 | 154 | def test_empty_list_no_pagination(self): 155 | tags = Tag.objects.filter(label='idonetexist') 156 | 157 | # To check that iterating over the empty queryset works 158 | for i in tags: 159 | pass 160 | 161 | self.assertEqual(tags.count(), 0) 162 | -------------------------------------------------------------------------------- /examples/django_roa_client/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | from django.template.defaultfilters import slugify 4 | 5 | from django_roa import Model 6 | 7 | class RemotePage(Model): 8 | title = models.CharField(max_length=50, blank=True, null=True) 9 | 10 | def __unicode__(self): 11 | return u'%s (%s)' % (self.title, self.pk) 12 | 13 | @staticmethod 14 | def get_resource_url_list(): 15 | return u'http://127.0.0.1:8081/django_roa_server/remotepage/' 16 | 17 | 18 | class RemotePageWithManyFields(Model): 19 | #auto_field = models.AutoField(primary_key=True) 20 | char_field = models.CharField(max_length=50, blank=True, null=True) 21 | date_field = models.DateField(blank=True, null=True) 22 | datetime_field = models.DateTimeField(blank=True, null=True) 23 | decimal_field = models.DecimalField(decimal_places=3, max_digits=5, blank=True, null=True) 24 | email_field = models.EmailField(blank=True, null=True) 25 | filepath_field = models.FilePathField(blank=True, null=True) 26 | float_field = models.FloatField(blank=True, null=True) 27 | integer_field = models.IntegerField(blank=True, null=True) 28 | ipaddress_field = models.IPAddressField(blank=True, null=True) 29 | positiveinteger_field = models.PositiveIntegerField(blank=True, null=True) 30 | positivesmallinteger_field = models.PositiveSmallIntegerField(blank=True, null=True) 31 | slug_field = models.SlugField(blank=True, null=True) 32 | smallinteger_field = models.SmallIntegerField(blank=True, null=True) 33 | text_field = models.TextField(blank=True, null=True) 34 | time_field = models.TimeField(blank=True, null=True) 35 | url_field = models.URLField(blank=True, null=True) 36 | xml_field = models.CharField(max_length=128, blank=True, null=True) 37 | 38 | file_field = models.FileField(upload_to="files", blank=True, null=True) 39 | image_field = models.ImageField(upload_to="images", blank=True, null=True) 40 | 41 | def __unicode__(self): 42 | return u'%s (%s)' % (self.__class__.__name__, self.pk) 43 | 44 | @staticmethod 45 | def get_resource_url_list(): 46 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithmanyfields/' 47 | 48 | 49 | class RemotePageWithBooleanFields(Model): 50 | boolean_field = models.BooleanField() 51 | null_boolean_field = models.NullBooleanField() 52 | 53 | @staticmethod 54 | def get_resource_url_list(): 55 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithbooleanfields/' 56 | 57 | 58 | class RemotePageWithCustomSlug(Model): 59 | title = models.CharField(max_length=50) 60 | slug = models.SlugField() 61 | 62 | def __unicode__(self): 63 | return u'%s (%s)' % (self.title, self.pk) 64 | 65 | def save(self, force_insert=False, force_update=False, using=None): 66 | if not self.slug: 67 | self.slug = slugify(self.title) 68 | super(RemotePageWithCustomSlug, self).save(force_insert, force_update) 69 | 70 | @staticmethod 71 | def get_resource_url_list(): 72 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithcustomslug/' 73 | 74 | def get_resource_url_detail(self): 75 | return u"%s%s-%s/" % (self.get_resource_url_list(), self.pk, self.slug) 76 | 77 | 78 | class RemotePageWithOverriddenUrls(Model): 79 | title = models.CharField(max_length=50) 80 | slug = models.SlugField() 81 | 82 | def __unicode__(self): 83 | return u'%s (%s)' % (self.title, self.pk) 84 | 85 | def save(self, force_insert=False, force_update=False, using=None): 86 | if not self.slug: 87 | self.slug = slugify(self.title) 88 | super(RemotePageWithOverriddenUrls, self).save(force_insert, force_update) 89 | 90 | @staticmethod 91 | def get_resource_url_list(): 92 | return u'' # overridden by settings 93 | 94 | 95 | class RemotePageWithRelationsThrough(Model): 96 | title = models.CharField(max_length=50) 97 | remote_page_with_relations = models.ForeignKey("RemotePageWithRelations", blank=True, null=True) 98 | remote_page_with_many_fields = models.ForeignKey("RemotePageWithManyFields", blank=True, null=True) 99 | 100 | def __unicode__(self): 101 | return u'%s (%s)' % (self.title, self.pk) 102 | 103 | @staticmethod 104 | def get_resource_url_list(): 105 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithrelationsthrough/' 106 | 107 | 108 | class RemotePageWithRelations(Model): 109 | title = models.CharField(max_length=50) 110 | remote_page = models.ForeignKey(RemotePage, blank=True, null=True) 111 | remote_page_fields = models.ManyToManyField(RemotePageWithManyFields, through=RemotePageWithRelationsThrough, blank=True, null=True) 112 | 113 | def __unicode__(self): 114 | return u'%s (%s)' % (self.title, self.pk) 115 | 116 | @staticmethod 117 | def get_resource_url_list(): 118 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithrelations/' 119 | 120 | 121 | class RemotePageWithNamedRelations(Model): 122 | title = models.CharField(max_length=50) 123 | first_page = models.ForeignKey(RemotePage, blank=True, null=True, related_name="from_first") 124 | last_page = models.ForeignKey(RemotePage, blank=True, null=True, related_name="from_last") 125 | 126 | def __unicode__(self): 127 | return u'%s (%s)' % (self.title, self.pk) 128 | 129 | @staticmethod 130 | def get_resource_url_list(): 131 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithnamedrelations/' 132 | 133 | 134 | class RemotePageWithProxy(RemotePage): 135 | 136 | class Meta: 137 | proxy = True 138 | 139 | @staticmethod 140 | def get_resource_url_list(): 141 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithproxy/' 142 | 143 | class RemotePageWithCustomPrimaryKey(Model): 144 | auto_field = models.AutoField(primary_key=True) 145 | title = models.CharField(max_length=50) 146 | 147 | def __unicode__(self): 148 | return u'%s (%s)' % (self.title, self.pk) 149 | 150 | @staticmethod 151 | def get_resource_url_list(): 152 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithcustomprimarykey/' 153 | 154 | class RemotePageWithCustomPrimaryKeyCountOverridden(RemotePageWithCustomPrimaryKey): 155 | 156 | class Meta: 157 | proxy = True 158 | 159 | def get_resource_url_count(self): 160 | return u'http://127.0.0.1:8081/django_roa_server/remotepagewithcustomprimarykey/count2/' 161 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ====================== 2 | Django-ROA's changelog 3 | ====================== 4 | 5 | Version 1.8.1, 21 Nov 2014: 6 | -------------------------- 7 | 8 | * Fix pagination in remote querysets (checking limit_start/limit_stop) 9 | Thanks to @jathanism 10 | 11 | 12 | Version 1.8, 12 Sep 2014: 13 | -------------------------- 14 | 15 | * Django REST Framework support + bug fixes. 16 | Thanks to @JocelynDelalande, @sandersky, @jathanism and @charles-vdulac 17 | 18 | 19 | Version 1.7, 11 May 2012: 20 | -------------------------- 21 | 22 | * @neokeeper aka Martin Skala updated the code in many ways: introduced an 23 | ROA_FILTERS setting to provide authentication on resources' access, 24 | improved single instance retrieval, dealt with pks instead of ids when 25 | possible, updated requirements, code clean up, documentation and 26 | example updates. Many thanks! 27 | * @NikhilChelliah fixed a bug related to object saving and non-int ids. 28 | Thanks! 29 | 30 | 31 | Version 1.6, 16 July 2010: 32 | -------------------------- 33 | 34 | * Switch to pip's requirements.txt file for external dependencies (restkit and 35 | piston, note that the later is only useful for tests). 36 | * @cyril fixed a lot of bugs, see changeset 86a68259e83e for details. Thanks! 37 | * @cyrilrbt fixed missing ROA_CUSTOM_ARGS on delete. Thanks! 38 | 39 | 40 | Version 1.5, 7 January 2010: 41 | ---------------------------- 42 | 43 | * Support for Django 1.2 alpha, no more patch required. 44 | * Handle ManyToMany relations only with the required "through" argument, 45 | it allows you to define a URL for this resource. 46 | * Update restkit to the latest tip (>0.9.2) 47 | * Update piston to the latest tip (>0.2.3rc1) 48 | * Deprecate "remoteauth" application for now, mapping of M2M relations is not 49 | fully compatible, you should still be able to use the User model though but 50 | Permissions and Groups' relation will not be correct anymore. 51 | This is still a work in progress, suggestions welcome! 52 | 53 | 54 | Version 1.4, 15 September 2009: 55 | ------------------------------- 56 | 57 | * Support for named relations. 58 | * Support for proxy models. 59 | * Fix a bug to deal with ModelForms 60 | * Switch from restclient to the latest tip of restkit (>0.8.2) 61 | * Update piston to the latest tip (>0.2.3rc1) 62 | * Introduce a new settings, ROA_HEADERS, to specify headers sent to the server 63 | * Better errors' feedback from the server 64 | 65 | 66 | Version 1.3, 25 May 2009: 67 | ------------------------- 68 | 69 | * First Open-Source release! (BSD license) 70 | * Handle ManyToMany relations in a RESTful way. 71 | * Support for unicode. 72 | * Support of custom de/serialization improved. 73 | * Addition of a Django command (inspectresources). 74 | * Add a couple of useful new settings (see documentation). 75 | * Switch tests to django-piston (0.2.1) and to unittests, use of logging. 76 | * Update py-restclient to the latest revision (1.3.2). 77 | * Backward incompatible changes: count is now handled by it's own URL via 78 | get_resource_url_count (default is get_resource_url_list/count/ but you can 79 | customize it). 80 | 81 | 82 | Version 1.2, 30 April 2009: 83 | --------------------------- 84 | 85 | * Update (Null)Boolean fields, Django doesn't allow anymore null=True. 86 | * Move documentation to BitBucket's wiki. 87 | * Update py-restclient to the latest revision (1.3). 88 | 89 | 90 | Version 1.1, 28 February 2009: 91 | ------------------------------ 92 | 93 | * Add support for Groups and Permissions in remoteauth application (still 94 | depends on builtin Django's ContentTypes). 95 | * Add support for ModelForms with complex relations. 96 | * Add an example to document the use of a custom serializer. 97 | * Update py-restclient to the latest revision. 98 | * Backward incompatible changes: RemoteUser model has been renamed to User and 99 | ROAUserManager to UserManager to be consistent with the existing auth app. 100 | 101 | 102 | Version 1.0, 23 January 2009: 103 | ----------------------------- 104 | 105 | * Add support for many-to-many relations. 106 | * Warning: Many-to-many relations depends on Django's issue #10109, apply the 107 | attached patch if you need it. 108 | * Update py-restclient to 1.1.4. 109 | 110 | 111 | Version 0.9, 9 January 2009: 112 | ---------------------------- 113 | 114 | * Ease subclassing of MethodDispatcher with custom slugs. 115 | 116 | 117 | Version 0.8, 7 January 2009: 118 | ---------------------------- 119 | 120 | * Add support for admin options except search_fields because of advanced 121 | querysets based on Q objects. 122 | * Support for ManyToMany relations in progress. 123 | * Warning: trunk version of Django is required for now, prior to revision 9695 124 | in order to handle FloatFields 125 | * Bugfixes: ROA_URL_OVERRIDES_* settings are optionnal and ForeignKey fields 126 | should work as expected. 127 | * Backward incompatible change: admin classes must inherit from 128 | django_roa.ModelAdmin to be consistent (see documentation and example). 129 | 130 | 131 | Version 0.7, 2 January 2009: 132 | ---------------------------- 133 | 134 | * Add support for most useful fields (see specifications). 135 | * Warning: there are some Django bugs (hopefully with patches) which are 136 | required in order to use BooleanFields with None values and JSON 137 | serialization (#5563) or FloatFields which are considered as unicode 138 | (#9942). Patch your Django installation if you need those ones, that's why 139 | actual tests failed. 140 | 141 | 142 | Version 0.6, 28 December 2008: 143 | ------------------------------ 144 | 145 | * Clean up remoteauth application. 146 | * Update py-restclient to 1.0.1. 147 | 148 | 149 | Version 0.5, 27 December 2008: 150 | ------------------------------ 151 | 152 | * Backward incompatible change: Remote* classes had been renamed to ROA*, this 153 | is an internal change which should not affect your code. 154 | * Add support for ROA_URL_OVERRIDES_* settings and a complete example. 155 | * Declaration of default manager is no more required, if you inherit from 156 | django_roa.Model it will be added automatically given your ROA_MODELS 157 | setting. 158 | * Use the latest version of restclient (0.2.1), no more httplib2 dependency. 159 | This version uses pycurl, urllib2 or httplib2 given your configuration, in 160 | this order of preferences. 161 | 162 | 163 | Version 0.4, 27 December 2008: 164 | ------------------------------ 165 | 166 | * Backward incompatible change: resource_url_list is no more defined in Meta 167 | class, now you must define your own get_resource_url_list static method in 168 | your Model class. resource_url_detail has been renamed as 169 | get_resource_url_detail for consistency and is no more a property. 170 | * Add support for ForeignKeys and XML serialization. 171 | * Bugfixes: DateTimeField and BooleanField should work as expected. 172 | * Add a complete example with a custom slug for multiple primary keys. 173 | * Ease debugging with a summary displayed if you use the test server instead 174 | of the whole HTML error. 175 | 176 | 177 | Version 0.3, 23 December 2008: 178 | ------------------------------ 179 | 180 | * Backward incompatible change: resource_url_id is no more defined in Meta 181 | class, now you can define your own resource_url_detail property in your 182 | Model class. resource_url Meta attribute has been renamed as 183 | resource_url_list for consistency. 184 | * Bugfixes: RemoteQuerySet.count() and BooleanField should work as expected. 185 | * Better documentation, still need improvements. 186 | 187 | 188 | Version 0.2, 15 December 2008: 189 | ------------------------------ 190 | 191 | Include restclient dependency. 192 | 193 | 194 | Version 0.1, 12 December 2008: 195 | ------------------------------ 196 | 197 | Initial release. 198 | -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap setuptools installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import sys 17 | DEFAULT_VERSION = "0.6c9" 18 | DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] 19 | 20 | md5_data = { 21 | 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 22 | 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 23 | 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 24 | 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 25 | 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 26 | 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 27 | 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 28 | 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 29 | 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 30 | 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 31 | 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 32 | 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 33 | 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 34 | 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 35 | 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 36 | 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 37 | 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 38 | 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 39 | 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 40 | 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 41 | 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 42 | 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 43 | 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 44 | 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 45 | 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 46 | 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 47 | 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 48 | 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 49 | 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 50 | 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 51 | 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 52 | 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 53 | 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 54 | 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', 55 | } 56 | 57 | import sys, os 58 | try: from hashlib import md5 59 | except ImportError: from md5 import md5 60 | 61 | def _validate_md5(egg_name, data): 62 | if egg_name in md5_data: 63 | digest = md5(data).hexdigest() 64 | if digest != md5_data[egg_name]: 65 | print >>sys.stderr, ( 66 | "md5 validation of %s failed! (Possible download problem?)" 67 | % egg_name 68 | ) 69 | sys.exit(2) 70 | return data 71 | 72 | def use_setuptools( 73 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 74 | download_delay=15 75 | ): 76 | """Automatically find/download setuptools and make it available on sys.path 77 | 78 | `version` should be a valid setuptools version number that is available 79 | as an egg for download under the `download_base` URL (which should end with 80 | a '/'). `to_dir` is the directory where setuptools will be downloaded, if 81 | it is not already available. If `download_delay` is specified, it should 82 | be the number of seconds that will be paused before initiating a download, 83 | should one be required. If an older version of setuptools is installed, 84 | this routine will print a message to ``sys.stderr`` and raise SystemExit in 85 | an attempt to abort the calling script. 86 | """ 87 | was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules 88 | def do_download(): 89 | egg = download_setuptools(version, download_base, to_dir, download_delay) 90 | sys.path.insert(0, egg) 91 | import setuptools; setuptools.bootstrap_install_from = egg 92 | try: 93 | import pkg_resources 94 | except ImportError: 95 | return do_download() 96 | try: 97 | pkg_resources.require("setuptools>="+version); return 98 | except pkg_resources.VersionConflict, e: 99 | if was_imported: 100 | print >>sys.stderr, ( 101 | "The required version of setuptools (>=%s) is not available, and\n" 102 | "can't be installed while this script is running. Please install\n" 103 | " a more recent version first, using 'easy_install -U setuptools'." 104 | "\n\n(Currently using %r)" 105 | ) % (version, e.args[0]) 106 | sys.exit(2) 107 | else: 108 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 109 | return do_download() 110 | except pkg_resources.DistributionNotFound: 111 | return do_download() 112 | 113 | def download_setuptools( 114 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 115 | delay = 15 116 | ): 117 | """Download setuptools from a specified location and return its filename 118 | 119 | `version` should be a valid setuptools version number that is available 120 | as an egg for download under the `download_base` URL (which should end 121 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 122 | `delay` is the number of seconds to pause before an actual download attempt. 123 | """ 124 | import urllib2, shutil 125 | egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) 126 | url = download_base + egg_name 127 | saveto = os.path.join(to_dir, egg_name) 128 | src = dst = None 129 | if not os.path.exists(saveto): # Avoid repeated downloads 130 | try: 131 | from distutils import log 132 | if delay: 133 | log.warn(""" 134 | --------------------------------------------------------------------------- 135 | This script requires setuptools version %s to run (even to display 136 | help). I will attempt to download it for you (from 137 | %s), but 138 | you may need to enable firewall access for this script first. 139 | I will start the download in %d seconds. 140 | 141 | (Note: if this machine does not have network access, please obtain the file 142 | 143 | %s 144 | 145 | and place it in this directory before rerunning this script.) 146 | ---------------------------------------------------------------------------""", 147 | version, download_base, delay, url 148 | ); from time import sleep; sleep(delay) 149 | log.warn("Downloading %s", url) 150 | src = urllib2.urlopen(url) 151 | # Read/write all in one block, so we don't create a corrupt file 152 | # if the download is interrupted. 153 | data = _validate_md5(egg_name, src.read()) 154 | dst = open(saveto,"wb"); dst.write(data) 155 | finally: 156 | if src: src.close() 157 | if dst: dst.close() 158 | return os.path.realpath(saveto) 159 | 160 | def main(argv, version=DEFAULT_VERSION): 161 | """Install or upgrade setuptools and EasyInstall""" 162 | try: 163 | import setuptools 164 | except ImportError: 165 | egg = None 166 | try: 167 | egg = download_setuptools(version, delay=0) 168 | sys.path.insert(0,egg) 169 | from setuptools.command.easy_install import main 170 | return main(list(argv)+[egg]) # we're done here 171 | finally: 172 | if egg and os.path.exists(egg): 173 | os.unlink(egg) 174 | else: 175 | if setuptools.__version__ == '0.0.1': 176 | print >>sys.stderr, ( 177 | "You have an obsolete version of setuptools installed. Please\n" 178 | "remove it from your system entirely before rerunning this script." 179 | ) 180 | sys.exit(2) 181 | 182 | req = "setuptools>="+version 183 | import pkg_resources 184 | try: 185 | pkg_resources.require(req) 186 | except pkg_resources.VersionConflict: 187 | try: 188 | from setuptools.command.easy_install import main 189 | except ImportError: 190 | from easy_install import main 191 | main(list(argv)+[download_setuptools(delay=0)]) 192 | sys.exit(0) # try to force an exit 193 | else: 194 | if argv: 195 | from setuptools.command.easy_install import main 196 | main(argv) 197 | else: 198 | print "Setuptools version",version,"or greater has been installed." 199 | print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' 200 | 201 | def update_md5(filenames): 202 | """Update our built-in md5 registry""" 203 | 204 | import re 205 | 206 | for name in filenames: 207 | base = os.path.basename(name) 208 | f = open(name,'rb') 209 | md5_data[base] = md5(f.read()).hexdigest() 210 | f.close() 211 | 212 | data = [" %r: %r,\n" % it for it in md5_data.items()] 213 | data.sort() 214 | repl = "".join(data) 215 | 216 | import inspect 217 | srcfile = inspect.getsourcefile(sys.modules[__name__]) 218 | f = open(srcfile, 'rb'); src = f.read(); f.close() 219 | 220 | match = re.search("\nmd5_data = {\n([^}]+)}", src) 221 | if not match: 222 | print >>sys.stderr, "Internal error!" 223 | sys.exit(2) 224 | 225 | src = src[:match.start(1)] + repl + src[match.end(1):] 226 | f = open(srcfile,'w') 227 | f.write(src) 228 | f.close() 229 | 230 | 231 | if __name__=='__main__': 232 | if len(sys.argv)>2 and sys.argv[1]=='--md5update': 233 | update_md5(sys.argv[2:]) 234 | else: 235 | main(sys.argv[1:]) 236 | -------------------------------------------------------------------------------- /examples/django_roa_server/handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.auth.models import User, Group, Permission 4 | try: 5 | from django.contrib.auth.models import Message 6 | except ImportError: 7 | Message = None 8 | from django.db import models 9 | from django.http import Http404 10 | from django.shortcuts import get_object_or_404, _get_queryset 11 | 12 | from piston.handler import BaseHandler, AnonymousBaseHandler 13 | from piston.utils import rc 14 | 15 | from django_roa_server.models import RemotePage, RemotePageWithManyFields, \ 16 | RemotePageWithBooleanFields, RemotePageWithCustomSlug, \ 17 | RemotePageWithOverriddenUrls, RemotePageWithRelations, \ 18 | RemotePageWithNamedRelations, RemotePageWithRelationsThrough, \ 19 | RemotePageWithCustomPrimaryKey 20 | 21 | logger = logging.getLogger("django_roa_server") 22 | 23 | 24 | class ROAHandler(BaseHandler): 25 | 26 | def flatten_dict(self, dct): 27 | return dict([ (str(k), dct.get(k)) for k in dct.keys() \ 28 | if (k, dct.get(k)) != (u'id', u'None') and (k, dct.get(k)) != (u'pk', u'None')]) 29 | 30 | @staticmethod 31 | def _get_object(model, *args, **kwargs): 32 | return get_object_or_404(model, pk=kwargs['pk']) 33 | 34 | def read(self, request, *args, **kwargs): 35 | """ 36 | Retrieves an object or a list of objects. 37 | """ 38 | if not self.has_model(): 39 | return rc.NOT_IMPLEMENTED 40 | 41 | logger.debug('Args: %s' % str(args)) 42 | logger.debug('Kwargs: %s' % str(kwargs)) 43 | 44 | if kwargs.values() != [None]: 45 | # Returns a single object 46 | return [self._get_object(self.model, *args, **kwargs)] 47 | 48 | # Initialization 49 | queryset = _get_queryset(self.model) 50 | logger.debug('Before filters: %s' % str(queryset)) 51 | 52 | # Filtering 53 | filters, excludes = {}, {} 54 | for k, v in request.GET.iteritems(): 55 | if k.startswith('filter_'): 56 | filters[k[7:]] = v 57 | if k.startswith('exclude_'): 58 | excludes[k[8:]] = v 59 | queryset = queryset.filter(*filters.items()).exclude(*excludes.items()) 60 | 61 | logger.debug('Filters: %s' % str(filters)) 62 | logger.debug('Excludes: %s' % str(excludes)) 63 | logger.debug('After filters: %s' % str(queryset)) 64 | 65 | # Ordering (test custom parameters' name) 66 | if 'order' in request.GET: 67 | order_bys = request.GET['order'].split(',') 68 | queryset = queryset.order_by(*order_bys) 69 | 70 | # Slicing 71 | limit_start = int(request.GET.get('limit_start', 0)) 72 | limit_stop = request.GET.get('limit_stop', False) and int(request.GET['limit_stop']) or None 73 | queryset = queryset[limit_start:limit_stop] 74 | 75 | obj_list = list(queryset) 76 | if not obj_list: 77 | raise Http404('No %s matches the given query.' % queryset.model._meta.object_name) 78 | logger.debug(u'Objects: %s retrieved' % [unicode(obj) for obj in obj_list]) 79 | return queryset 80 | 81 | def create(self, request, *args, **kwargs): 82 | """ 83 | Creates an object given request args, returned as a list. 84 | """ 85 | if not self.has_model(): 86 | return rc.NOT_IMPLEMENTED 87 | 88 | data = request.POST.copy() 89 | 90 | values = {} 91 | for field in self.model._meta.local_fields: 92 | field_value = data.get(field.name, None) 93 | 94 | if field_value not in (u'', u'None'): 95 | 96 | # Handle FK fields 97 | if field.rel and isinstance(field.rel, models.ManyToOneRel): 98 | field_value = data.get(field.attname, None) 99 | if field_value not in (u'', u'None'): 100 | values[field.attname] = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value) 101 | else: 102 | values[field.attname] = None 103 | 104 | # Handle all other fields 105 | else: 106 | if isinstance(field, models.fields.BooleanField): 107 | field_value = field.to_python(field_value) 108 | elif isinstance(field, models.fields.FloatField): 109 | if field_value is not None: 110 | field_value = float(field_value) 111 | values[field.name] = field_value 112 | 113 | obj = self.model.objects.create(**values) 114 | 115 | response = [self.model.objects.get(pk=obj.pk)] 116 | logger.debug(u'Object "%s" created' % unicode(obj)) 117 | return response 118 | 119 | def update(self, request, *args, **kwargs): 120 | """ 121 | Modifies an object given request args, returned as a list. 122 | """ 123 | if not self.has_model(): 124 | return rc.NOT_IMPLEMENTED 125 | 126 | data = request.PUT.copy() 127 | logger.debug(u'Received: %s as PUT data' % data) 128 | obj = self._get_object(self.model, *args, **kwargs) 129 | 130 | for field in self.model._meta.local_fields: 131 | field_name = field.name 132 | 133 | # Handle FK fields 134 | if field.rel and isinstance(field.rel, models.ManyToOneRel): 135 | field_value = data.get(field.attname, None) 136 | if field_value not in (u'', u'None', None): 137 | field_value = field.rel.to._meta.get_field(field.rel.field_name).to_python(field_value) 138 | else: 139 | field_value = None 140 | setattr(obj, field.attname, field_value) 141 | 142 | # Handle all other fields 143 | elif field_name in data: 144 | field_value = data[field_name] 145 | if field_value in (u'', u'None'): 146 | field_value = None 147 | if isinstance(field, models.fields.BooleanField) \ 148 | or isinstance(field, models.fields.NullBooleanField) \ 149 | or isinstance(field, models.fields.IntegerField): 150 | field_value = field.to_python(field_value) 151 | elif isinstance(field, models.fields.FloatField): 152 | if field_value is not None: 153 | field_value = float(field_value) 154 | elif isinstance(field, models.fields.CharField): 155 | if field_value is None: 156 | field_value = u'' 157 | setattr(obj, field_name, field_value) 158 | 159 | obj.save() 160 | 161 | response = [self.model.objects.get(pk=obj.pk)] 162 | logger.debug(u'Object "%s" modified with %s' % ( 163 | unicode(obj), unicode(data.items()))) 164 | return response 165 | 166 | def delete(self, request, *args, **kwargs): 167 | """ 168 | Deletes an object. 169 | """ 170 | if not self.has_model(): 171 | raise NotImplementedError 172 | 173 | try: 174 | obj = self._get_object(self.model, *args, **kwargs) 175 | obj.delete() 176 | logger.debug(u'Object "%s" deleted, remains %s' % ( 177 | unicode(obj), 178 | [unicode(obj) for obj in self.model.objects.all()])) 179 | 180 | return rc.DELETED 181 | except self.model.MultipleObjectsReturned: 182 | return rc.DUPLICATE_ENTRY 183 | except self.model.DoesNotExist: 184 | return rc.NOT_HERE 185 | 186 | 187 | class ROACountHandler(BaseHandler): 188 | allowed_methods = ('GET', ) 189 | 190 | def read(self, request, *args, **kwargs): 191 | """ 192 | Retrieves the number of objects. 193 | """ 194 | if not self.has_model(): 195 | return rc.NOT_IMPLEMENTED 196 | 197 | # Initialization 198 | queryset = _get_queryset(self.model) 199 | 200 | # Filtering 201 | filters, excludes = {}, {} 202 | for k, v in request.GET.iteritems(): 203 | if k.startswith('filter_'): 204 | filters[k[7:]] = v 205 | if k.startswith('exclude_'): 206 | excludes[k[8:]] = v 207 | queryset = queryset.filter(*filters.items()).exclude(*excludes.items()) 208 | 209 | # Ordering 210 | if 'order_by' in request.GET: 211 | order_bys = request.GET['order_by'].split(',') 212 | queryset = queryset.order_by(*order_bys) 213 | 214 | # Counting 215 | counter = queryset.count() 216 | logger.debug(u'Count: %s objects' % counter) 217 | return counter 218 | 219 | 220 | class ROAWithSlugHandler(ROAHandler): 221 | 222 | @staticmethod 223 | def _get_object(model, *args, **kwargs): 224 | """Returns an object from a slug. 225 | 226 | Useful when the slug is a combination of many fields. 227 | """ 228 | pk, slug = kwargs['object_slug'].split('-', 1) 229 | obj = get_object_or_404(model, pk=pk) 230 | return obj 231 | 232 | 233 | class RemotePageHandler(ROAHandler): 234 | model = RemotePage 235 | 236 | class RemotePageCountHandler(ROACountHandler): 237 | model = RemotePage 238 | 239 | 240 | class RemotePageWithManyFieldsHandler(ROAHandler): 241 | model = RemotePageWithManyFields 242 | 243 | class RemotePageWithManyFieldsCountHandler(ROACountHandler): 244 | model = RemotePageWithManyFields 245 | 246 | 247 | class RemotePageWithBooleanFieldsHandler(ROAHandler): 248 | model = RemotePageWithBooleanFields 249 | 250 | class RemotePageWithBooleanFieldsCountHandler(ROACountHandler): 251 | model = RemotePageWithBooleanFields 252 | 253 | 254 | class RemotePageWithCustomSlugHandler(ROAWithSlugHandler): 255 | model = RemotePageWithCustomSlug 256 | 257 | class RemotePageWithCustomSlugCountHandler(ROACountHandler): 258 | model = RemotePageWithCustomSlug 259 | 260 | class RemotePageWithCustomPrimaryKeyHandler(ROAHandler): 261 | model = RemotePageWithCustomPrimaryKey 262 | 263 | class RemotePageWithCustomPrimaryKeyCountHandler(ROACountHandler): 264 | model = RemotePageWithCustomPrimaryKey 265 | 266 | class RemotePageWithCustomPrimaryKeyCount2Handler(ROACountHandler): 267 | model = RemotePageWithCustomPrimaryKey 268 | 269 | def read(self, request, *args, **kwargs): 270 | return 'invalid counter' 271 | 272 | 273 | class RemotePageWithOverriddenUrlsHandler(ROAWithSlugHandler): 274 | model = RemotePageWithOverriddenUrls 275 | 276 | class RemotePageWithOverriddenUrlsCountHandler(ROACountHandler): 277 | model = RemotePageWithOverriddenUrls 278 | 279 | 280 | class RemotePageWithRelationsHandler(ROAHandler): 281 | model = RemotePageWithRelations 282 | 283 | class RemotePageWithRelationsCountHandler(ROACountHandler): 284 | model = RemotePageWithRelations 285 | 286 | 287 | class RemotePageWithRelationsThroughHandler(ROAHandler): 288 | model = RemotePageWithRelationsThrough 289 | 290 | class RemotePageWithRelationsThroughCountHandler(ROACountHandler): 291 | model = RemotePageWithRelationsThrough 292 | 293 | 294 | class RemotePageWithNamedRelationsHandler(ROAHandler): 295 | model = RemotePageWithNamedRelations 296 | 297 | class RemotePageWithNamedRelationsCountHandler(ROACountHandler): 298 | model = RemotePageWithNamedRelations 299 | 300 | 301 | class UserHandler(ROAHandler): 302 | model = User 303 | 304 | MessageHandler = None 305 | if Message: 306 | class MessageHandler(ROAHandler): 307 | model = Message 308 | 309 | class PermissionHandler(ROAHandler): 310 | model = Permission 311 | 312 | class GroupHandler(ROAHandler): 313 | model = Group 314 | -------------------------------------------------------------------------------- /django_roa/db/query.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from StringIO import StringIO 3 | 4 | from django.conf import settings 5 | from django.db.models import query 6 | from django.core import serializers 7 | # Django >= 1.5 8 | from django_roa.db import get_roa_headers 9 | 10 | try: 11 | from django.db.models.constants import LOOKUP_SEP 12 | #Django < 1.4 13 | except: 14 | from django.db.models.sql.constants import LOOKUP_SEP 15 | from django.db.models.query_utils import Q 16 | from django.utils.encoding import force_unicode 17 | 18 | from restkit import Resource, ResourceNotFound 19 | from django_roa.db.exceptions import ROAException, ROANotImplementedYetException 20 | 21 | logger = logging.getLogger("django_roa") 22 | 23 | ROA_MODEL_NAME_MAPPING = getattr(settings, 'ROA_MODEL_NAME_MAPPING', []) 24 | ROA_ARGS_NAMES_MAPPING = getattr(settings, 'ROA_ARGS_NAMES_MAPPING', {}) 25 | ROA_FORMAT = getattr(settings, 'ROA_FORMAT', 'json') 26 | ROA_FILTERS = getattr(settings, 'ROA_FILTERS', {}) 27 | ROA_SSL_ARGS = getattr(settings, 'ROA_SSL_ARGS', {}) 28 | 29 | DEFAULT_CHARSET = getattr(settings, 'DEFAULT_CHARSET', 'utf-8') 30 | 31 | 32 | class Query(object): 33 | def __init__(self): 34 | self.order_by = [] 35 | self.extra_order_by = [] 36 | self.default_ordering = [] 37 | self.filters = {} 38 | self.excludes = {} 39 | self.search_term = None 40 | self.filterable = True 41 | self.limit_start = None 42 | self.limit_stop = None 43 | self.where = False 44 | self.select_related = False 45 | self.max_depth = None 46 | self.extra_select = {} 47 | self.select_for_update = False 48 | 49 | def can_filter(self): 50 | return self.filterable 51 | 52 | def clone(self): 53 | return self 54 | 55 | def clear_ordering(self): 56 | self.order_by = [] 57 | 58 | def filter(self, *args, **kwargs): 59 | self.filters.update(kwargs) 60 | 61 | def search(self, search_term, limit_start=None, limit_stop=None): 62 | self.search_term = search_term 63 | self.limit_start = limit_start 64 | self.limit_stop = limit_stop 65 | 66 | def exclude(self, *args, **kwargs): 67 | self.excludes.update(kwargs) 68 | 69 | def set_limits(self, start=None, stop=None): 70 | self.limit_start = start 71 | self.limit_stop = stop 72 | self.filterable = False 73 | 74 | def add_select_related(self, fields): 75 | """ 76 | Sets up the select_related data structure so that we only select 77 | certain related models (as opposed to all models, when 78 | self.select_related=True). 79 | """ 80 | field_dict = {} 81 | for field in fields: 82 | d = field_dict 83 | for part in field.split(LOOKUP_SEP): 84 | d = d.setdefault(part, {}) 85 | self.select_related = field_dict 86 | self.related_select_cols = [] 87 | self.related_select_fields = [] 88 | 89 | @property 90 | def parameters(self): 91 | """ 92 | Returns useful parameters as a dictionary. 93 | """ 94 | parameters = {} 95 | 96 | # Filtering 97 | for k, v in self.filters.iteritems(): 98 | key = '%s%s' % (ROA_ARGS_NAMES_MAPPING.get('FILTER_', 'filter_'), k) 99 | # v could be an object 100 | try: 101 | v = v.id 102 | except: 103 | pass 104 | 105 | if key in ROA_ARGS_NAMES_MAPPING: 106 | parameters[ROA_ARGS_NAMES_MAPPING[key]] = v 107 | else: 108 | parameters[key] = v 109 | for k, v in self.excludes.iteritems(): 110 | key = '%s%s' % (ROA_ARGS_NAMES_MAPPING.get('EXCLUDE_', 'exclude_'), k) 111 | if key in ROA_ARGS_NAMES_MAPPING: 112 | parameters[ROA_ARGS_NAMES_MAPPING[key]] = v 113 | else: 114 | parameters[key] = v 115 | if self.search_term: 116 | parameters[ROA_ARGS_NAMES_MAPPING.get('SEARCH_', 'search')] = self.search_term 117 | 118 | # Ordering 119 | if self.order_by: 120 | order_by = ','.join(self.order_by) 121 | parameters[ROA_ARGS_NAMES_MAPPING.get('ORDER_BY', 'order_by')] = order_by 122 | 123 | # Slicing 124 | if self.limit_start: 125 | parameters[ROA_ARGS_NAMES_MAPPING.get('LIMIT_START', 'limit_start')] = self.limit_start 126 | if self.limit_stop: 127 | parameters[ROA_ARGS_NAMES_MAPPING.get('LIMIT_STOP', 'limit_stop')] = self.limit_stop 128 | 129 | # Format 130 | parameters[ROA_ARGS_NAMES_MAPPING.get('FORMAT', 'format')] = ROA_FORMAT 131 | 132 | parameters.update(getattr(settings, 'ROA_CUSTOM_ARGS', {})) 133 | return parameters 134 | 135 | ########################################## 136 | # Fake methods required by admin options # 137 | ########################################## 138 | 139 | def add_fields(self, field_names, allow_m2m=True): 140 | """ Fake method. """ 141 | pass 142 | 143 | def trim_extra_select(self, names): 144 | """ Fake method. """ 145 | pass 146 | 147 | def results_iter(self): 148 | """ Fake method. """ 149 | return [] 150 | 151 | def combine(self, rhs, connector): 152 | """ Fake method. """ 153 | pass 154 | 155 | def has_results(self, *args, **kwargs): 156 | """ Fake method. """ 157 | return True 158 | 159 | 160 | class RemoteQuerySet(query.QuerySet): 161 | """ 162 | QuerySet which access remote resources. 163 | """ 164 | def __init__(self, model=None, query=None): 165 | self.model = model 166 | self.query = query or Query() 167 | self._result_cache = None 168 | self._iter = None 169 | self._sticky_filter = False 170 | self._db = False 171 | self._for_write = False 172 | self._hints = {} 173 | 174 | self.params = {} 175 | 176 | self._prefetch_related_lookups = False 177 | 178 | ######################## 179 | # PYTHON MAGIC METHODS # 180 | ######################## 181 | 182 | def __repr__(self): 183 | if not self.query.limit_start and not self.query.limit_stop: 184 | data = list(self[:query.REPR_OUTPUT_SIZE + 1]) 185 | if len(data) > query.REPR_OUTPUT_SIZE: 186 | data[-1] = "...(remaining elements truncated)..." 187 | else: 188 | data = list(self) 189 | return repr(data) 190 | 191 | #################################### 192 | # METHODS THAT DO RESOURCE QUERIES # 193 | #################################### 194 | 195 | def iterator(self): 196 | """ 197 | An iterator over the results from applying this QuerySet to the 198 | remote web service. 199 | """ 200 | resource = Resource(self.model.get_resource_url_list(), 201 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 202 | try: 203 | parameters = self.query.parameters 204 | logger.debug(u"""Requesting: "%s" through %s with parameters "%s" """ % ( 205 | self.model.__name__, 206 | resource.uri, 207 | force_unicode(parameters))) 208 | response = resource.get(headers=self._get_http_headers(), **parameters) 209 | except ResourceNotFound: 210 | return 211 | except Exception as e: 212 | raise ROAException(e) 213 | 214 | response = force_unicode(response.body_string()).encode(DEFAULT_CHARSET) 215 | 216 | # Deserializing objects: 217 | data = self.model.get_parser().parse(StringIO(response)) 218 | 219 | # Check limit_start and limit_stop arguments for pagination and only 220 | # slice data if they are both numeric and there are results left to go. 221 | # We only perform this check on lists. 222 | limit_start = getattr(self.query, 'limit_start', None) 223 | limit_stop = getattr(self.query, 'limit_stop', None) 224 | if (isinstance(limit_start, int) and isinstance(limit_stop, int) and 225 | limit_stop - limit_start < len(data) and limit_stop <= len(data) and 226 | isinstance(data, list)): 227 | data = data[limit_start:limit_stop] 228 | 229 | # [] is the case of empty no-paginated result 230 | if data != []: 231 | serializer = self.model.get_serializer(data=data) 232 | if not serializer.is_valid(): 233 | raise ROAException(u'Invalid deserialization for %s model: %s' % (self.model, serializer.errors)) 234 | 235 | for obj in serializer.object: 236 | yield obj 237 | 238 | def count(self): 239 | """ 240 | Returns the number of records as an integer. 241 | 242 | The result is not cached nor comes from cache, cache must be handled 243 | by the server. 244 | """ 245 | clone = self._clone() 246 | 247 | # Instantiation of clone.model is necessary because we can't set 248 | # a staticmethod for get_resource_url_count and avoid to set it 249 | # for all model without relying on get_resource_url_list 250 | instance = clone.model() 251 | resource = Resource(instance.get_resource_url_count(), 252 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 253 | try: 254 | parameters = clone.query.parameters 255 | logger.debug(u"""Counting : "%s" through %s with parameters "%s" """ % ( 256 | clone.model.__name__, 257 | resource.uri, 258 | force_unicode(parameters))) 259 | response = resource.get(headers=self._get_http_headers(), **parameters) 260 | except Exception as e: 261 | raise ROAException(e) 262 | 263 | response = force_unicode(response.body_string()).encode(DEFAULT_CHARSET) 264 | data = self.model.get_parser().parse(StringIO(response)) 265 | return self.model.count_response(data) 266 | 267 | def _get_from_id_or_pk(self, id=None, pk=None, **kwargs): 268 | """ 269 | Returns an object given an id or pk, request directly with the 270 | get_resource_url_detail method without filtering on ids 271 | (as Django's ORM do). 272 | """ 273 | clone = self._clone() 274 | 275 | # Instantiation of clone.model is necessary because we can't set 276 | # a staticmethod for get_resource_url_detail and avoid to set it 277 | # for all model without relying on get_resource_url_list 278 | instance = clone.model() 279 | if pk is None: 280 | instance.id = id 281 | else: 282 | instance.pk = pk 283 | extra_args = {} 284 | extra_args.update(kwargs) 285 | extra_args.update(ROA_SSL_ARGS) 286 | resource = Resource(instance.get_resource_url_detail(), 287 | filters=ROA_FILTERS, 288 | **extra_args) 289 | try: 290 | parameters = clone.query.parameters 291 | logger.debug(u"""Retrieving : "%s" through %s with parameters "%s" """ % ( 292 | clone.model.__name__, 293 | resource.uri, 294 | force_unicode(parameters))) 295 | response = resource.get(headers=self._get_http_headers(), **parameters) 296 | except Exception as e: 297 | raise ROAException(e) 298 | 299 | response = force_unicode(response.body_string()).encode(DEFAULT_CHARSET) 300 | 301 | for local_name, remote_name in ROA_MODEL_NAME_MAPPING: 302 | response = response.replace(remote_name, local_name) 303 | 304 | # Deserializing objects: 305 | data = self.model.get_parser().parse(StringIO(response)) 306 | serializer = self.model.get_serializer(data=data) 307 | if not serializer.is_valid(): 308 | raise ROAException(u'Invalid deserialization for %s model: %s' % (self.model, serializer.errors)) 309 | 310 | return serializer.object 311 | 312 | def get(self, *args, **kwargs): 313 | """ 314 | Performs the query and returns a single object matching the given 315 | keyword arguments. 316 | """ 317 | # special case, get(id=X) directly request the resource URL and do not 318 | # filter on ids like Django's ORM do. 319 | 320 | # keep the custom attribute name of model for later use 321 | custom_pk = self.model._meta.pk.attname 322 | # search PK, ID or custom PK attribute name for exact match and get set 323 | # of unique matches 324 | attributes_set = set(attr for attr in ['id__exact', 'pk__exact', '%s__exact' % custom_pk] if attr in kwargs.keys()) 325 | exact_match = list(attributes_set) 326 | # common way of getting particular object 327 | if kwargs.keys() == ['id']: 328 | return self._get_from_id_or_pk(id=kwargs['id']) 329 | # useful for admin which relies on PKs 330 | elif kwargs.keys() == ['pk']: 331 | return self._get_from_id_or_pk(pk=kwargs['pk']) 332 | # check the case of PK attribute with custom name 333 | elif kwargs.keys() == [custom_pk]: 334 | return self._get_from_id_or_pk(pk=kwargs[custom_pk]) 335 | # check if there's an exact match filter 336 | elif len(exact_match) == 1: 337 | # use the value of exact match filter to retrieve object by PK 338 | return self._get_from_id_or_pk(pk=kwargs[exact_match[0]]) 339 | else: 340 | # filter the request rather than retrieve it through get method 341 | return super(RemoteQuerySet, self).get(*args, **kwargs) 342 | 343 | def latest(self, field_name=None): 344 | """ 345 | Returns the latest object, according to the model's 'get_latest_by' 346 | option or optional given field_name. 347 | """ 348 | latest_by = field_name or self.model._meta.get_latest_by 349 | assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model" 350 | 351 | self.query.order_by.append('-%s' % latest_by) 352 | return self.iterator().next() 353 | 354 | def delete(self): 355 | """ 356 | Deletes the records in the current QuerySet. 357 | """ 358 | assert self.query.can_filter(), \ 359 | "Cannot use 'limit' or 'offset' with delete." 360 | 361 | del_query = self._clone() 362 | 363 | # Disable non-supported fields. 364 | del_query.query.select_related = False 365 | del_query.query.clear_ordering() 366 | 367 | for obj in del_query: 368 | obj.delete() 369 | 370 | # Clear the result cache, in case this QuerySet gets reused. 371 | self._result_cache = None 372 | delete.alters_data = True 373 | 374 | ################################################################## 375 | # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET # 376 | ################################################################## 377 | 378 | def filter(self, *args, **kwargs): 379 | """ 380 | Returns a filtered QuerySet instance. 381 | """ 382 | if args or kwargs: 383 | assert self.query.can_filter(), \ 384 | "Cannot filter a query once a slice has been taken." 385 | 386 | clone = self._clone() 387 | clone.query.filter(*args, **kwargs) 388 | return clone 389 | 390 | def exclude(self, *args, **kwargs): 391 | """ 392 | Returns a filtered QuerySet instance. 393 | """ 394 | if args or kwargs: 395 | assert self.query.can_filter(), \ 396 | "Cannot filter a query once a slice has been taken." 397 | 398 | clone = self._clone() 399 | clone.query.exclude(*args, **kwargs) 400 | return clone 401 | 402 | def search(self, search_term, limit_start=None, limit_stop=None, *args, **kwargs): 403 | """ 404 | Returns a filtered QuerySet instance. 405 | """ 406 | assert not(args or kwargs), "Search accept only one arg (search_term) and limit_start/limit_stop kwargs" 407 | 408 | clone = self._clone() 409 | clone.query.search(search_term, limit_start=limit_start, limit_stop=limit_stop) 410 | return clone 411 | 412 | def complex_filter(self, filter_obj): 413 | """ 414 | Returns a new QuerySet instance with filter_obj added to the filters. 415 | 416 | filter_obj can be a Q object (or anything with an add_to_query() 417 | method) or a dictionary of keyword lookup arguments. 418 | 419 | This exists to support framework features such as 'limit_choices_to', 420 | and usually it will be more natural to use other methods. 421 | """ 422 | if isinstance(filter_obj, Q) or hasattr(filter_obj, 'add_to_query'): 423 | raise ROAException('Not implemented yet') 424 | return self.filter(**filter_obj) 425 | 426 | def select_related(self, *fields, **kwargs): 427 | """ 428 | Returns a new QuerySet instance that will select related objects. 429 | 430 | If fields are specified, they must be ForeignKey fields and only those 431 | related objects are included in the selection. 432 | """ 433 | depth = kwargs.pop('depth', 0) 434 | if kwargs: 435 | raise TypeError('Unexpected keyword arguments to select_related: %s' 436 | % (kwargs.keys(),)) 437 | obj = self._clone() 438 | if fields: 439 | if depth: 440 | raise TypeError('Cannot pass both "depth" and fields to select_related()') 441 | obj.query.add_select_related(fields) 442 | else: 443 | obj.query.select_related = True 444 | if depth: 445 | obj.query.max_depth = depth 446 | return obj 447 | 448 | def order_by(self, *field_names): 449 | """ 450 | Returns a QuerySet instance with the ordering changed. 451 | """ 452 | assert self.query.can_filter(), \ 453 | "Cannot reorder a query once a slice has been taken." 454 | 455 | clone = self._clone() 456 | for field_name in field_names: 457 | clone.query.order_by.append(field_name) 458 | return clone 459 | 460 | def extra(self, select=None, where=None, params=None, tables=None, 461 | order_by=None, select_params=None): 462 | """ 463 | Only to handle the case of the "cute trick" used in ModelForms (and 464 | per extension admin) for unique and date constraints. 465 | 466 | Example: ``.extra(select={'a': 1}).values('a').order_by()``. 467 | 468 | http://code.djangoproject.com/browser/django/trunk/django/forms/models.py#L322 469 | is an interesting documentation for details. 470 | """ 471 | assert self.query.can_filter(), \ 472 | "Cannot change a query once a slice has been taken" 473 | if select == {'a': 1}: 474 | # Totally hackish but we need a fake object to deal with 475 | # successive calls to values and order_by based on a count 476 | # which is the less expensive action for our implementation. 477 | class FakeInt(object): 478 | def __init__(self, count): 479 | self.count = count 480 | 481 | def values(self, *fields): 482 | if fields == ('a',): # double check that it's our case 483 | return self 484 | 485 | def order_by(self): 486 | return self.count 487 | 488 | return FakeInt(self.count()) 489 | raise ROANotImplementedYetException('extra is not yet fully implemented.') 490 | 491 | ################### 492 | # PRIVATE METHODS # 493 | ################### 494 | 495 | def _clone(self, klass=None, setup=False, **kwargs): 496 | if klass is None: 497 | klass = self.__class__ 498 | query = self.query.clone() 499 | if self._sticky_filter: 500 | query.filter_is_sticky = True 501 | c = klass(model=self.model, query=query) 502 | c.__dict__.update(kwargs) 503 | if setup and hasattr(c, '_setup_query'): 504 | c._setup_query() 505 | return c 506 | 507 | def _as_url(self): 508 | """ 509 | Returns the internal query's URL and parameters 510 | 511 | as (u'url', {'arg_key': 'arg_value'}). 512 | """ 513 | return self.model.get_resource_url_list(), self.query.parameters 514 | 515 | 516 | def _get_http_headers(self): 517 | return get_roa_headers() 518 | -------------------------------------------------------------------------------- /django_roa/db/models.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import copy 3 | import logging 4 | from StringIO import StringIO 5 | from django.utils import six 6 | 7 | import django 8 | 9 | from django.conf import settings 10 | from django.core import serializers 11 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned,\ 12 | FieldError 13 | from django.db import models 14 | from django.db.models import signals 15 | from django.db.models.options import Options 16 | from django.db.models.loading import register_models, get_model 17 | from django.db.models.base import ModelBase, subclass_exception, \ 18 | get_absolute_url, method_get_order, method_set_order 19 | from django.db.models.fields.related import (OneToOneField, add_lazy_relation) 20 | from django.utils.functional import curry 21 | from django.core.serializers.base import DeserializationError 22 | from django.core.serializers.python import Deserializer as PythonDeserializer, _get_model 23 | 24 | from functools import update_wrapper 25 | 26 | from django.utils.encoding import force_unicode, smart_unicode 27 | from rest_framework.parsers import JSONParser, XMLParser, YAMLParser 28 | from rest_framework.renderers import JSONRenderer, XMLRenderer, YAMLRenderer 29 | 30 | from restkit import Resource, RequestFailed, ResourceNotFound 31 | from django_roa.db import get_roa_headers 32 | from django_roa.db.exceptions import ROAException 33 | 34 | logger = logging.getLogger("django_roa") 35 | 36 | 37 | DJANGO_LT_1_7 = django.VERSION[:2] < (1, 7) 38 | DJANGO_GT_1_4 = django.VERSION[:2] > (1, 4) 39 | 40 | 41 | ROA_ARGS_NAMES_MAPPING = getattr(settings, 'ROA_ARGS_NAMES_MAPPING', {}) 42 | ROA_FORMAT = getattr(settings, 'ROA_FORMAT', 'json') 43 | ROA_FILTERS = getattr(settings, 'ROA_FILTERS', {}) 44 | ROA_MODEL_NAME_MAPPING = getattr(settings, 'ROA_MODEL_NAME_MAPPING', []) 45 | ROA_MODEL_CREATE_MAPPING = getattr(settings, 'ROA_MODEL_CREATE_MAPPING', {}) 46 | ROA_MODEL_UPDATE_MAPPING = getattr(settings, 'ROA_MODEL_UPDATE_MAPPING', {}) 47 | ROA_CUSTOM_ARGS = getattr(settings, "ROA_CUSTOM_ARGS", {}) 48 | ROA_SSL_ARGS = getattr(settings, 'ROA_SSL_ARGS', {}) 49 | 50 | DEFAULT_CHARSET = getattr(settings, 'DEFAULT_CHARSET', 'utf-8') 51 | 52 | 53 | class ROAModelBase(ModelBase): 54 | def __new__(cls, name, bases, attrs): 55 | if DJANGO_LT_1_7: 56 | return cls._new_old_django(name, bases, attrs) 57 | else: 58 | return cls._new_recent_django(name, bases, attrs) 59 | 60 | @classmethod 61 | def _new_recent_django(cls, name, bases, attrs): 62 | """ 63 | Exactly the same except the line with ``isinstance(b, ROAModelBase)``. 64 | """ 65 | import warnings 66 | from django.apps.config import MODELS_MODULE_NAME 67 | from django.apps import apps 68 | from django.utils.deprecation import RemovedInDjango19Warning 69 | 70 | super_new = super(ModelBase, cls).__new__ 71 | 72 | # Also ensure initialization is only performed for subclasses of Model 73 | # (excluding Model class itself). 74 | parents = [b for b in bases if isinstance(b, ROAModelBase)] 75 | if not parents: 76 | return super_new(cls, name, bases, attrs) 77 | 78 | # Create the class. 79 | module = attrs.pop('__module__') 80 | new_class = super_new(cls, name, bases, {'__module__': module}) 81 | attr_meta = attrs.pop('Meta', None) 82 | abstract = getattr(attr_meta, 'abstract', False) 83 | if not attr_meta: 84 | meta = getattr(new_class, 'Meta', None) 85 | else: 86 | meta = attr_meta 87 | base_meta = getattr(new_class, '_meta', None) 88 | 89 | # Look for an application configuration to attach the model to. 90 | app_config = apps.get_containing_app_config(module) 91 | 92 | if getattr(meta, 'app_label', None) is None: 93 | 94 | if app_config is None: 95 | # If the model is imported before the configuration for its 96 | # application is created (#21719), or isn't in an installed 97 | # application (#21680), use the legacy logic to figure out the 98 | # app_label by looking one level up from the package or module 99 | # named 'models'. If no such package or module exists, fall 100 | # back to looking one level up from the module this model is 101 | # defined in. 102 | 103 | # For 'django.contrib.sites.models', this would be 'sites'. 104 | # For 'geo.models.places' this would be 'geo'. 105 | 106 | msg = ( 107 | "Model class %s.%s doesn't declare an explicit app_label " 108 | "and either isn't in an application in INSTALLED_APPS or " 109 | "else was imported before its application was loaded. " % 110 | (module, name)) 111 | if abstract: 112 | msg += "Its app_label will be set to None in Django 1.9." 113 | else: 114 | msg += "This will no longer be supported in Django 1.9." 115 | warnings.warn(msg, RemovedInDjango19Warning, stacklevel=2) 116 | 117 | model_module = sys.modules[new_class.__module__] 118 | package_components = model_module.__name__.split('.') 119 | package_components.reverse() # find the last occurrence of 'models' 120 | try: 121 | app_label_index = package_components.index(MODELS_MODULE_NAME) + 1 122 | except ValueError: 123 | app_label_index = 1 124 | kwargs = {"app_label": package_components[app_label_index]} 125 | 126 | else: 127 | kwargs = {"app_label": app_config.label} 128 | 129 | else: 130 | kwargs = {} 131 | 132 | new_class.add_to_class('_meta', Options(meta, **kwargs)) 133 | if not abstract: 134 | new_class.add_to_class( 135 | 'DoesNotExist', 136 | subclass_exception( 137 | str('DoesNotExist'), 138 | tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,), 139 | module, 140 | attached_to=new_class)) 141 | new_class.add_to_class( 142 | 'MultipleObjectsReturned', 143 | subclass_exception( 144 | str('MultipleObjectsReturned'), 145 | tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,), 146 | module, 147 | attached_to=new_class)) 148 | if base_meta and not base_meta.abstract: 149 | # Non-abstract child classes inherit some attributes from their 150 | # non-abstract parent (unless an ABC comes before it in the 151 | # method resolution order). 152 | if not hasattr(meta, 'ordering'): 153 | new_class._meta.ordering = base_meta.ordering 154 | if not hasattr(meta, 'get_latest_by'): 155 | new_class._meta.get_latest_by = base_meta.get_latest_by 156 | 157 | is_proxy = new_class._meta.proxy 158 | 159 | # If the model is a proxy, ensure that the base class 160 | # hasn't been swapped out. 161 | if is_proxy and base_meta and base_meta.swapped: 162 | raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) 163 | 164 | if getattr(new_class, '_default_manager', None): 165 | if not is_proxy: 166 | # Multi-table inheritance doesn't inherit default manager from 167 | # parents. 168 | new_class._default_manager = None 169 | new_class._base_manager = None 170 | else: 171 | # Proxy classes do inherit parent's default manager, if none is 172 | # set explicitly. 173 | new_class._default_manager = new_class._default_manager._copy_to_model(new_class) 174 | new_class._base_manager = new_class._base_manager._copy_to_model(new_class) 175 | 176 | # Add all attributes to the class. 177 | for obj_name, obj in attrs.items(): 178 | new_class.add_to_class(obj_name, obj) 179 | 180 | # All the fields of any type declared on this model 181 | new_fields = ( 182 | new_class._meta.local_fields + 183 | new_class._meta.local_many_to_many + 184 | new_class._meta.virtual_fields 185 | ) 186 | field_names = set(f.name for f in new_fields) 187 | 188 | # Basic setup for proxy models. 189 | if is_proxy: 190 | base = None 191 | for parent in [kls for kls in parents if hasattr(kls, '_meta')]: 192 | if parent._meta.abstract: 193 | if parent._meta.fields: 194 | raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name) 195 | else: 196 | continue 197 | if base is not None: 198 | raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name) 199 | else: 200 | base = parent 201 | if base is None: 202 | raise TypeError("Proxy model '%s' has no non-abstract model base class." % name) 203 | new_class._meta.setup_proxy(base) 204 | new_class._meta.concrete_model = base._meta.concrete_model 205 | else: 206 | new_class._meta.concrete_model = new_class 207 | 208 | # Collect the parent links for multi-table inheritance. 209 | parent_links = {} 210 | for base in reversed([new_class] + parents): 211 | # Conceptually equivalent to `if base is Model`. 212 | if not hasattr(base, '_meta'): 213 | continue 214 | # Skip concrete parent classes. 215 | if base != new_class and not base._meta.abstract: 216 | continue 217 | # Locate OneToOneField instances. 218 | for field in base._meta.local_fields: 219 | if isinstance(field, OneToOneField): 220 | parent_links[field.rel.to] = field 221 | 222 | # Do the appropriate setup for any model parents. 223 | for base in parents: 224 | original_base = base 225 | if not hasattr(base, '_meta'): 226 | # Things without _meta aren't functional models, so they're 227 | # uninteresting parents. 228 | continue 229 | 230 | parent_fields = base._meta.local_fields + base._meta.local_many_to_many 231 | # Check for clashes between locally declared fields and those 232 | # on the base classes (we cannot handle shadowed fields at the 233 | # moment). 234 | for field in parent_fields: 235 | if field.name in field_names: 236 | raise FieldError( 237 | 'Local field %r in class %r clashes ' 238 | 'with field of similar name from ' 239 | 'base class %r' % (field.name, name, base.__name__) 240 | ) 241 | if not base._meta.abstract: 242 | # Concrete classes... 243 | base = base._meta.concrete_model 244 | if base in parent_links: 245 | field = parent_links[base] 246 | elif not is_proxy: 247 | attr_name = '%s_ptr' % base._meta.model_name 248 | field = OneToOneField(base, name=attr_name, 249 | auto_created=True, parent_link=True) 250 | # Only add the ptr field if it's not already present; 251 | # e.g. migrations will already have it specified 252 | if not hasattr(new_class, attr_name): 253 | new_class.add_to_class(attr_name, field) 254 | else: 255 | field = None 256 | new_class._meta.parents[base] = field 257 | else: 258 | # .. and abstract ones. 259 | for field in parent_fields: 260 | new_class.add_to_class(field.name, copy.deepcopy(field)) 261 | 262 | # Pass any non-abstract parent classes onto child. 263 | new_class._meta.parents.update(base._meta.parents) 264 | 265 | # Inherit managers from the abstract base classes. 266 | new_class.copy_managers(base._meta.abstract_managers) 267 | 268 | # Proxy models inherit the non-abstract managers from their base, 269 | # unless they have redefined any of them. 270 | if is_proxy: 271 | new_class.copy_managers(original_base._meta.concrete_managers) 272 | 273 | # Inherit virtual fields (like GenericForeignKey) from the parent 274 | # class 275 | for field in base._meta.virtual_fields: 276 | if base._meta.abstract and field.name in field_names: 277 | raise FieldError( 278 | 'Local field %r in class %r clashes ' 279 | 'with field of similar name from ' 280 | 'abstract base class %r' % (field.name, name, base.__name__) 281 | ) 282 | new_class.add_to_class(field.name, copy.deepcopy(field)) 283 | 284 | if abstract: 285 | # Abstract base models can't be instantiated and don't appear in 286 | # the list of models for an app. We do the final setup for them a 287 | # little differently from normal models. 288 | attr_meta.abstract = False 289 | new_class.Meta = attr_meta 290 | return new_class 291 | 292 | new_class._prepare() 293 | new_class._meta.apps.register_model(new_class._meta.app_label, new_class) 294 | return new_class 295 | 296 | @classmethod 297 | def _new_old_django(cls, name, bases, attrs): 298 | """ 299 | Exactly the same except the line with ``isinstance(b, ROAModelBase)`` and part delimited by 'ROA HACK' 300 | """ 301 | super_new = super(ModelBase, cls).__new__ 302 | 303 | # six.with_metaclass() inserts an extra class called 'NewBase' in the 304 | # inheritance tree: Model -> NewBase -> object. But the initialization 305 | # should be executed only once for a given model class. 306 | 307 | # attrs will never be empty for classes declared in the standard way 308 | # (ie. with the `class` keyword). This is quite robust. 309 | if name == 'NewBase' and attrs == {}: 310 | return super_new(cls, name, bases, attrs) 311 | 312 | # Also ensure initialization is only performed for subclasses of Model 313 | # (excluding Model class itself). 314 | parents = [b for b in bases if isinstance(b, ROAModelBase) and 315 | not (b.__name__ == 'NewBase' and b.__mro__ == (b, object))] 316 | if not parents: 317 | return super_new(cls, name, bases, attrs) 318 | 319 | # Create the class. 320 | module = attrs.pop('__module__') 321 | new_class = super_new(cls, name, bases, {'__module__': module}) 322 | attr_meta = attrs.pop('Meta', None) 323 | abstract = getattr(attr_meta, 'abstract', False) 324 | if not attr_meta: 325 | meta = getattr(new_class, 'Meta', None) 326 | else: 327 | meta = attr_meta 328 | base_meta = getattr(new_class, '_meta', None) 329 | 330 | if getattr(meta, 'app_label', None) is None: 331 | # Figure out the app_label by looking one level up. 332 | # For 'django.contrib.sites.models', this would be 'sites'. 333 | model_module = sys.modules[new_class.__module__] 334 | kwargs = {"app_label": model_module.__name__.split('.')[-2]} 335 | else: 336 | kwargs = {} 337 | 338 | new_class.add_to_class('_meta', Options(meta, **kwargs)) 339 | if not abstract: 340 | # ROA HACK: 341 | 342 | subclass_kwargs = { 343 | 'name': str('DoesNotExist'), 344 | 'parents': tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) 345 | or (ObjectDoesNotExist,), 346 | 'module': module 347 | } 348 | if DJANGO_GT_1_4: 349 | subclass_kwargs['attached_to'] = new_class 350 | 351 | new_class.add_to_class('DoesNotExist', subclass_exception(**subclass_kwargs)) 352 | 353 | subclass_kwargs = { 354 | 'name': str('MultipleObjectsReturned'), 355 | 'parents': tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) 356 | or (MultipleObjectsReturned,), 357 | 'module': module 358 | } 359 | if DJANGO_GT_1_4: 360 | subclass_kwargs['attached_to'] = new_class 361 | 362 | new_class.add_to_class('MultipleObjectsReturned', subclass_exception(**subclass_kwargs)) 363 | 364 | # END HACK 365 | 366 | if base_meta and not base_meta.abstract: 367 | # Non-abstract child classes inherit some attributes from their 368 | # non-abstract parent (unless an ABC comes before it in the 369 | # method resolution order). 370 | if not hasattr(meta, 'ordering'): 371 | new_class._meta.ordering = base_meta.ordering 372 | if not hasattr(meta, 'get_latest_by'): 373 | new_class._meta.get_latest_by = base_meta.get_latest_by 374 | 375 | is_proxy = new_class._meta.proxy 376 | 377 | # If the model is a proxy, ensure that the base class 378 | # hasn't been swapped out. 379 | if is_proxy and base_meta and base_meta.swapped: 380 | raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) 381 | 382 | if getattr(new_class, '_default_manager', None): 383 | if not is_proxy: 384 | # Multi-table inheritance doesn't inherit default manager from 385 | # parents. 386 | new_class._default_manager = None 387 | new_class._base_manager = None 388 | else: 389 | # Proxy classes do inherit parent's default manager, if none is 390 | # set explicitly. 391 | new_class._default_manager = new_class._default_manager._copy_to_model(new_class) 392 | new_class._base_manager = new_class._base_manager._copy_to_model(new_class) 393 | 394 | # Bail out early if we have already created this class. 395 | m = get_model(new_class._meta.app_label, name, 396 | seed_cache=False, only_installed=False) 397 | if m is not None: 398 | return m 399 | 400 | # Add all attributes to the class. 401 | for obj_name, obj in attrs.items(): 402 | new_class.add_to_class(obj_name, obj) 403 | 404 | # All the fields of any type declared on this model 405 | new_fields = new_class._meta.local_fields + \ 406 | new_class._meta.local_many_to_many + \ 407 | new_class._meta.virtual_fields 408 | field_names = set([f.name for f in new_fields]) 409 | 410 | # Basic setup for proxy models. 411 | if is_proxy: 412 | base = None 413 | for parent in [cls for cls in parents if hasattr(cls, '_meta')]: 414 | if parent._meta.abstract: 415 | if parent._meta.fields: 416 | raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name) 417 | else: 418 | continue 419 | if base is not None: 420 | raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name) 421 | else: 422 | base = parent 423 | if base is None: 424 | raise TypeError("Proxy model '%s' has no non-abstract model base class." % name) 425 | if (new_class._meta.local_fields or 426 | new_class._meta.local_many_to_many): 427 | raise FieldError("Proxy model '%s' contains model fields." % name) 428 | new_class._meta.setup_proxy(base) 429 | new_class._meta.concrete_model = base._meta.concrete_model 430 | else: 431 | new_class._meta.concrete_model = new_class 432 | 433 | # Do the appropriate setup for any model parents. 434 | o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields 435 | if isinstance(f, OneToOneField)]) 436 | 437 | for base in parents: 438 | original_base = base 439 | if not hasattr(base, '_meta'): 440 | # Things without _meta aren't functional models, so they're 441 | # uninteresting parents. 442 | continue 443 | 444 | parent_fields = base._meta.local_fields + base._meta.local_many_to_many 445 | # Check for clashes between locally declared fields and those 446 | # on the base classes (we cannot handle shadowed fields at the 447 | # moment). 448 | for field in parent_fields: 449 | if field.name in field_names: 450 | raise FieldError('Local field %r in class %r clashes ' 451 | 'with field of similar name from ' 452 | 'base class %r' % 453 | (field.name, name, base.__name__)) 454 | if not base._meta.abstract: 455 | # Concrete classes... 456 | base = base._meta.concrete_model 457 | if base in o2o_map: 458 | field = o2o_map[base] 459 | elif not is_proxy: 460 | attr_name = '%s_ptr' % base._meta.model_name 461 | field = OneToOneField(base, name=attr_name, 462 | auto_created=True, parent_link=True) 463 | new_class.add_to_class(attr_name, field) 464 | else: 465 | field = None 466 | new_class._meta.parents[base] = field 467 | else: 468 | # .. and abstract ones. 469 | for field in parent_fields: 470 | new_class.add_to_class(field.name, copy.deepcopy(field)) 471 | 472 | # Pass any non-abstract parent classes onto child. 473 | new_class._meta.parents.update(base._meta.parents) 474 | 475 | # Inherit managers from the abstract base classes. 476 | new_class.copy_managers(base._meta.abstract_managers) 477 | 478 | # Proxy models inherit the non-abstract managers from their base, 479 | # unless they have redefined any of them. 480 | if is_proxy: 481 | new_class.copy_managers(original_base._meta.concrete_managers) 482 | 483 | # Inherit virtual fields (like GenericForeignKey) from the parent 484 | # class 485 | for field in base._meta.virtual_fields: 486 | if base._meta.abstract and field.name in field_names: 487 | raise FieldError('Local field %r in class %r clashes '\ 488 | 'with field of similar name from '\ 489 | 'abstract base class %r' % \ 490 | (field.name, name, base.__name__)) 491 | new_class.add_to_class(field.name, copy.deepcopy(field)) 492 | 493 | if abstract: 494 | # Abstract base models can't be instantiated and don't appear in 495 | # the list of models for an app. We do the final setup for them a 496 | # little differently from normal models. 497 | attr_meta.abstract = False 498 | new_class.Meta = attr_meta 499 | return new_class 500 | 501 | new_class._prepare() 502 | register_models(new_class._meta.app_label, new_class) 503 | 504 | # Because of the way imports happen (recursively), we may or may not be 505 | # the first time this model tries to register with the framework. There 506 | # should only be one class for each model, so we always return the 507 | # registered version. 508 | return get_model(new_class._meta.app_label, name, 509 | seed_cache=False, only_installed=False) 510 | 511 | def _prepare(cls): 512 | """ 513 | Creates some methods once self._meta has been populated. 514 | """ 515 | opts = cls._meta 516 | opts._prepare(cls) 517 | 518 | if opts.order_with_respect_to: 519 | cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) 520 | cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) 521 | 522 | # defer creating accessors on the foreign class until we are 523 | # certain it has been created 524 | def make_foreign_order_accessors(field, model, cls): 525 | setattr( 526 | field.rel.to, 527 | 'get_%s_order' % cls.__name__.lower(), 528 | curry(method_get_order, cls) 529 | ) 530 | setattr( 531 | field.rel.to, 532 | 'set_%s_order' % cls.__name__.lower(), 533 | curry(method_set_order, cls) 534 | ) 535 | add_lazy_relation( 536 | cls, 537 | opts.order_with_respect_to, 538 | opts.order_with_respect_to.rel.to, 539 | make_foreign_order_accessors 540 | ) 541 | 542 | # Give the class a docstring -- its definition. 543 | if cls.__doc__ is None: 544 | cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join([f.attname for f in opts.fields])) 545 | 546 | if hasattr(cls, 'get_absolute_url'): 547 | cls.get_absolute_url = update_wrapper(curry(get_absolute_url, opts, cls.get_absolute_url), 548 | cls.get_absolute_url) 549 | 550 | if hasattr(cls, 'get_resource_url_list'): 551 | cls.get_resource_url_list = staticmethod(curry(get_resource_url_list, 552 | opts, cls.get_resource_url_list)) 553 | 554 | if hasattr(cls, 'get_resource_url_count'): 555 | cls.get_resource_url_count = update_wrapper(curry(get_resource_url_count, opts, cls.get_resource_url_count), 556 | cls.get_resource_url_count) 557 | 558 | if hasattr(cls, 'get_resource_url_detail'): 559 | cls.get_resource_url_detail = update_wrapper(curry(get_resource_url_detail, opts, cls.get_resource_url_detail), 560 | cls.get_resource_url_detail) 561 | 562 | signals.class_prepared.send(sender=cls) 563 | 564 | 565 | class ROAModel(models.Model): 566 | """ 567 | Model which access remote resources. 568 | """ 569 | __metaclass__ = ROAModelBase 570 | 571 | @classmethod 572 | def serializer(cls): 573 | """ 574 | Return a like Django Rest Framework serializer class 575 | """ 576 | raise NotImplementedError 577 | 578 | def get_renderer(self): 579 | """ 580 | Cf from rest_framework.renderers import JSONRenderer 581 | """ 582 | if ROA_FORMAT == 'json': 583 | return JSONRenderer() 584 | elif ROA_FORMAT == 'xml': 585 | return XMLRenderer() 586 | elif ROAException == 'yaml': 587 | return YAMLRenderer() 588 | else: 589 | raise NotImplementedError 590 | 591 | @classmethod 592 | def get_parser(cls): 593 | """ 594 | Cf from rest_framework.parsers import JSONParser 595 | """ 596 | if ROA_FORMAT == 'json': 597 | return JSONParser() 598 | elif ROA_FORMAT == 'xml': 599 | return XMLParser() 600 | elif ROAException == 'yaml': 601 | return YAMLParser() 602 | else: 603 | raise NotImplementedError 604 | 605 | def get_serializer_content_type(self): 606 | if ROA_FORMAT == 'json': 607 | return {'Content-Type' : 'application/json'} 608 | elif ROA_FORMAT == 'xml': 609 | return {'Content-Type' : 'application/xml'} 610 | elif ROAException == 'yaml': 611 | return {'Content-Type' : 'text/x-yaml'} 612 | else: 613 | raise NotImplementedError 614 | 615 | @classmethod 616 | def get_serializer(cls, instance=None, data=None, partial=False, **kwargs): 617 | """ 618 | Transform API response to Django model objects. 619 | """ 620 | serializer_class = cls.serializer() 621 | serializer = None 622 | 623 | if instance: 624 | serializer = serializer_class(instance, partial=partial, **kwargs) 625 | elif data: 626 | data = data['results'] if 'results' in data else data 627 | serializer = serializer_class(data=data, many=isinstance(data, list), **kwargs) 628 | 629 | return serializer 630 | 631 | @staticmethod 632 | def get_resource_url_list(): 633 | raise Exception("Static method get_resource_url_list is not defined.") 634 | 635 | @classmethod 636 | def count_response(cls, data, **kwargs): 637 | """ 638 | Read count query response and return result 639 | """ 640 | if 'count' in data: # with default DRF : with pagination 641 | count = int(data['count']) 642 | elif isinstance(data, (list, tuple)): 643 | count = len(data) # with default DRF : without pagination 644 | else: 645 | count = int(data) 646 | return count 647 | 648 | def get_resource_url_count(self): 649 | # By default this method is not with compatible with json Django Rest Framework standard viewset urls 650 | # In this case, you just have to override it and return self.get_resource_url_list() 651 | return u"%scount/" % (self.get_resource_url_list(),) 652 | 653 | def get_resource_url_detail(self): 654 | return u"%s%s/" % (self.get_resource_url_list(), self.pk) 655 | 656 | def save_base(self, raw=False, cls=None, origin=None, force_insert=False, 657 | force_update=False, using=None, update_fields=None): 658 | """ 659 | Does the heavy-lifting involved in saving. Subclasses shouldn't need to 660 | override this method. It's separate from save() in order to hide the 661 | need for overrides of save() to pass around internal-only parameters 662 | ('raw', 'cls', and 'origin'). 663 | """ 664 | 665 | assert not (force_insert and force_update) 666 | 667 | record_exists = False 668 | 669 | if cls is None: 670 | cls = self.__class__ 671 | meta = cls._meta 672 | if not meta.proxy: 673 | origin = cls 674 | else: 675 | meta = cls._meta 676 | 677 | if origin and not getattr(meta, "auto_created", False): 678 | signals.pre_save.send(sender=origin, instance=self, raw=raw) 679 | 680 | model_name = str(meta) 681 | 682 | # If we are in a raw save, save the object exactly as presented. 683 | # That means that we don't try to be smart about saving attributes 684 | # that might have come from the parent class - we just save the 685 | # attributes we have been given to the class we have been given. 686 | # We also go through this process to defer the save of proxy objects 687 | # to their actual underlying model. 688 | if not raw or meta.proxy: 689 | if meta.proxy: 690 | org = cls 691 | else: 692 | org = None 693 | for parent, field in meta.parents.items(): 694 | # At this point, parent's primary key field may be unknown 695 | # (for example, from administration form which doesn't fill 696 | # this field). If so, fill it. 697 | if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None: 698 | setattr(self, parent._meta.pk.attname, getattr(self, field.attname)) 699 | 700 | self.save_base(cls=parent, origin=org, using=using) 701 | 702 | if field: 703 | setattr(self, field.attname, self._get_pk_val(parent._meta)) 704 | if meta.proxy: 705 | return 706 | 707 | if not meta.proxy: 708 | pk_val = self._get_pk_val(meta) 709 | pk_is_set = pk_val is not None 710 | 711 | get_args = {} 712 | get_args[ROA_ARGS_NAMES_MAPPING.get('FORMAT', 'format')] = ROA_FORMAT 713 | get_args.update(ROA_CUSTOM_ARGS) 714 | 715 | # Construct Json payload 716 | serializer = self.get_serializer(self) 717 | payload = self.get_renderer().render(serializer.data) 718 | 719 | # Add serializer content_type 720 | headers = get_roa_headers() 721 | headers.update(self.get_serializer_content_type()) 722 | 723 | # check if resource use custom primary key 724 | if not meta.pk.attname in ['pk', 'id']: 725 | # consider it might be inserting so check it first 726 | # @todo: try to improve this block to check if custom pripary key is not None first 727 | resource = Resource(self.get_resource_url_detail(), 728 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 729 | try: 730 | response = resource.get(payload=None, headers=headers, **get_args) 731 | except ResourceNotFound: 732 | # since such resource does not exist, it's actually creating 733 | pk_is_set = False 734 | except RequestFailed: 735 | pk_is_set = False 736 | 737 | if force_update or pk_is_set and not self.pk is None: 738 | record_exists = True 739 | resource = Resource(self.get_resource_url_detail(), 740 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 741 | try: 742 | logger.debug(u"""Modifying : "%s" through %s with payload "%s" and GET args "%s" """ % ( 743 | force_unicode(self), 744 | force_unicode(resource.uri), 745 | force_unicode(payload), 746 | force_unicode(get_args))) 747 | response = resource.put(payload=payload, headers=headers, **get_args) 748 | except RequestFailed as e: 749 | raise ROAException(e) 750 | else: 751 | record_exists = False 752 | resource = Resource(self.get_resource_url_list(), 753 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 754 | try: 755 | logger.debug(u"""Creating : "%s" through %s with payload "%s" and GET args "%s" """ % ( 756 | force_unicode(self), 757 | force_unicode(resource.uri), 758 | force_unicode(payload), 759 | force_unicode(get_args))) 760 | response = resource.post(payload=payload, headers=headers, **get_args) 761 | except RequestFailed as e: 762 | raise ROAException(e) 763 | 764 | response = force_unicode(response.body_string()).encode(DEFAULT_CHARSET) 765 | 766 | data = self.get_parser().parse(StringIO(response)) 767 | serializer = self.get_serializer(data=data) 768 | if not serializer.is_valid(): 769 | raise ROAException(u'Invalid deserialization for %s model: %s' % (self, serializer.errors)) 770 | try: 771 | self.pk = int(serializer.object.pk) 772 | except ValueError: 773 | self.pk = serializer.object.pk 774 | self = serializer.object 775 | 776 | if origin: 777 | signals.post_save.send(sender=origin, instance=self, 778 | created=(not record_exists), raw=raw) 779 | 780 | save_base.alters_data = True 781 | 782 | def delete(self): 783 | assert self._get_pk_val() is not None, "%s object can't be deleted " \ 784 | "because its %s attribute is set to None." \ 785 | % (self._meta.object_name, self._meta.pk.attname) 786 | 787 | # Deletion in cascade should be done server side. 788 | resource = Resource(self.get_resource_url_detail(), 789 | filters=ROA_FILTERS, **ROA_SSL_ARGS) 790 | 791 | logger.debug(u"""Deleting : "%s" through %s""" % \ 792 | (unicode(self), unicode(resource.uri))) 793 | 794 | # Add serializer content_type 795 | headers = get_roa_headers() 796 | headers.update(self.get_serializer_content_type()) 797 | 798 | result = resource.delete(headers=headers, **ROA_CUSTOM_ARGS) 799 | if result.status_int in [200, 202, 204]: 800 | self.pk = None 801 | 802 | delete.alters_data = True 803 | 804 | def _get_unique_checks(self, exclude=None): 805 | """ 806 | We don't want to check unicity that way for now. 807 | """ 808 | unique_checks, date_checks = [], [] 809 | return unique_checks, date_checks 810 | 811 | 812 | ############################################## 813 | # HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # 814 | ############################################## 815 | 816 | ROA_URL_OVERRIDES_LIST = getattr(settings, 'ROA_URL_OVERRIDES_LIST', {}) 817 | ROA_URL_OVERRIDES_COUNT = getattr(settings, 'ROA_URL_OVERRIDES_COUNT', {}) 818 | ROA_URL_OVERRIDES_DETAIL = getattr(settings, 'ROA_URL_OVERRIDES_DETAIL', {}) 819 | 820 | 821 | def get_resource_url_list(opts, func, *args, **kwargs): 822 | if DJANGO_LT_1_7: 823 | key = '%s.%s' % (opts.app_label, opts.module_name) 824 | else: 825 | key = '%s.%s' % (opts.app_label, opts.model_name) 826 | 827 | overridden = ROA_URL_OVERRIDES_LIST.get(key, False) 828 | return overridden and overridden or func(*args, **kwargs) 829 | 830 | 831 | def get_resource_url_count(opts, func, self, *args, **kwargs): 832 | if DJANGO_LT_1_7: 833 | key = '%s.%s' % (opts.app_label, opts.module_name) 834 | else: 835 | key = '%s.%s' % (opts.app_label, opts.model_name) 836 | 837 | return ROA_URL_OVERRIDES_COUNT.get(key, func)(self, *args, **kwargs) 838 | 839 | 840 | def get_resource_url_detail(opts, func, self, *args, **kwargs): 841 | if DJANGO_LT_1_7: 842 | key = '%s.%s' % (opts.app_label, opts.module_name) 843 | else: 844 | key = '%s.%s' % (opts.app_label, opts.model_name) 845 | 846 | return ROA_URL_OVERRIDES_DETAIL.get(key, func)(self, *args, **kwargs) 847 | -------------------------------------------------------------------------------- /examples/django_roa_client/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | r""" 3 | Unit tests for django-roa. 4 | 5 | These tests assume that you've completed all the prerequisites for 6 | getting django-roa running in the default setup, to wit: 7 | 8 | 1. You have created the remote database: go to ``examples/django_roa_server`` 9 | and run ``syncdb`` command with ``--noinput`` option in order to create 10 | a superuser named "roa_user" from fixtures: 11 | 12 | $ python manage.py syncdb --noinput 13 | 14 | 2. You have launched the project's server on port 8081 in order to test this 15 | suite against it with this command: 16 | 17 | $ python manage.py runserver 8081 18 | 19 | Now, you can go to ``examples/django_roa_client`` and run this command: 20 | 21 | $ python manage.py test django_roa_client 22 | 23 | It should return no error and you will be able to see logs from the test 24 | server which confirm that it works as expected: remote requests are done. 25 | 26 | Note: do not try to launch tests' projects if you put ``django_roa`` 27 | application into your own project, otherwise it will fail. Django do not 28 | handle very well projects inside projects. 29 | """ 30 | from datetime import time, date, datetime 31 | 32 | from django.test import TestCase 33 | from django.conf import settings 34 | from django.test.client import Client 35 | from django.core.serializers import register_serializer 36 | from django.contrib.contenttypes.models import ContentType 37 | 38 | from restkit import Resource 39 | from django_roa.remoteauth.models import User, Message, Group, Permission 40 | from django_roa_client.models import RemotePage, RemotePageWithManyFields, \ 41 | RemotePageWithBooleanFields, RemotePageWithRelations, \ 42 | RemotePageWithCustomSlug, RemotePageWithOverriddenUrls, \ 43 | RemotePageWithNamedRelations, RemotePageWithProxy, \ 44 | RemotePageWithRelationsThrough, RemotePageWithCustomPrimaryKey, \ 45 | RemotePageWithCustomPrimaryKeyCountOverridden 46 | from django_roa_client.forms import TestForm, RemotePageForm 47 | from django_roa.db.exceptions import ROAException 48 | 49 | ROA_FILTERS = getattr(settings, 'ROA_FILTERS', {}) 50 | 51 | class ROATestCase(TestCase): 52 | 53 | def setUp(self): 54 | RemotePage.objects.all().delete() 55 | RemotePageWithCustomPrimaryKey.objects.all().delete() 56 | 57 | def tearDown(self): 58 | RemotePage.objects.all().delete() 59 | RemotePageWithCustomPrimaryKey.objects.all().delete() 60 | 61 | class ROAUserTestCase(ROATestCase): 62 | 63 | def setUp(self): 64 | super(ROAUserTestCase, self).setUp() 65 | User.objects.all().delete() 66 | self.user = User.objects.create_superuser('admin', 'admin@example.org', 'admin') 67 | 68 | def tearDown(self): 69 | super(ROAUserTestCase, self).tearDown() 70 | User.objects.all().delete() 71 | Group.objects.all().delete() 72 | Permission.objects.all().delete() 73 | if Message: 74 | Message.objects.all().delete() 75 | 76 | 77 | class ROAInitializationTests(ROATestCase): 78 | 79 | def test_inheritance(self): 80 | self.assertEqual(str(RemotePage.objects.__class__), "") 81 | self.assertEqual(str(RemotePage.__class__), "") 82 | 83 | 84 | class ROABaseTests(ROATestCase): 85 | 86 | def test_basic_crud(self): 87 | page = RemotePage.objects.create(title=u'A first remote page') 88 | self.assertEqual(repr(page), '') 89 | page.title = u'Another title' 90 | page.save() 91 | page = RemotePage.objects.get(title=u'Another title') 92 | self.assertEqual(page.title, u'Another title') 93 | pages = RemotePage.objects.all() 94 | self.assertEqual(repr(pages), '[]') 95 | page.delete() 96 | self.assertEqual(repr(RemotePage.objects.all()), '[]') 97 | # test custom mapping of arguments too 98 | self.assertEqual(RemotePage.objects.count(), 0) 99 | 100 | def test_complex_crud(self): 101 | page = RemotePage.objects.create(title=u'') 102 | self.assertEqual(page.title, u'') 103 | page.title = u'A temporary title' 104 | page.save() 105 | page = RemotePage.objects.get(title=u'A temporary title') 106 | self.assertEqual(page.title, u'A temporary title') 107 | self.assertEqual(RemotePage.objects.count(), 1) 108 | page.title = u'' 109 | page.save() 110 | page = RemotePage.objects.all()[0] 111 | self.assertEqual(page.title, u'') 112 | page.delete() 113 | self.assertEqual(repr(RemotePage.objects.all()), '[]') 114 | self.assertEqual(RemotePage.objects.count(), 0) 115 | 116 | 117 | class ROAUnicodeTests(ROATestCase): 118 | 119 | def test_remotepage(self): 120 | emilie = RemotePage.objects.create(title=u"Émilie") 121 | self.assertEqual(emilie.title, u'Émilie') 122 | emilie = RemotePage.objects.get(title=u"Émilie") 123 | self.assertEqual(emilie.title, u'Émilie') 124 | amelie = emilie 125 | amelie.title = u'Amélie' 126 | amelie.save() 127 | self.assertEqual(amelie.title, u'Amélie') 128 | amelie = RemotePage.objects.get(title=u"Amélie") 129 | self.assertEqual(amelie.title, u'Amélie') 130 | amelie.delete() 131 | 132 | 133 | class ROAFieldsTests(ROATestCase): 134 | 135 | def tearDown(self): 136 | super(ROAFieldsTests, self).tearDown() 137 | RemotePageWithManyFields.objects.all().delete() 138 | RemotePageWithBooleanFields.objects.all().delete() 139 | 140 | def test_empty_values(self): 141 | default_page = RemotePageWithManyFields.objects.create() 142 | self.assertEqual(default_page.id, 1) 143 | self.assertEqual(default_page.char_field, None) 144 | self.assertEqual(default_page.date_field, None) 145 | self.assertEqual(default_page.datetime_field, None) 146 | self.assertEqual(default_page.decimal_field, None) 147 | self.assertEqual(default_page.email_field, None) 148 | self.assertEqual(default_page.filepath_field, None) 149 | self.assertEqual(default_page.float_field, None) 150 | self.assertEqual(default_page.integer_field, None) 151 | self.assertEqual(default_page.ipaddress_field, None) 152 | self.assertEqual(default_page.positiveinteger_field, None) 153 | self.assertEqual(default_page.positivesmallinteger_field, None) 154 | self.assertEqual(default_page.slug_field, None) 155 | self.assertEqual(default_page.smallinteger_field, None) 156 | self.assertEqual(default_page.text_field, None) 157 | self.assertEqual(default_page.time_field, None) 158 | self.assertEqual(default_page.url_field, None) 159 | self.assertEqual(default_page.xml_field, None) 160 | self.assertEqual(repr(default_page.file_field), '') 161 | self.assertEqual(repr(default_page.image_field), '') 162 | retrieved_default_page = RemotePageWithManyFields.objects.get(id=default_page.id) 163 | self.assertEqual(default_page.id, retrieved_default_page.id) 164 | self.assertEqual(default_page.char_field, retrieved_default_page.char_field) 165 | self.assertEqual(default_page.date_field, retrieved_default_page.date_field) 166 | self.assertEqual(default_page.datetime_field, retrieved_default_page.datetime_field) 167 | self.assertEqual(default_page.decimal_field, retrieved_default_page.decimal_field) 168 | self.assertEqual(default_page.email_field, retrieved_default_page.email_field) 169 | self.assertEqual(default_page.filepath_field, retrieved_default_page.filepath_field) 170 | self.assertEqual(default_page.float_field, retrieved_default_page.float_field) 171 | self.assertEqual(default_page.integer_field, retrieved_default_page.integer_field) 172 | self.assertEqual(default_page.ipaddress_field, retrieved_default_page.ipaddress_field) 173 | self.assertEqual(default_page.positiveinteger_field, retrieved_default_page.positiveinteger_field) 174 | self.assertEqual(default_page.positivesmallinteger_field, retrieved_default_page.positivesmallinteger_field) 175 | self.assertEqual(default_page.slug_field, retrieved_default_page.slug_field) 176 | self.assertEqual(default_page.smallinteger_field, retrieved_default_page.smallinteger_field) 177 | self.assertEqual(default_page.text_field, retrieved_default_page.text_field) 178 | self.assertEqual(default_page.time_field, retrieved_default_page.time_field) 179 | self.assertEqual(default_page.url_field, retrieved_default_page.url_field) 180 | self.assertEqual(default_page.xml_field, retrieved_default_page.xml_field) 181 | self.assertEqual(repr(default_page.file_field), repr(retrieved_default_page.file_field)) 182 | self.assertEqual(repr(default_page.image_field), repr(retrieved_default_page.image_field)) 183 | default_page.delete() 184 | 185 | def test_empty_boolean_values(self): 186 | boolean_page = RemotePageWithBooleanFields.objects.create(boolean_field=True) 187 | self.assertEqual(boolean_page.id, 1) 188 | self.assertEqual(boolean_page.boolean_field, True) 189 | self.assertEqual(boolean_page.null_boolean_field, None) 190 | retrieved_boolean_page = RemotePageWithBooleanFields.objects.get(id=boolean_page.id) 191 | self.assertEqual(boolean_page.id, retrieved_boolean_page.id) 192 | self.assertEqual(boolean_page.boolean_field, retrieved_boolean_page.boolean_field) 193 | self.assertEqual(boolean_page.null_boolean_field, retrieved_boolean_page.null_boolean_field) 194 | boolean_page.delete() 195 | 196 | def test_boolean_field(self): 197 | page = RemotePageWithBooleanFields.objects.create(boolean_field=True) 198 | self.assertEqual(page.boolean_field, True) 199 | page = RemotePageWithBooleanFields.objects.get(id=page.id) 200 | self.assertEqual(page.boolean_field, True) 201 | page.boolean_field = False 202 | page.save() 203 | page = RemotePageWithBooleanFields.objects.get(id=page.id) 204 | self.assertEqual(page.boolean_field, False) 205 | page.delete() 206 | 207 | def test_char_field(self): 208 | page = RemotePageWithManyFields.objects.create(char_field=u'foo') 209 | self.assertEqual(page.char_field, u'foo') 210 | page = RemotePageWithManyFields.objects.get(id=page.id) 211 | self.assertEqual(page.char_field, u'foo') 212 | page.char_field = u'bar' 213 | page.save() 214 | page = RemotePageWithManyFields.objects.get(id=page.id) 215 | self.assertEqual(page.char_field, u'bar') 216 | page.delete() 217 | 218 | def test_date_field(self): 219 | page = RemotePageWithManyFields.objects.create(date_field=date(2008, 12, 24)) 220 | self.assertEqual(repr(page.date_field), 'datetime.date(2008, 12, 24)') 221 | page = RemotePageWithManyFields.objects.get(id=page.id) 222 | self.assertEqual(repr(page.date_field), 'datetime.date(2008, 12, 24)') 223 | page.date_field = date(2008, 12, 25) 224 | page.save() 225 | page = RemotePageWithManyFields.objects.get(id=page.id) 226 | self.assertEqual(repr(page.date_field), 'datetime.date(2008, 12, 25)') 227 | page.delete() 228 | 229 | def test_datetime_field(self): 230 | page = RemotePageWithManyFields.objects.create(datetime_field=datetime(2008, 12, 24, 11, 53, 57)) 231 | self.assertEqual(repr(page.datetime_field), 'datetime.datetime(2008, 12, 24, 11, 53, 57)') 232 | page = RemotePageWithManyFields.objects.get(id=page.id) 233 | self.assertEqual(repr(page.datetime_field), 'datetime.datetime(2008, 12, 24, 11, 53, 57)') 234 | page.datetime_field = datetime(2008, 12, 25, 13, 20) 235 | page.save() 236 | page = RemotePageWithManyFields.objects.get(id=page.id) 237 | self.assertEqual(repr(page.datetime_field), 'datetime.datetime(2008, 12, 25, 13, 20)') 238 | page.delete() 239 | 240 | def test_decimal_field(self): 241 | page = RemotePageWithManyFields.objects.create(decimal_field=1.55) 242 | self.assertEqual(page.decimal_field, 1.55) 243 | page = RemotePageWithManyFields.objects.get(id=page.id) 244 | self.assertEqual(repr(page.decimal_field), "Decimal('1.55')") 245 | page.decimal_field = 20.09 246 | page.save() 247 | page = RemotePageWithManyFields.objects.get(id=page.id) 248 | self.assertEqual(repr(page.decimal_field), "Decimal('20.09')") 249 | page.delete() 250 | 251 | def test_email_field(self): 252 | page = RemotePageWithManyFields.objects.create(email_field=u'test@example.com') 253 | self.assertEqual(page.email_field, u'test@example.com') 254 | page = RemotePageWithManyFields.objects.get(id=page.id) 255 | self.assertEqual(page.email_field, u'test@example.com') 256 | page.email_field = u'test@example.org' 257 | page.save() 258 | page = RemotePageWithManyFields.objects.get(id=page.id) 259 | self.assertEqual(page.email_field, u'test@example.org') 260 | page.delete() 261 | 262 | def test_filepath_field(self): 263 | page = RemotePageWithManyFields.objects.create(filepath_field=u'/foo/bar.zip') 264 | self.assertEqual(page.filepath_field, u'/foo/bar.zip') 265 | page = RemotePageWithManyFields.objects.get(id=page.id) 266 | self.assertEqual(page.filepath_field, u'/foo/bar.zip') 267 | page.filepath_field = u'/foo/bar/baz.tar.gz' 268 | page.save() 269 | page = RemotePageWithManyFields.objects.get(id=page.id) 270 | self.assertEqual(page.filepath_field, u'/foo/bar/baz.tar.gz') 271 | page.delete() 272 | 273 | def test_float_field(self): 274 | page = RemotePageWithManyFields.objects.create(float_field=1.55) 275 | self.assertEqual(page.float_field, 1.55) 276 | page = RemotePageWithManyFields.objects.get(id=page.id) 277 | self.assertEqual(page.float_field, 1.55) 278 | page.float_field = 20.09 279 | page.save() 280 | page = RemotePageWithManyFields.objects.get(id=page.id) 281 | self.assertEqual(page.float_field, 20.09) 282 | page.delete() 283 | 284 | def test_integer_field(self): 285 | page = RemotePageWithManyFields.objects.create(integer_field=155) 286 | self.assertEqual(page.integer_field, 155) 287 | page = RemotePageWithManyFields.objects.get(id=page.id) 288 | self.assertEqual(page.integer_field, 155) 289 | page.integer_field = 2009 290 | page.save() 291 | page = RemotePageWithManyFields.objects.get(id=page.id) 292 | self.assertEqual(page.integer_field, 2009) 293 | page.delete() 294 | 295 | def test_nullboolean_field(self): 296 | page = RemotePageWithBooleanFields.objects.create(null_boolean_field=True) 297 | self.assertEqual(page.null_boolean_field, True) 298 | page = RemotePageWithBooleanFields.objects.get(id=page.id) 299 | self.assertEqual(page.null_boolean_field, True) 300 | page.null_boolean_field = False 301 | page.save() 302 | page = RemotePageWithBooleanFields.objects.get(id=page.id) 303 | self.assertEqual(page.null_boolean_field, False) 304 | page.delete() 305 | 306 | def test_slug_field(self): 307 | page = RemotePageWithManyFields.objects.create(slug_field=u'foo-bar') 308 | self.assertEqual(page.slug_field, u'foo-bar') 309 | page = RemotePageWithManyFields.objects.get(id=page.id) 310 | self.assertEqual(page.slug_field, u'foo-bar') 311 | page.slug_field = u'bar-baz' 312 | page.save() 313 | page = RemotePageWithManyFields.objects.get(id=page.id) 314 | self.assertEqual(page.slug_field, u'bar-baz') 315 | page.delete() 316 | 317 | def test_text_field(self): 318 | page = RemotePageWithManyFields.objects.create(text_field=u'foo bar') 319 | self.assertEqual(page.text_field, u'foo bar') 320 | page = RemotePageWithManyFields.objects.get(id=page.id) 321 | self.assertEqual(page.text_field, u'foo bar') 322 | page.text_field = u'foo bar\nbaz' 323 | page.save() 324 | page = RemotePageWithManyFields.objects.get(id=page.id) 325 | self.assertEqual(page.text_field, u'foo bar\nbaz') 326 | page.delete() 327 | 328 | def test_time_field(self): 329 | page = RemotePageWithManyFields.objects.create(time_field=time(3, 51, 28)) 330 | self.assertEqual(repr(page.time_field), 'datetime.time(3, 51, 28)') 331 | page = RemotePageWithManyFields.objects.get(id=page.id) 332 | self.assertEqual(repr(page.time_field), 'datetime.time(3, 51, 28)') 333 | page.time_field = time(11, 20, 53) 334 | page.save() 335 | page = RemotePageWithManyFields.objects.get(id=page.id) 336 | self.assertEqual(repr(page.time_field), 'datetime.time(11, 20, 53)') 337 | page.delete() 338 | 339 | def test_url_field(self): 340 | page = RemotePageWithManyFields.objects.create(url_field=u'http://example.com') 341 | self.assertEqual(page.url_field, u'http://example.com') 342 | page = RemotePageWithManyFields.objects.get(id=page.id) 343 | self.assertEqual(page.url_field, u'http://example.com') 344 | page.url_field = u'http://example.org' 345 | page.save() 346 | page = RemotePageWithManyFields.objects.get(id=page.id) 347 | self.assertEqual(page.url_field, u'http://example.org') 348 | page.delete() 349 | 350 | 351 | class ROARelationsTests(ROATestCase): 352 | 353 | def tearDown(self): 354 | super(ROARelationsTests, self).tearDown() 355 | RemotePageWithManyFields.objects.all().delete() 356 | RemotePageWithRelations.objects.all().delete() 357 | RemotePageWithNamedRelations.objects.all().delete() 358 | 359 | def test_empty_relation(self): 360 | relations_page = RemotePageWithRelations.objects.create() 361 | self.assertEqual(relations_page.id, 1) 362 | self.assertEqual(relations_page.title, '') 363 | self.assertEqual(relations_page.remote_page, None) 364 | retrieved_relations_page = RemotePageWithRelations.objects.get(id=relations_page.id) 365 | self.assertEqual(relations_page.id, retrieved_relations_page.id) 366 | self.assertEqual(relations_page.title, retrieved_relations_page.title) 367 | self.assertEqual(relations_page.remote_page, retrieved_relations_page.remote_page) 368 | relations_page.delete() 369 | 370 | def test_foreignkey_relation(self): 371 | remote_page = RemotePage.objects.create(title=u'A remote page') 372 | another_remote_page = RemotePage.objects.create(title=u'Another remote page') 373 | relations_page = RemotePageWithRelations.objects.create(remote_page=remote_page) 374 | self.assertEqual(repr(relations_page.remote_page), '') 375 | relations_page = RemotePageWithRelations.objects.get(id=relations_page.id) 376 | self.assertEqual(repr(relations_page.remote_page), '') 377 | self.assertEqual(repr(remote_page.remotepagewithrelations_set.all()), '[]') 378 | relations_page.remote_page = another_remote_page 379 | relations_page.save() 380 | relations_page = RemotePageWithRelations.objects.get(id=relations_page.id) 381 | self.assertEqual(repr(relations_page.remote_page), '') 382 | relations_page.delete() 383 | another_remote_page.delete() 384 | remote_page.delete() 385 | 386 | def test_manytomany_relation(self): 387 | remote_page = RemotePageWithManyFields.objects.create(char_field=u'A remote page') 388 | another_remote_page = RemotePageWithManyFields.objects.create(char_field=u'Another remote page') 389 | relations_page = RemotePageWithRelations.objects.create(title=u'A remote relation page') 390 | relations_page_through = RemotePageWithRelationsThrough.objects.create(title=u'A remote relation page through', 391 | remote_page_with_relations=relations_page, 392 | remote_page_with_many_fields=remote_page) 393 | self.assertEqual(repr(relations_page.remote_page_fields.all()), '[]') 394 | relations_page = RemotePageWithRelations.objects.get(id=relations_page.id) 395 | self.assertEqual(repr(relations_page.remote_page_fields.all()), '[]') 396 | another_relations_page_through = RemotePageWithRelationsThrough.objects.create(title=u'Another remote relation page through', 397 | remote_page_with_relations=relations_page, 398 | remote_page_with_many_fields=another_remote_page) 399 | relations_page = RemotePageWithRelations.objects.get(id=relations_page.id) 400 | self.assertEqual(repr(relations_page.remote_page_fields.all()), '[, ]') 401 | self.assertEqual(repr(remote_page.remotepagewithrelations_set.all()), '[]') 402 | relations_page_through.delete() 403 | self.assertEqual(repr(relations_page.remote_page_fields.all()), '[]') 404 | another_relations_page_through.delete() 405 | self.assertEqual(repr(relations_page.remote_page_fields.all()), '[]') 406 | relations_page.delete() 407 | another_remote_page.delete() 408 | remote_page.delete() 409 | 410 | def test_named_relation(self): 411 | remote_page = RemotePage.objects.create(title=u'A remote page') 412 | another_remote_page = RemotePage.objects.create(title=u'Another remote page') 413 | named_relations_page = RemotePageWithNamedRelations.objects.create(first_page=remote_page, 414 | last_page=another_remote_page) 415 | self.assertEqual(repr(named_relations_page.first_page), '') 416 | named_relations_page = RemotePageWithNamedRelations.objects.get(id=named_relations_page.id) 417 | self.assertEqual(repr(named_relations_page.first_page), '') 418 | self.assertEqual(repr(remote_page.from_first.all()), '[]') 419 | self.assertEqual(repr(another_remote_page.from_last.all()), '[]') 420 | named_relations_page.first_page = another_remote_page 421 | named_relations_page.save() 422 | named_relations_page = RemotePageWithNamedRelations.objects.get(id=named_relations_page.id) 423 | self.assertEqual(repr(named_relations_page.first_page), '') 424 | named_relations_page.delete() 425 | another_remote_page.delete() 426 | remote_page.delete() 427 | 428 | def test_proxy_relation(self): 429 | remote_page = RemotePage.objects.create(title=u'A remote page') 430 | proxy_remote_page = RemotePageWithProxy.objects.create(title=u'A proxy remote page') 431 | self.assertEqual(repr(remote_page), '') 432 | self.assertEqual(repr(proxy_remote_page), '') 433 | self.assertEqual(repr(RemotePage.objects.all()), '[, ]') 434 | self.assertEqual(repr(RemotePageWithProxy.objects.all()), '[, ]') 435 | proxy_remote_page.title = u'A modified proxy remote page' 436 | proxy_remote_page.save() 437 | proxy_remote_page = RemotePageWithProxy.objects.get(id=2) 438 | self.assertEqual(repr(proxy_remote_page), '') 439 | self.assertEqual(repr(RemotePage.objects.all()[1]), '') 440 | proxy_remote_page.delete() 441 | remote_page.delete() 442 | 443 | 444 | class ROAQuerysetTests(ROATestCase): 445 | 446 | def setUp(self): 447 | super(ROAQuerysetTests, self).setUp() 448 | self.remote_page1 = RemotePage.objects.create(title='A remote page') 449 | self.remote_page2 = RemotePage.objects.create(title='Another remote page') 450 | self.remote_page3 = RemotePage.objects.create(title='Yet another remote page') 451 | self.remote_page4 = RemotePage.objects.create(title='Still another remote page') 452 | self.remote_page_with_custom_primary_key1 = RemotePageWithCustomPrimaryKey.objects.create(title=u'Remote test page with custom primary') 453 | self.remote_page_with_custom_primary_key2 = RemotePageWithCustomPrimaryKey.objects.create(title=u'Another remote test page with custom primary') 454 | 455 | def test_getorcreate(self): 456 | self.assertEqual(repr(RemotePage.objects.get_or_create(title='A remote page')), '(, False)') 457 | remote_page, created = RemotePage.objects.get_or_create(title='A created remote page') 458 | self.assertEqual(created, True) 459 | 460 | def test_latest(self): 461 | self.assertEqual(repr(RemotePage.objects.latest('id')), '') 462 | self.assertEqual(repr(RemotePage.objects.latest('title')), '') 463 | 464 | def test_filtering(self): 465 | self.assertEqual(repr(RemotePage.objects.exclude(id=2)), '[, , ]') 466 | self.assertEqual(repr(RemotePage.objects.filter(title__iexact='ANOTHER remote page')), '[]') 467 | self.assertEqual(repr(RemotePage.objects.filter(title__contains='another')), '[, , ]') 468 | 469 | def test_ordering(self): 470 | self.assertEqual(repr(RemotePage.objects.order_by('title')), '[, , , ]') 471 | self.assertEqual(repr(RemotePage.objects.order_by('-title', '-id')), '[, , , ]') 472 | 473 | def test_slicing(self): 474 | self.assertEqual(repr(RemotePage.objects.all()[1:3]), '[, ]') 475 | self.assertEqual(repr(RemotePage.objects.all()[0]), '') 476 | 477 | def test_extra(self): 478 | self.assertEqual(bool(RemotePage.objects.all().extra(select={'a': 1}).values('a').order_by()), True) 479 | RemotePage.objects.all().delete() 480 | self.assertEqual(bool(RemotePage.objects.all().extra(select={'a': 1}).values('a').order_by()), False) 481 | 482 | def test_combined(self): 483 | self.assertEqual(repr(RemotePage.objects.exclude(title__contains='yet').order_by('title', '-id')[:2]), '[, ]') 484 | 485 | def test_get(self): 486 | # test get by pk, id directly 487 | self.assertEqual(repr(RemotePage.objects.get(id=1)), '') 488 | self.assertEqual(repr(RemotePage.objects.get(pk=2)), '') 489 | # test get by pk, id with exact filter 490 | self.assertEqual(repr(RemotePage.objects.get(id__exact=3)), '') 491 | self.assertEqual(repr(RemotePage.objects.get(pk__exact=4)), '') 492 | 493 | # test get by custom primary key attribute name 494 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(auto_field=1)), '') 495 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(auto_field=2)), '') 496 | with self.assertRaisesRegexp(ROAException, 'Not Found'): 497 | RemotePageWithCustomPrimaryKey.objects.get(auto_field=999) 498 | 499 | # test get by custom primary key with exact filter 500 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(auto_field__exact=1)), '') 501 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(auto_field__exact=2)), '') 502 | with self.assertRaisesRegexp(ROAException, 'Not Found'): 503 | RemotePageWithCustomPrimaryKey.objects.get(auto_field__exact=999) 504 | 505 | # test get by multiple attributes 506 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(title='Another remote test page with custom primary', auto_field=2)), '') 507 | self.assertEqual(repr(RemotePageWithCustomPrimaryKey.objects.get(title__exact='Another remote test page with custom primary', auto_field__exact=2)), '') 508 | with self.assertRaisesRegexp(ROAException, 'Not Found'): 509 | RemotePageWithCustomPrimaryKey.objects.get(title__exact='Another remote test page with custom primary', auto_field__exact=999) 510 | 511 | def test_count(self): 512 | self.assertEqual(RemotePageWithCustomPrimaryKey.objects.count(), 2) 513 | RemotePageWithCustomPrimaryKey.objects.all().delete() 514 | self.assertEqual(RemotePageWithCustomPrimaryKey.objects.count(), 0) 515 | 516 | RemotePageWithCustomPrimaryKey.objects.create(title=u'Remote test page with custom primary') 517 | self.assertEqual(RemotePageWithCustomPrimaryKeyCountOverridden.objects.count(), 0) 518 | 519 | class ROAAdminTests(ROAUserTestCase): 520 | 521 | def test_admin_views(self): 522 | remote_page1 = RemotePage.objects.create(title='A remote page') 523 | remote_page2 = RemotePage.objects.create(title='Another remote page') 524 | remote_page3 = RemotePage.objects.create(title='Yet another remote page') 525 | remote_page4 = RemotePage.objects.create(title='Still another remote page') 526 | bob = User.objects.create_superuser(username=u'bob', password=u'secret', email=u'bob@example.com') 527 | bob = User.objects.get(username=u'bob') 528 | self.assertEqual(bob.is_superuser, True) 529 | c = Client() 530 | response = c.login(username=u'bob', password=u'secret') 531 | self.assertEqual(response, True) 532 | response = c.get('/admin/') 533 | # ._wrapped necessary because we compare string, comparison should 534 | # work with User.objects.get(username="bob") but slower... 535 | self.assertEqual(repr(response.context[-1]["user"]._wrapped), '') 536 | response = c.get('/admin/django_roa_client/remotepage/') 537 | self.assertEqual(response.status_code, 200) 538 | self.assertEqual(repr(response.context[-1]["cl"].result_list), '[, ]') 539 | self.assertEqual(response.context[-1]["cl"].result_count, 4) 540 | 541 | 542 | class ROAFormsTests(ROATestCase): 543 | 544 | def test_form_validation(self): 545 | form = TestForm() 546 | remote_page1 = RemotePage.objects.create(title='A remote page') 547 | self.assertEqual(form.is_valid(), False) 548 | form = TestForm(data={u'test_field': u'Test data', u'remote_page': remote_page1.id}) 549 | self.assertEqual(form.is_valid(), True) 550 | 551 | def test_modelform_validation(self): 552 | form = RemotePageForm() 553 | self.assertEqual(form.is_valid(), False) 554 | form = RemotePageForm(data={u'title': u'Test data'}) 555 | self.assertEqual(form.is_valid(), True) 556 | remote_page = form.save() 557 | self.assertEqual(repr(remote_page), '') 558 | 559 | def test_modelform_rendering(self): 560 | c = Client() 561 | remote_page1 = RemotePage.objects.create(title='A remote page') 562 | response = c.get('/') 563 | self.assertEqual('' in response.content, True) 564 | 565 | 566 | class ROARemoteAuthTests(ROAUserTestCase): 567 | 568 | def test_remote_users(self): 569 | self.assertEqual(repr(User.objects.all()), '[]') 570 | alice = User.objects.create_user(username=u'alice', password=u'secret', email=u'alice@example.com') 571 | self.assertEqual(alice.is_superuser, False) 572 | self.assertEqual(repr(User.objects.all()), '[, ]') 573 | self.assertEqual(alice.id, 2) 574 | if Message: 575 | self.assertEqual(repr(Message.objects.all()), '[]') 576 | message = Message.objects.create(user=alice, message=u'Test message') 577 | self.assertEqual(message.message, u'Test message') 578 | self.assertEqual(repr(message.user), '') 579 | self.assertEqual(repr(Message.objects.all()), '[]') 580 | self.assertEqual(repr(alice.message_set.all()), '[]') 581 | 582 | def test_select_related(self): 583 | # Not supported, we just verify that it doesn't break anything 584 | if Message: 585 | alice = User.objects.create_user(username=u'alice', password=u'secret', email=u'alice@example.com') 586 | message = Message.objects.create(user=alice, message=u'Test message') 587 | self.assertEqual(repr(Message.objects.all().select_related()), '[]') 588 | self.assertEqual(repr(Message.objects.all().select_related('user')), '[]') 589 | 590 | def test_groups(self): 591 | bob = User.objects.create_superuser(username=u'bob', password=u'secret', email=u'bob@example.com') 592 | self.assertEqual(repr(Group.objects.all()), '[]') 593 | self.assertEqual(repr(bob.groups.all()), '[]') 594 | ct_group = ContentType.objects.get(name='group') 595 | group_permission = Permission.objects.create(name=u"Custom permission to group model", 596 | content_type=ct_group, 597 | codename=u"custom_group_permission") 598 | group = Group.objects.create(name=u"Custom group") 599 | #group.permissions.add(group_permission) 600 | #bob.groups.add(group) 601 | #self.assertEqual(repr(bob.groups.all()), '[]') 602 | #self.assertEqual(repr(bob.groups.all()[0].permissions.all()), '[]') 603 | self.assertEqual(repr(bob.groups.all()), '[]') 604 | 605 | def test_permissions(self): 606 | bob = User.objects.create_superuser(username=u'bob', password=u'secret', email=u'bob@example.com') 607 | self.assertEqual(repr(bob.user_permissions.all()), '[]') 608 | ct_user = ContentType.objects.get(name='user') 609 | user_permission = Permission.objects.create(name=u"Custom permission to user model", 610 | content_type=ct_user, 611 | codename=u"custom_user_permission") 612 | #bob.user_permissions.add(user_permission) 613 | #self.assertEqual(repr(bob.user_permissions.all()), '[]') 614 | self.assertEqual(repr(bob.user_permissions.all()), '[]') 615 | ct_group = ContentType.objects.get(name='group') 616 | group_permission = Permission.objects.create(name=u"Custom permission to group model", 617 | content_type=ct_group, 618 | codename=u"custom_group_permission") 619 | group = Group.objects.create(name=u"Custom group") 620 | #group.permissions.add(group_permission) 621 | #bob.groups.add(group) 622 | #self.assertEqual(bob.get_group_permissions(), set([u'remoteauth.custom_group_permission'])) 623 | #self.assertEqual(bob.get_all_permissions(), set([u'remoteauth.custom_group_permission', u'remoteauth.custom_user_permission'])) 624 | self.assertEqual(bob.get_group_permissions(), set([])) 625 | self.assertEqual(bob.get_all_permissions(), set([])) 626 | 627 | 628 | class ROAExceptionsTests(ROAUserTestCase): 629 | 630 | def test_roa_errors(self): 631 | """ 632 | FIXME: Find a way to do the same test with unittests: 633 | 634 | > User.objects.create_user(username="alice", password="secret", email="alice@example.com") 635 | Traceback (most recent call last): 636 | ... 637 | ROAException: IntegrityError at /auth/user/: column username is not unique 638 | Request Method: POST 639 | Request URL: http://127.0.0.1:8081/auth/user/?format=django 640 | Exception Type: IntegrityError 641 | Exception Value: column username is not unique 642 | Exception Location: ..., line ... 643 | Status code: 500 644 | """ 645 | User.objects.create_user(username="alice", password="secret", email="alice@example.com") 646 | #User.objects.create_user(username="alice", password="secret", email="alice@example.com") 647 | 648 | 649 | class ROASettingsTests(ROATestCase): 650 | 651 | def tearDown(self): 652 | super(ROASettingsTests, self).tearDown() 653 | RemotePageWithCustomSlug.objects.all().delete() 654 | RemotePageWithOverriddenUrls.objects.all().delete() 655 | 656 | def test_custom_args(self): 657 | settings.ROA_CUSTOM_ARGS = {'foo': 'bar'} 658 | self.assertEqual(RemotePage.objects.all()._as_url(), (u'http://127.0.0.1:8081/django_roa_server/remotepage/', {'foo': 'bar', 'format': 'django'})) 659 | settings.ROA_CUSTOM_ARGS = {} 660 | 661 | def test_custom_slug(self): 662 | page_custom = RemotePageWithCustomSlug.objects.create(title=u"Test custom page") 663 | self.assertEqual(page_custom.slug, u'test-custom-page') 664 | page_custom = RemotePageWithCustomSlug.objects.get(title=u"Test custom page") 665 | self.assertEqual(repr(page_custom), '') 666 | page_custom.delete() 667 | 668 | def test_roa_url_overrides(self): 669 | page_overridden = RemotePageWithOverriddenUrls.objects.create(title=u"Test overridden urls") 670 | self.assertEqual(page_overridden.slug, u'test-overridden-urls') 671 | page_overridden = RemotePageWithOverriddenUrls.objects.get(title=u"Test overridden urls") 672 | self.assertEqual(repr(page_overridden), '') 673 | self.assertEqual(RemotePageWithOverriddenUrls.objects.all()._as_url(), (u'http://127.0.0.1:8081/django_roa_server/remotepagewithoverriddenurls/', {'format': 'django'})) 674 | 675 | def test_custom_serializer(self): 676 | register_serializer('custom', 'examples.django_roa_client.serializers') 677 | initial_roa_format_setting = settings.ROA_FORMAT 678 | settings.ROA_FORMAT = 'custom' 679 | page = RemotePage.objects.create(title=u'A custom serialized page') 680 | self.assertEqual(repr(page), '') 681 | r = Resource('http://127.0.0.1:8081/django_roa_server/remotepage/', filters=ROA_FILTERS) 682 | response = r.get(**{'format': 'custom'}) 683 | self.assertEqual(repr(response.body_string()), '\'\\n\\n \\n A custom serialized page\\n \\n\'') 684 | self.assertEqual(len(RemotePage.objects.all()), 1) 685 | page = RemotePage.objects.get(id=page.id) 686 | self.assertEqual(repr(page), '') 687 | settings.ROA_FORMAT = initial_roa_format_setting 688 | --------------------------------------------------------------------------------