Login

object-oriented generic views

Author:
carljm
Posted:
August 28, 2008
Language:
Python
Version:
.96
Score:
3 (after 5 ratings)

Here's an example of writing generic views in an object-oriented style, which allows for very fine-grained customization via subclassing. The snippet includes generic create and update views which are backwards compatible with Django's versions.

To use one of these generic views, it should be wrapped in a function that creates a new instance of the view object and calls it:

def create_object(request, *args, **kwargs):
    return CreateObjectView()(request, *args, **kwargs)

If an instance of one of these views is placed directly in the URLconf without such a wrapper, it will not be thread-safe.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
from django.template import RequestContext, loader
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.core.exceptions import ObjectDoesNotExist
from django import forms

class CreateObjectView(object):
    """
    Generic view to create instances of a model.

    """ 
        
    def __call__(self, request, model=None, form_class=None,
                 template_name=None, extra_context=None,
                 post_save_redirect=None):
        """
        Create a new object using a ModelForm. Accepts arguments:

        ``request``
            The HttpRequest object.

        ``model``
            Model type to create (either this or form_class is
            required)

        ``form_class``
            ModelForm subclass to use (either this or model is
            required)

        ``template_name``
            name of template to use, or list of templates - defaults
            to <app_label>/<model_name>_form.html

        ``extra_context``
            dictionary of items and/or callables to add to template
            context.

        ``post_save_redirect``
            URL to redirect to after successful object save. If
            post_save_redirect is None or an empty string, default is
            to send to the instances get_absolute_url method or in its
            absence, the site root.

        """
        self.request = request
        (self.model, self.form_class) = self.get_model_and_form_class(
            model, form_class)
        form = self.get_form(request, self.form_class)
        if request.method == 'POST' and form.is_valid():
            return self.get_redirect(post_save_redirect, self.save_form(form))

        c = self.apply_extra_context(extra_context,
                                     self.get_context(request,
                                                      {'form': form}))
        t = self.get_template(self.model, template_name)
        return self.get_response(t, c)
    
    def get_model_and_form_class(self, _model, form_class):
        """
        Return a model and form class based on model or form_class
        argument.
        
        """
        if _model is None:
            try:
                _model = form_class._meta.model
            except AttributeError:
                raise ValueError("%s requires either model or form_class" %
                                 (self.__class__.__name__,))
        if form_class is None:
            class Meta:
                model = _model
            class_name = _model.__name__ + 'Form'
            form_class = forms.models.ModelFormMetaclass(
                class_name, (forms.ModelForm,), {'Meta': Meta})
        return (_model, form_class)

    def apply_extra_context(self, extra_context, context):
        """
        Add items from extra_context dict to the given context,
        calling any callables in extra_context.  Return the updated
        context.

        """
        extra_context = extra_context or {}
        for key, value in extra_context.iteritems():
            if callable(value):
                context[key] = value()
            else:
                context[key] = value
        return context

    def get_form_kwargs(self, request):
        """
        Get dictionary of arguments to construct the appropriate
        ``form_class`` instance.

        """
        if request.method == 'POST':
            return {'data': request.POST, 'files': request.FILES}
        return {}

    def get_form(self, request, form_class):
        """
        Return the appropriate ``form_class`` instance based on the
        ``request``.

        """
        return form_class(**self.get_form_kwargs(request))

    def save_instance(self, obj):
        """
        Save and return model instance.

        """
        obj.save()
        return obj
    
    def save_form(self, form):
        """
        Save form, returning saved object.

        """
        return self.save_instance(form.save(commit=False))
    
    def get_redirect(self, post_save_redirect, obj):
        """
        Return a HttpResponseRedirect based on ``post_save_redirect``
        argument and just-saved object ``obj``.

        """
        if not post_save_redirect:
            if hasattr(obj, 'get_absolute_url'):
                post_save_redirect = obj.get_absolute_url()
            else:
                post_save_redirect = "/"
        return HttpResponseRedirect(post_save_redirect)

    def get_template(self, model, template_name):
        """
        Return a template to use based on ``template_name`` and ``model``.

        """
        template_name = template_name or "%s/%s_form.html" % (
            model._meta.app_label, model._meta.object_name.lower())
        if isinstance(template_name, (list, tuple)):
            return loader.select_template(template_name)
        else:
            return loader.get_template(template_name)

    def get_context(self, request, dictionary):
        """
        Return a context instance with data in ``dictionary``.

        """
        return RequestContext(request, dictionary)
        
    def get_response(self, template, context_instance):
        """
        Return a HttpResponse object based on given request, template,
        and context.

        """
        return HttpResponse(template.render(context_instance))


class UpdateObjectView(CreateObjectView):
    """
    Generic view to update instances of a model.

    """
    def __call__(self, request, object_id=None, slug=None, slug_field='slug',
                 *args, **kwargs):
        """
        Update an existing object using a ModelForm.  Accepts same
        arguments as CreateObjectView, and also:

        ``object_id``
            id of object to update (either this or slug+slug_field is
            required)

        ``slug``
            slug of object to update (either this or object_id is
            required)

        ``slug_field``
            field to look up slug in (defaults to ``slug``)
        
        """
        self.object_id = object_id
        self.slug = slug
        self.slug_field = slug_field
        return super(UpdateObjectView, self).__call__(request, *args, **kwargs)
    
    def get_model_and_form_class(self, *args, **kwargs):
        """
        Wrap parent ``get_model_and_form_class`` and save the model
        class so we can get to it in get_form_args.

        """
        ret = super(UpdateObjectView, self).get_model_and_form_class(*args,
                                                                      **kwargs)
        self.model = ret[0]
        return ret

    def get_form_kwargs(self, request):
        instance = self.lookup_object(self.model, self.object_id,
                                      self.slug, self.slug_field)
        kwargs = super(UpdateObjectView, self).get_form_kwargs(request)
        kwargs['instance'] = instance
        return kwargs

    def lookup_object(self, model, object_id, slug, slug_field):
        """
        Find and return an object of type ``model`` based on either
        the given ``object_id`` or ``slug`` and ``slug_field``.
        
        """
        lookup_kwargs = {}
        if object_id:
            lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
        elif slug and slug_field:
            lookup_kwargs['%s__exact' % slug_field] = slug
        else:
            raise ValueError("%s requires object_id or slug+slug_field"
                             % (self.__class__.__name__,))
        try:
            return model.objects.get(**lookup_kwargs)
        except ObjectDoesNotExist:
            raise Http404, "No %s found for %s" % (model._meta.verbose_name,
                                                   lookup_kwargs)
        

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

simon (on August 29, 2008):

I'm a big fan of this approach, but I don't think you're taking advantage of subclassing enough in this code. Instead of passing configuration parameters to the _call_ method I suggest having them as class properties, and then requiring that people subclass your generic views to customise them.

For example, your code could look like this:

class CreateObjectView(object):
    model = None
    form_class = None
    template_name = None
    ...

    def __call__(self, request):
        ...

Then your users would just have to do this:

class CreateArticleObject(CreateObjectView):
    model = Article

#

carljm (on August 29, 2008):

Thanks Simon. You're totally right, of course. I briefly thought of doing that, but for some reason I had in mind as a goal to preserve backwards-compatibility with Django's generic views. Now I can't actually think of a single good reason to do that.

#

jamesgpearce (on September 1, 2009):

Why have separate classes for Create, Update etc - each with their own __call__?

If you just have one class (say, Views), you could have create, update methods etc on it.

The URL dispatcher can call a 'instance.verb' method just as well as it can __call__ a 'verb_instance' class.

Firstly, you only have to instantiate one class for the URL dispatcher, and secondly (as Simon does), the class properties are only provided once, regardless of how many view verbs you have.

#

jamesgpearce (on September 1, 2009):

Oh - and now (v1.1) that the URL dispatcher include() can take sequences of patterns, the same class can even generate standardised URLs to tie to its own views.

#

artur_mwaigaryan (on October 29, 2011):

quite efficient,cuts out boiler-plate code,@simon thanks alot for the insight:-)

#

Please login first before commenting.