Skip to content

Commit

Permalink
Significant update
Browse files Browse the repository at this point in the history
Better namespacing of fieldnames
Better encapsulation of functionality
Use sessions to pass form data back to page/plugin when form is invalid, still fallback to GET params when necessary.
Call `form_valid` on plugin when form is valid
Pass source URL via form field instead of HTTP_REFERER
Don't clobber existing GET params
Support both forms.Form and forms.ModelForm
Cleaner code, use cached_property for efficiency
Updated README
Bump to 0.1.0
  • Loading branch information
mkoistinen committed Apr 24, 2016
1 parent a1e7cb3 commit 6386431
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 51 deletions.
47 changes: 42 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,45 @@
CMSPlugin Form Handler
======================

.. important::
This package aims to provide a mechanism for handling form-submissions in
django-CMS plugins.

This is only a Proof-of-concept, but it works!

Background
----------

Plugins are a key component of `django CMS <https://django-cms.org>`_ for
creating reusable, configurable content fragments in django CMS projects. Due to
their flexibility and utility, project developers would benefit from emitting
forms and handling form submissions using plugins.

Since CMS plugins are fragments of a page, they do not provide a unique, RESTful
URL for receiving and handling form submissions. This presents numerous
challenges when attempting to process form submissions.


Approach
--------

To get around these limitations, the approach taken in this package is to direct
form submissions from plugins which sub-class ``FormPluginBase`` to a URL that
is outside of the django CMS URL-space and handled by a ``ProcessFormView``
provided by this package.

The ``ProcessFormView`` accepts form-submissions, processes them, and if valid,
the request is redirected to a ``success_url`` provided by the plugin. On
validation errors, the view will redirect the request back to the originating
page and provide the form data via a session variable back to the plugin's form.

The user experience is precisely as expected and the handling of the form is
performed without "thrown HTTPRedirectResponses" or special middleware.

This package encapsulates all extra logic so that the plugin developer need
only to subclass ``cmsplugin_form_handler.cms_plugins.FormPluginBase`` rather
than the usual ``cms.plugin_base.CMSPluginBase``.

The ``Form`` or ``ModelForm`` presented in the CMS plugin should also include
the "mixin" ``cmsplugin_form_handler.forms.FormPluginFormMixin``.


------------
Expand Down Expand Up @@ -55,10 +91,10 @@ inherit from PluginFormBase as follows::
from cms.models import CMSPlugin

from cmsplugin_form_handler.cms_plugins import PluginFormBase
from cmsplugin_form_handler.forms import FormPluginFormBase
from cmsplugin_form_handler.forms import FormPluginFormMixin


class CoolForm(FormPluginFormBase):
class CoolForm(FormPluginFormMixin, forms.Form):
# Define your form as per usual...
cool_field = forms.CharField(...)

Expand All @@ -74,8 +110,9 @@ inherit from PluginFormBase as follows::

As usual, you must define a ``render_template`` for your plugin. Here's one::

{% load cmsplugin_form_tags %}
<h2>Form Plugin ({{ instance.pk }})</h2>
<form action="{{ plugin_form_action }}" method="post">
<form action="{% cmsplugin_form_action %}" method="post">
{% csrf_token %}
{{ form }}
<input type="submit">
Expand Down
8 changes: 7 additions & 1 deletion cmsplugin_form_handler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# -*- coding: utf-8 -*-

__version__ = '0.0.1'
from __future__ import unicode_literals

__version__ = '0.1.0'


def get_session_key(plugin_id):
return 'cmsplugin_form_{0}'.format(plugin_id)
50 changes: 37 additions & 13 deletions cmsplugin_form_handler/cms_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from __future__ import unicode_literals

from django.core.urlresolvers import reverse

from cms.plugin_base import CMSPluginBase

from . import get_session_key


class FormPluginBase(CMSPluginBase):
"""
Expand All @@ -19,28 +19,52 @@ class FormPluginBase(CMSPluginBase):

def get_form_class(self, instance):
"""
Override in subclass as required
Returns the form class to be used by this plugin.
Default implementation is to return the contents of
FormPluginBase.form_class, but this method can be overridden as
required for more elaborate circumstances.
"""
return self.form_class

def get_success_url(self, instance):
"""
Override in subclass as required
Returns the redirect URL for successful form submissions.
Default implementation is to return the contents of
FormPluginBase.success_url, but this method can be overridden as
required for more elaborate circumstances.
"""
return self.success_url

def form_valid(self, instance, form):
"""
If the form validates, this method will be called before the user is
redirected to the success_url. The default implementation is to just
save the form.
"""
form.save()

def render(self, context, instance, placeholder):
context = super(FormPluginBase, self).render(context, instance, placeholder)
context = super(FormPluginBase, self).render(context, instance, placeholder) # noqa
request = context.get('request')
form_class = self.get_form_class(instance)

form_class = self.get_form_class(instance)
if form_class:
context['cmsplugin_form_handler_action'] = reverse(
'cmsplugin_form_handler:process_form', args=(instance.pk, ))
instance_id = request.GET.get('instance_id')
if instance_id and int(instance_id) == instance.pk:
data = request.GET.copy()
context['form'] = form_class(instance, data=data)
source_url = request.path
data = None

if hasattr(request, 'session'):
data = request.session.get(get_session_key(instance.pk))
elif request.GET.get('cmsplugin_form_plugin_id'):
# Sessions aren't available, see if we fell-back to GET params
plugin_id = request.GET.get('cmsplugin_form_plugin_id')
if plugin_id and int(plugin_id) == instance.pk:
data = request.GET.copy()

if data:
context['cmsplugin_form'] = form_class(source_url, data=data)
else:
context['form'] = form_class(instance)
request.session.set_test_cookie()
context['cmsplugin_form'] = form_class(source_url)
return context
10 changes: 4 additions & 6 deletions cmsplugin_form_handler/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
from django import forms


class FormPluginFormBase(forms.Form):
instance_id = forms.IntegerField(widget=forms.HiddenInput)

def __init__(self, instance_id, *args, **kwargs):
super(FormPluginFormBase, self).__init__(*args, **kwargs)
self.fields['instance_id'].initial = instance_id
class FormPluginFormMixin(object):
def __init__(self, source_url, *args, **kwargs):
super(FormPluginFormMixin, self).__init__(*args, **kwargs)
self.fields['cmsplugin_form_source_url'] = forms.CharField(widget=forms.HiddenInput, initial=source_url)
4 changes: 4 additions & 0 deletions cmsplugin_form_handler/templatetags/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

37 changes: 37 additions & 0 deletions cmsplugin_form_handler/templatetags/cmsplugin_form_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

from django import template
from django.core.urlresolvers import reverse

from classytags.arguments import Argument
from classytags.core import Options
from classytags.helpers import AsTag

register = template.Library()


class FormAction(AsTag):
name = 'cmsplugin_form_action'
options = Options(
Argument('plugin_id', required=False, resolve=False),
'as',
Argument('varname', required=False, resolve=False),
)

def get_value(self, context, **kwargs):
"""
If no «plugin_id» is provided, then set plugin_id to `instance.pk`
where `instance` comes from the context and is the plugin instance
which is conventionally added to the context in CMSPluginBase.render().
"""
plugin_id = kwargs.get('plugin_id')
if not plugin_id and 'instance' in context:
plugin_id = context.get('instance').pk
return reverse(
'cmsplugin_form_handler:process_form',
args=(plugin_id, )
)

register.tag(FormAction)
2 changes: 1 addition & 1 deletion cmsplugin_form_handler/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
app_name = 'plugin_form_handler'

urlpatterns = [
url(r'^(?P<instance_id>\d+)/$', ProcessFormView.as_view(), name='process_form'),
url(r'^(?P<plugin_id>\d+)/$', ProcessFormView.as_view(), name='process_form'),
]
102 changes: 77 additions & 25 deletions cmsplugin_form_handler/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

from __future__ import unicode_literals

import urllib.parse
try:
from urllib.parse import urlparse, urlencode # py3
except ImportError:
from urlparse import urlparse # py2
from urllib import urlencode # py2

from django.shortcuts import Http404, redirect
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect
from django.utils.functional import cached_property
from django.views.generic import FormView

from cms.models import CMSPlugin

from . import get_session_key


class ProcessFormView(FormView):
"""
Expand All @@ -19,50 +27,94 @@ class ProcessFormView(FormView):
If the form is not valid, then send it back whence it came.
"""
http_method_names = ['post', ]
http_method_names = ['post', 'put', ]

def get_plugin(self):
@cached_property
def plugin(self):
"""
Returns (instance, plugin) for the source plugin if found, else 404.
"""
if self.kwargs['instance_id']:
cms_plugin_instance = CMSPlugin.objects.get(pk=self.kwargs['instance_id'])
return cms_plugin_instance.get_plugin_instance()
raise Http404('Source plugin not found.')
try:
plugin_id = int(self.kwargs.get('plugin_id'))
cms_plugin_instance = CMSPlugin.objects.get(pk=plugin_id)
except (KeyError, TypeError, CMSPlugin.DoesNotExist) as e:
raise ImproperlyConfigured('Source form plugin not found.')
return cms_plugin_instance.get_plugin_instance()

@cached_property
def source_url(self):
source_url = self.request.POST.get('cmsplugin_form_source_url')
return source_url

def get_form_class(self):
instance, plugin = self.get_plugin()
instance, plugin = self.plugin
if hasattr(plugin, 'get_form_class'):
return plugin.get_form_class(instance)
else:
raise Http404('Source plugin does not define `get_form_class()`.')
raise ImproperlyConfigured(
'Source form plugin does not define `get_form_class()`.')

def get_form_kwargs(self):
kwargs = super(ProcessFormView, self).get_form_kwargs()
kwargs['instance_id'] = self.kwargs['instance_id']
kwargs['source_url'] = self.source_url
return kwargs

def get_source_url(self):
url_obj = urllib.parse.urlparse(self.request.META.get('HTTP_REFERER'))
return url_obj.path

def get_success_url(self):
instance, plugin = self.get_plugin()
if hasattr(plugin, 'get_success_url'):
instance, plugin = self.plugin
try:
url = plugin.get_success_url(instance)
return url
raise Http404('Source plugin does not define `get_success_url()`.')
except AttributeError:
raise ImproperlyConfigured(
'Source plugin does not define `get_success_url()`.')

def get_form_valid(self):
"""
Returns the `form_valid()` callback as a bound method from the
source plugin.
"""
instance, plugin = self.plugin
try:
callback = plugin.on_valid_form(instance)
return callback
except AttributeError:
return None

def form_valid(self, form):
"""
Send the validated form back to the plugin for handling before
redirecting to the `success_url`.
"""
# Clean up our session var as it is no longer relevant.
if hasattr(self.request, 'session'):
session_key = get_session_key(self.plugin[0].pk)
if session_key in self.request.session:
del self.request.session[session_key]

# If the source plugin has declared a `form_valid` method, call it with
# the validated form before redirecting to the `success_url`.
instance, plugin = self.plugin
plugin.form_valid(instance, form)

return super(ProcessFormView, self).form_valid(form)

def form_invalid(self, form):
"""
Return to sender
"""
# Probably don't need this on the URL
plugin_id = self.plugin[0].pk
url = self.source_url
data = form.data.copy()
del(data['csrfmiddlewaretoken'])
if getattr(self.request, 'session'):
session_key = get_session_key(plugin_id)
self.request.session[session_key] = data
else:
# Fallback to GET params...
# Don't need this on the URL
del data['csrfmiddlewaretoken']
# We will need this though.
data['cmsplugin_form_plugin_id'] = plugin_id

url = '{0}?{1}'.format(
self.get_source_url(),
urllib.parse.urlencode(data),
)
params = urlparse(url)
params.update(data)
url = '{0}?{1}'.format(self.source_url, params)
return redirect(url)

0 comments on commit 6386431

Please sign in to comment.