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 5: 09d4c8655670
parent 4: f1e139854d36
branch: tests
Testing branch, based on BaseView instead of BaseDetailView.
Will Larson
2 years ago

Changed (Δ18.9 KB):

raw changeset »

base.py (100 lines added, 0 lines removed)

generic/__init__.py

generic/rest_views.py

generic/test-runner.py (10 lines added, 30 lines removed)

generic/test_models.py (1 lines added, 1 lines removed)

generic/test_settings.py

generic/test_urls.py

generic/tests.py

models.py (null-size change)

rest_views.py (315 lines added, 0 lines removed)

test_project/__init__.py (null-size change)

test_project/manage.py (11 lines added, 0 lines removed)

test_project/settings.py (28 lines added, 0 lines removed)

test_project/templates/test_app/post_detail.html (4 lines added, 0 lines removed)

test_project/templates/test_app/post_list.html (6 lines added, 0 lines removed)

test_project/test_app/__init__.py (null-size change)

test_project/test_app/models.py (9 lines added, 0 lines removed)

test_project/test_app/tests.py (25 lines added, 0 lines removed)

test_project/test_app/views.py (1 lines added, 0 lines removed)

test_project/urls.py (17 lines added, 0 lines removed)

tests.py (null-size change)

Up to file-list base.py:

1
from django.template import RequestContext, loader
2
from django.http import Http404, HttpResponse, HttpResponseRedirect
3
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
4
from django.utils.translation import ugettext
5
from django import newforms as forms
6
from django.newforms.models import ModelFormMetaclass, ModelForm
7
8
9
class BaseView(object):
10
    """
11
    Base class for generic object creation and update view.
12
13
    Templates: ``<app_label>/<model_name>_form.html``
14
    Context:
15
        form
16
            the ``ModelForm`` instance for the object
17
    """
18
    def __init__(self, model, post_save_redirect=None):
19
        self.model = model
20
        self.post_save_redirect = None
21
22
    def __call__(self, request):
23
        return self.main(request, self.get_instance(request))
24
25
    def main(self, request, instance):
26
        Form = self.get_form(request)
27
        if request.POST:
28
            form = Form(request.POST, request.FILES, instance=instance)
29
            if form.is_valid():
30
                new_object = self.save(request, form)
31
                return self.on_success(request, new_object)
32
        else:
33
            form = Form()
34
        rendered_template = self.get_rendered_template(request, instance, form)
35
        return HttpResponse(rendered_template)
36
37
    def get_form(self, request):
38
        """
39
        Returns a ``ModelForm`` class to be used in this view.
40
        """
41
        # TODO: we should be able to construct a ModelForm without creating
42
        # and passing in a temporary inner class
43
        class Meta:
44
            model = self.model
45
        class_name = self.model.__name__ + 'Form'
46
        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
47
48
    def get_context(self, request, instance, form=None):
49
        """
50
        Returns a ``Context`` instance to be used when rendering this view.
51
        """
52
        return RequestContext(request, {'form': form, 'object': instance})
53
54
    def get_template(self, request):
55
        """
56
        Returns the template to be used when rendering this view. Those who
57
        wish to use a custom template loader should do so here.
58
        """
59
        opts = self.model._meta
60
        template_name = "%s/%s_form.html" % (opts.app_label, opts.object_name.lower())
61
        return loader.get_template(template_name)
62
63
    def get_rendered_template(self, request, instance, form=None):
64
        """
65
        Returns a rendered template. This will be passed as the sole argument
66
        to HttpResponse()
67
        """
68
        template = self.get_template(request)
69
        context = self.get_context(request, instance, form)
70
        return template.render(context)
71
72
    def save(self, request, form):
73
        """
74
        Saves the object represented by the given ``form``. This method will
75
        only be called if the form is valid, and should in most cases return
76
        an HttpResponseRediect. It's return value will be the return value
77
        for the view on success.
78
        """
79
        return form.save()
80
81
    def on_success(self, request, new_object):
82
        """
83
        Returns an HttpResonse, generally an HttpResponse redirect. This will
84
        be the final return value of the view and will only be called if the
85
        object was saved successfuly.
86
        """
87
        if request.user.is_authenticated():
88
            message = self.get_message(request, new_object)
89
            request.user.message_set.create(message=message)
90
        # Redirect to the new object: first by trying post_save_redirect,
91
        # then by obj.get_absolute_url; fail if neither works.
92
        if self.post_save_redirect:
93
            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
94
        elif hasattr(new_object, 'get_absolute_url'):
95
            return HttpResponseRedirect(new_object.get_absolute_url())
96
        else:
97
            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
98
99
100

Up to file-list generic/test-runner.py:

@@ -32,36 +32,11 @@ except NameError:
32
32
##
33
33
34
34
class ClassViewTests(unittest.TestCase):
35
    def __init__(self):
36
        unittest.TestCase.__init__(self)
37
        self.model_label = "django-modelviews.generic.test_models"
38
39
35
    def runTest(self):
40
        from django.core.management.validation import get_validation_errors
41
        from django.db.models.loading import load_app
42
        from cStringIO import StringIO
43
        
44
        module = load_app(self.model_label)
45
46
        orig_stdout = sys.stdout
47
        s = StringIO()
48
        sys.stdout = s
49
        count = get_validation_errors(s, module)
50
        sys.stdout = orig_stdout
51
        s.seek(0)
52
        error_log = s.read()
53
        actual = error_log.split('\n')
54
        expected = module.model_errors.split('\n')
55
56
        unexpected = [err for err in actual if err not in expected]
57
        missing = [err for err in expected if err not in actual]
58
59
        self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected))
60
        self.assert_(not missing, "Missing Errors: " + '\n'.join(missing))
61
36
        suite = unittest.TestLoader().loadTestsFromTestCase(self.__class__)
37
        unittest.TextTestRunner(verbosity=1).run(suite)
62
38
63
39
    def setUp(self):
64
        print "setting up class view tests!"
65
40
        pass
66
41
67
42
    def tearDown(self):
@@ -72,6 +47,11 @@ class ClassViewTests(unittest.TestCase):
72
47
        self.assertEqual(0,1)
73
48
        #response = self.client.post('/test1/', test_data)
74
49
50
    def test_model(self):
51
        from test_models import Article
52
        a = Article.objects.create(headline="ahl")
53
        self.assertEqual(a.headline,"ahl")
54
75
55
76
56
77
57
##
@@ -93,9 +73,9 @@ def modelview_tests(verbosity, interacti
93
73
    if mod:
94
74
        settings.INSTALLED_APPS.append(model_label)
95
75
96
    extra_tests = [
97
        ClassViewTests(),
98
        ]
76
    extra_tests = [ClassViewTests(), ]
77
   
78
99
79
100
80
    # Run the test suite, including the extra validation tests.
101
81
    from django.test.simple import run_tests

Up to file-list generic/test_models.py:

@@ -2,7 +2,7 @@ from django.db import models
2
2
3
3
class Article(models.Model):
4
4
    headline = models.CharField(max_length=100)
5
    pub_date = models.DateTimeField()
5
    
6
6
7
7
    def __unicode__(self):
8
8
        return self.headline

Up to file-list rest_views.py:

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

Up to file-list test_project/manage.py:

1
#!/usr/bin/env python
2
from django.core.management import execute_manager
3
try:
4
    import settings # Assumed to be in the same directory.
5
except ImportError:
6
    import sys
7
    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__)
8
    sys.exit(1)
9
10
if __name__ == "__main__":
11
    execute_manager(settings)

Up to file-list test_project/settings.py:

1
import os, os.path
2
DEBUG = True
3
TEMPLATE_DEBUG = DEBUG
4
ADMINS = ()
5
MANAGERS = ADMINS
6
DATABASE_ENGINE = 'sqlite3'
7
DATABASE_NAME = u"%s/%s" % (os.getcwd(), "testdb.sqlite")
8
TIME_ZONE = 'America/Chicago'
9
LANGUAGE_CODE = 'en-us'
10
SITE_ID = 1
11
USE_I18N = True
12
MEDIA_ROOT = ''
13
MEDIA_URL = ''
14
ADMIN_MEDIA_PREFIX = '/media/'
15
SECRET_KEY = 'yi@)jk!m$r)^v_9)n8_ouzaey$!zqorqh&2)zfyq^=&_qe-x%^'
16
TEMPLATE_DIRS = (
17
    os.path.join(os.path.abspath(os.getcwd()), 'templates'),
18
)
19
20
print TEMPLATE_DIRS
21
22
MIDDLEWARE_CLASSES = ()
23
ROOT_URLCONF = 'test_project.urls'
24
TEMPLATE_DIRS = ()
25
INSTALLED_APPS = (
26
    'django_modelview',
27
    'django_modelview.test_project.test_app',
28
)

Up to file-list test_project/templates/test_app/post_detail.html:

1
2
<h3> {{ object.title }} </h3>
3
4
<p> {{ object.content }} </p>

Up to file-list test_project/templates/test_app/post_list.html:

1
2
{% for object in object_list %}
3
4
<h3> {{ object.title }} </h3>
5
6
{% endfor %}

Up to file-list test_project/test_app/models.py:

1
from django.db import models
2
3
class Post(models.Model):
4
    title = models.CharField(max_length=200)
5
    slug = models.SlugField(max_length=200, prepopulate_from=('title',), unique=True)
6
    content = models.TextField()
7
    
8
    def __unicode__(self):
9
        return self.title

Up to file-list test_project/test_app/tests.py:

1
import unittest
2
from django.test.client import Client
3
from models import Post
4
5
class modelTest(unittest.TestCase):
6
    def setUp(self):
7
        self.client = Client()
8
        Post.objects.create(title="abc",
9
                            slug="aabbcc",
10
                            content="A post.")
11
12
13
    def tearDown(self):
14
        for obj in Post.objects.all():
15
            obj.delete()
16
17
18
    def test_models(self):
19
        p = Post.objects.create(title="A simple post",
20
                                   slug="a-simple-post",
21
                                   content="A post.")
22
        self.assertEqual(p.title, "A simple post")
23
24
    def test_model_view(self):
25
        response = self.client.get('/blog/aabbcc/html/')

Up to file-list test_project/test_app/views.py:

1
# Create your views here.

Up to file-list test_project/urls.py:

1
from django.conf.urls.defaults import *
2
from django_modelview.rest_views import *
3
from test_app.models import Post
4
5
6
blog = ModelView(
7
    Post.objects.all(),
8
    responders=(HtmlResponder,), 
9
    methods=('GET',)
10
    )
11
12
13
urlpatterns = patterns(
14
    '', 
15
    (r'^blog/(?P<slug>[-\w]+)/(?P<format>(html|json))?/?$', blog),
16
17
    )