Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The model form are supported in the formapi and details #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,45 @@ An invalid call with the parameters ``{'dividend': "five", 'divisor': 2}`` would
{"errors": {"dividend": ["Enter a number."]}, "data": false, "success": false}


You can use ``django-formapi`` to edit or update database objects too:

.. code:: python

class UserCall(calls.APIModelCall):

"""
Returns a serialized user
"""

class Meta:
model = User
fields = ('username', 'first_name', 'last_name')

API.register(UserCall, 'auth', 'user', version='v1.0.0')

If your request has the param ``__instance_pk`` (this param is customizable) then the api will edit this object. If the request does not have this param the api will create an object in database.

It's very common that in your form you need the request: the authenticated user, the cookies, the session, etc. For this case you can add ``request_passed`` attr in your call:


.. code:: python

class MyCall(calls.APICall):
...

request_passed = True

def __init__(self, request, *args, **kwargs):
super(MyCall, self).__init__(*args, **kwargs)
self.request = request

def action(self, test):
user = self.request.user
....

API.register(MyCall, 'mynamespace', 'mycall', version='v1.0.0')


Authentication
--------------
By default ``APICalls`` have HMAC-authentication turned on. Disable it by setting ``signed_requests = False`` on your ``APICall``.
Expand Down
2 changes: 1 addition & 1 deletion formapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = (0, 0, 7, 'dev')
VERSION = (0, 0, 8, 'dev')

# Dynamically calculate the version based on VERSION tuple
if len(VERSION) > 2 and VERSION[2] is not None:
Expand Down
37 changes: 27 additions & 10 deletions formapi/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from collections import defaultdict
from decimal import Decimal
import datetime
import decimal
import hmac
import itertools
import logging
import urllib2

from collections import defaultdict
from decimal import Decimal
from hashlib import sha1
from json import dumps, loads, JSONEncoder
import datetime

from django.db.models.query import QuerySet, ValuesQuerySet
from django.conf import settings
from django.core import serializers
from django.http import HttpResponse, Http404
Expand All @@ -16,9 +20,8 @@
from django.utils.importlib import import_module
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView
from django.db.models.query import QuerySet, ValuesQuerySet
from django.utils.functional import curry, Promise
import itertools

from .models import APIKey

LOG = logging.getLogger('formapi')
Expand Down Expand Up @@ -83,11 +86,13 @@ class API(FormView):
template_name = 'formapi/api/form.html'
signed_requests = True
call_mapping = defaultdict(lambda: defaultdict(dict))
request_passed = False
request_kwarg = 'request'

@classmethod
def register(cls, call_cls, namespace, name=None, version='beta'):
call_name = name or call_cls.__name__
API.call_mapping[version][namespace][call_name] = call_cls
cls.call_mapping[version][namespace][call_name] = call_cls

@classonlymethod
def as_view(cls, **initkwargs):
Expand All @@ -96,18 +101,31 @@ def as_view(cls, **initkwargs):

def get_form_class(self):
try:
return API.call_mapping[self.version][self.namespace][self.call]
return self.call_mapping[self.version][self.namespace][self.call]
except KeyError:
raise Http404

def get_form_kwargs(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this method can get pretty complex by time. If we could break out the get_instance code into a _get_model_form_kwargs that is run if isinstance(form_class, APIModelCall) is True and then skip the get_attr checks.

kwargs = super(API, self).get_form_kwargs()
form_class = self.get_form_class()
if getattr(form_class, 'request_passed', self.request_passed):
request_kwarg = getattr(form_class, 'request_kwarg', self.request_kwarg)
kwargs[request_kwarg] = self.request
get_instance = getattr(form_class, 'get_instance', None)
if get_instance:
instance = get_instance(self.request)
if instance:
kwargs['instance'] = instance
return kwargs

def get_access_params(self):
key = self.request.REQUEST.get('key')
sign = self.request.REQUEST.get('sign')
return key, sign

def sign_ok(self, sign):
pairs = ((field, self.request.REQUEST.get(field))
for field in sorted(self.get_form_class()().fields.keys()))
for field in sorted(self.get_form_class()(**self.get_form_kwargs()).fields.keys()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cleaner to do this as for field in sorted(self.get_form(self.get_form_class()).fields.keys()))

filtered_pairs = itertools.ifilter(lambda x: x[1] is not None, pairs)
query_string = '&'.join(('='.join(pair) for pair in filtered_pairs))
query_string = urllib2.quote(query_string.encode('utf-8'))
Expand Down Expand Up @@ -157,7 +175,7 @@ def setup_log(self, log):
self.log = AddHeaderAdapter(log, {'header': self.get_log_header()})

def authorize(self):
if getattr(self.get_form_class(), 'signed_requests', API.signed_requests):
if getattr(self.get_form_class(), 'signed_requests', self.signed_requests):
key, sign = self.get_access_params()
### Check for not revoked api key
try:
Expand All @@ -173,7 +191,6 @@ def authorize(self):
def dispatch(self, request, *args, **kwargs):
# Set up request
self.request = request

# Set up form class
self.version = kwargs['version']
self.namespace = kwargs['namespace']
Expand Down
43 changes: 35 additions & 8 deletions formapi/calls.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
from django.forms import forms
from django import forms
from django.core import serializers
from django.forms.forms import NON_FIELD_ERRORS
from django.shortcuts import get_object_or_404


class APICall(forms.Form):
class BaseAPICall(object):

request_passed = False
request_kwarg = 'request'

def add_error(self, error_msg):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if this isn't very clean and I agree it should be removed we can't just remove this without deprecating it first.

errors = self.non_field_errors()
errors.append(error_msg)
self._errors[forms.NON_FIELD_ERRORS] = errors

def clean(self):
for name, data in self.cleaned_data.iteritems():
setattr(self, name, data)
return super(APICall, self).clean()
self._errors[NON_FIELD_ERRORS] = errors

def action(self, test):
raise NotImplementedError('APIForms must implement action(self, test)')


class APICall(forms.Form, BaseAPICall):

instance_kwargs = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see where instance_kwargs is used? And if it's used doesn't it belong in the APIModelCall-class?



class APIModelCall(forms.ModelForm, BaseAPICall):

instance_pk_param = '__instance_pk'
pk_field = 'pk'

@classmethod
def get_instance(cls, request):
instance_pk = request.REQUEST.get(cls.instance_pk_param, None)
if instance_pk:
model_class = cls._meta.model
return get_object_or_404(model_class, **{cls.pk_field: cls.webiste_slug})
return None

def serialize_obj(self, obj):
return serializers.serialize('json', [obj])[1:-1]

def action(self, test):
obj = self.save()
return self.serialize_obj(obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you're serializing the object in the form and not letting render_to_json_response handle this? I don't think we should have two ways to serialize data returned from action()

1 change: 0 additions & 1 deletion formapi/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ def call(request, version, namespace, call_name):
'docstring': form_class.__doc__
}
return render(request, 'formapi/api/call.html', context)