david / django-modelviews

Backup of an old repository with useful ideas. Initial goal: integrating REST to django admin (class-based views).

Clone this repository (size: 85.7 KB): HTTPS / SSH
$ hg clone http://code.welldev.org/django-modelviews
commit 6: 77fe6fc4bdf4
parent 5: 09d4c8655670
branch: default
Reverting to initial checkout.
Will Larson
2 years ago

Changed (Δ11.4 KB):

raw changeset »

__init__.py

base.py

generic/__init__.py (null-size change)

generic/rest_views.py (299 lines added, 0 lines removed)

generic/test-runner.py

generic/test_models.py

models.py

rest_views.py

test_project/__init__.py

test_project/manage.py

test_project/settings.py

test_project/templates/test_app/post_detail.html

test_project/templates/test_app/post_list.html

test_project/test_app/__init__.py

test_project/test_app/models.py

test_project/test_app/tests.py

test_project/test_app/views.py

test_project/urls.py

tests.py

Up to file-list generic/rest_views.py:

1
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
2
from django.http import Http404, HttpResponse, HttpResponseNotAllowed
3
from django.views.generic.base import BaseDetailView
4
5
6
class HtmlResponder(object):
7
    """
8
    HtmlResponder renders an object or a list of objects with the Django
9
    templating system.
10
    """
11
    def __init__(self, queryset):
12
        self.queryset = queryset
13
        
14
    def render_response(self, request, template, context_vars, mimetype=None):
15
        """
16
        Returns an HttpResponse for the given request, template object,
17
        dictionary of context variables, and optional mimetype.
18
        """
19
        context = RequestContext(request, context_vars)
20
        template = template.render(context)
21
        return HttpResponse(template, mimetype=mimetype)
22
    
23
    def get_template(self, opts, target='list'):
24
        """
25
        Returns a loaded template, the template_name depends of the model name.
26
        
27
        Examples::
28
        
29
            blog/post_list.html
30
            auth/user_detail.html
31
        """
32
        template_name = "%s/%s_%s.html" % (opts.app_label, opts.object_name.lower(), target)
33
        return loader.get_template(template_name)
34
    
35
    def get_context_vars(self, context_vars):
36
        """
37
        Returns a dictionary of context vars, override if you need more.
38
        """
39
        return context_vars
40
    
41
    def list(self, request, paginate_by, allow_empty):
42
        """
43
        Renders a list of model objects to HttpResponse.
44
        """
45
        if paginate_by:
46
            paginator = QuerySetPaginator(self.queryset, paginate_by,
47
                                          allow_empty_first_page=allow_empty)
48
            page = request.GET.get('page', 1)
49
            try:
50
                page_number = int(page)
51
            except ValueError:
52
                if page == 'last':
53
                    page_number = paginator.num_pages
54
                else:
55
                    # Page is not 'last', nor can it be converted to an int.
56
                    raise Http404
57
            try:
58
                page_obj = paginator.page(page_number)
59
            except InvalidPage:
60
                raise Http404
61
            object_list = page_obj.object_list
62
        else:
63
            object_list = self.queryset
64
            paginator = None
65
            page_obj = None
66
            if not allow_empty and len(self.queryset) == 0:
67
                raise Http404
68
        context_vars = self.get_context_vars({
69
            'object_list': object_list,
70
            'paginator': paginator,
71
            'page_obj': page_obj
72
        })
73
        opts = self.queryset.model._meta
74
        template = self.get_template(opts)
75
        return self.render_response(request, template, context_vars)
76
77
    def element(self, request, obj):
78
        """
79
        Renders single model objects to HttpResponse.
80
        """
81
        context_vars = self.get_context_vars({'object': obj})
82
        opts = self.queryset.model._meta
83
        template = self.get_template(opts, target='detail')
84
        response = self.render_response(request, template, context_vars)
85
        populate_xheaders(request, response, self.queryset.model, getattr(obj, opts.pk.name))
86
        return response
87
    
88
    def create_success(self, request, new_obj, post_save_redirect):
89
        """
90
        Returns an HttpResonse, generally an HttpResponse redirect. This will
91
        be the final return value of the view and will only be called if the
92
        object was saved successfuly.
93
        """
94
        # Redirect to the new object: first by trying post_save_redirect,
95
        # then by obj.get_absolute_url; fail if neither works.
96
        if post_save_redirect:
97
            return HttpResponseRedirect(post_save_redirect % new_obj.__dict__)
98
        elif hasattr(new_obj, 'get_absolute_url'):
99
            return HttpResponseRedirect(new_obj.get_absolute_url())
100
        else:
101
            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
102
103
    def update_success(self, request, obj, new_obj, post_save_redirect):
104
        """
105
        Returns an HttpResonse, generally an HttpResponse redirect. This will
106
        be the final return value of the view and will only be called if the
107
        object was saved successfuly.
108
        """
109
        # Redirect to the new object: first by trying post_save_redirect,
110
        # then by obj.get_absolute_url; fail if neither works.
111
        if post_save_redirect:
112
            return HttpResponseRedirect(post_save_redirect % new_obj.__dict__)
113
        elif hasattr(new_obj, 'get_absolute_url'):
114
            return HttpResponseRedirect(new_obj.get_absolute_url())
115
        else:
116
            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
117
118
    def delete_success(self, post_save_redirect):
119
        return HttpResponseRedirect(post_save_redirect)
120
        
121
122
class JsonResponder(object):
123
    """TODO"""
124
125
126
class ModelView(BaseDetailView):
127
    """
128
    ModelView: a RESTful class-based view of your resources
129
    =======================================================
130
    
131
    Philosophy
132
    ----------
133
    
134
    The goal of this class is to provide a lightweight REST interface, I know
135
    that django-rest-interface exists but it's abandoned and it suffers from
136
    some defaults. Given my experience with it, I always need to rewrite large
137
    parts of the library in order to customize it to fits with my own needs.
138
    
139
    In order to avoid this with ModelView, the philosophy is to allow 
140
    dead-easy subclassing with granularity at each point.
141
    
142
    Note: I totally respect what had been done before and this class is 
143
    inspired from a lot of existing projects so many thanks to all authors.
144
    
145
    
146
    Goals
147
    -----
148
    
149
    Basically, it dispatches requests to the appropriated function given the
150
    HTTP verb and it allows you to specify a responder in order to avoid 
151
    duplication of code at the resource logic level.
152
    
153
    You can specify your own responders, and restrict to allowed HTTP methods.
154
    
155
    Secret goal: to be honest, I dream of resource oriented stuff in 
156
    Django's trunk for years now. A RESTful Django's (newforms-)admin could be
157
    awesome too in order to provide a built-in API!
158
        
159
    
160
    Example
161
    -------
162
    
163
    A quick example in order to demonstrate what is possible.
164
    
165
    models.py::
166
    
167
        class Post(models.Model):
168
            title = models.Charfield(max_length=200)
169
            slug = models.SlugField(max_length=200, prepopulate_from=('title',), unique=True)
170
            content = models.TextField()
171
        
172
    urls.py::
173
    
174
        blog = ModelView(Post.objects.filter(is_online=True), responders=(HtmlResponder, JsonResponder), methods=('GET',))
175
        blog_admin = ModelView(Post.objects.all(), methods=('GET', 'POST', 'PUT', 'DELETE'))
176
        urlpatterns = pattern('', 
177
            url(^blog/(?P<slug>[-\w]+)/(?P<format>(html|json))?/?$), blog, name='blog'),
178
            url(^blog/admin/(?P<object_pk>\d)/), blog_admin, name='blog_admin'),
179
        )
180
    
181
    Note: here we consider that you want a custom admin for your blog,
182
    otherwise Django's built-in admin is much more interesting, of course.
183
    
184
    
185
    TODO
186
    ----
187
    
188
        * Add tests and documentation
189
        * Create a collection of generic Responders
190
        * Handle receivers in order to use it as an API (for the moment,
191
          it assumes that you receive formencoded data), need more reflexion.
192
        * Handle privacy, need more reflexion
193
        * See what can be done with APP, it could be fun!
194
        * Ideas?
195
    
196
    """
197
    def __init__(self, queryset, slug_field='slug', post_save_redirect=None,
198
            paginate_by=None, allow_empty=True,
199
            responders=(HtmlResponder,), methods=('GET', 'POST', 'PUT', 'DELETE')):
200
        self.post_save_redirect = post_save_redirect
201
        self.paginate_by = paginate_by
202
        self.allow_empty = allow_empty
203
        self.responders = dict((responder.__class__.__name__, responder) for responder in responders)
204
        self.methods = methods
205
        super(ModelView, self).__init__(queryset, slug_field)
206
207
    def __call__(self, request, object_pk=None, slug=None, format='html'):
208
        """
209
        Redirects to one of the CRUD methods depending on the HTTP method of 
210
        the request. Checks whether the requested method is allowed for this 
211
        resource.
212
        """
213
        # If we didn't get object_pk or slug, assume this is an add view.
214
        if object_pk is None and slug is None:
215
            obj = None
216
        else:
217
            obj = self.get_object(request, object_pk, slug)
218
        
219
        # Restrict
220
        request_method = request.method.upper()
221
        if request_method not in self.methods:
222
            raise HttpMethodNotAllowed
223
        
224
        # Dispatch
225
        if request_method == 'GET':
226
            return self.read(request, obj, self.get_responder(format))
227
        elif request_method == 'POST':
228
            return self.create(request, self.get_responder(format))
229
        elif request_method == 'PUT':
230
            return self.update(request, obj, self.get_responder(format))
231
        elif request_method == 'DELETE':
232
            return self.delete(request, obj, self.get_responder(format))
233
        else:
234
            raise Http404
235
236
    def get_responder(self, format):
237
        """
238
        Returns a ``Responder`` instance given the format.
239
        """
240
        try:
241
            return self.responders['%sResponder' % format.title()](self.get_query_set())
242
        except KeyError:
243
            raise ImproperlyConfigured("%s responder doesn't exist." % '%sResponder' % format.title())
244
245
    def get_form(self, request):
246
        """
247
        Returns a ``ModelForm`` class to be used in this view.
248
        """
249
        # TODO: we should be able to construct a ModelForm without creating
250
        # and passing in a temporary inner class
251
        class Meta:
252
            model = self.model
253
        class_name = self.model.__name__ + 'Form'
254
        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
255
256
    def save_form(self, request, form):
257
        """
258
        Saves and returns the object represented by the given form. This
259
        method will only be called if the form is valid.
260
        """
261
        return form.save()
262
263
    def read(self, request, obj, responder):
264
        """
265
        Returns a rendered list or element whether ``obj`` exists, idempotent.
266
        """
267
        if obj is None:
268
            return responder.list(request, self.paginate_by, self.allow_empty)
269
        else:
270
            return responder.element(request, obj)
271
272
    def create(self, request, responder):
273
        """
274
        Create a new resource.
275
        """
276
        Form = self.get_form(request)
277
        form = Form(request.POST, request.FILES)
278
        if form.is_valid():
279
            new_obj = self.save_form(request, form)
280
            return responder.create_success(request, new_obj, self.post_save_redirect)
281
282
    def update(self, request, obj, responder):
283
        """
284
        Update an existing resource (there is no PUT creation for now), idempotent.
285
        """
286
        Form = self.get_form(request)
287
        form = Form(request.POST, request.FILES, instance=obj)
288
        if form.is_valid():
289
            new_obj = self.save_form(request, form)
290
            return responder.update_success(request, obj, new_obj, self.post_save_redirect)
291
292
    def delete(self, request, obj, responder):
293
        """
294
        Delete an existing resource, idempotent.
295
        """
296
        if obj is not None:
297
            obj.delete()
298
        return responder.delete_success(self.post_save_redirect)
299