diff --git a/django/views/generic/base.py b/django/views/generic/base.py
new file mode 100644
index 0000000..ab90d91
--- /dev/null
+++ b/django/views/generic/base.py
@@ -0,0 +1,55 @@
+from django.http import Http404, HttpResponse
+from django.core.exceptions import ObjectDoesNotExist
+from django.template import RequestContext
+
+class BaseView(object):
+    """
+    Base class for creating class based view objects.
+    """
+    def __init__(self, queryset):
+        self.queryset = queryset
+        self.model = queryset.model
+
+    def get_query_set(self, request):
+        """
+        Hook to provide a custom queryset based on the request.
+        """
+        return self.queryset._clone()
+
+    def get_template(self, request, obj=None):
+        """
+        Returns a template object used to render this view.
+        """
+        raise NotImplementedError(u'Views must implement their own get_template method.')
+
+    def render_response(self, request, template, context_vars, mimetype=None):
+        """
+        Returns an HttpResponse for the given request, template object,
+        dictionary of context variables, and optional mimetype.
+        """
+        context = RequestContext(request, context_vars)
+        template = template.render(context)
+        return HttpResponse(template, mimetype=mimetype)
+
+class BaseDetailView(BaseView):
+    def __init__(self, queryset, slug_field='slug'):
+        self.slug_field = slug_field
+        super(BaseDetailView, self).__init__(queryset)
+
+    def get_object(self, request, object_pk=None, slug=None):
+        """
+        Returns the object to be viewed, changed or deleted, or raises a 404
+        if it doesn't exist.
+        """
+        # Look up the object to be changed or deleted
+        lookup_kwargs = {}
+        if object_pk:
+            lookup_kwargs['pk'] = object_pk
+        elif slug and self.slug_field:
+            lookup_kwargs[self.slug_field] = slug
+        else:
+            raise AttributeError("Generic view must be called with either an object_pk or a slug/slug_field")
+        try:
+            return self.get_query_set(request).get(**lookup_kwargs)
+        except ObjectDoesNotExist:
+            raise Http404, "No %s found for %s" % (self.model._meta.verbose_name, lookup_kwargs)
diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
index 46e92fe..fc88f13 100644
--- a/django/views/generic/create_update.py
+++ b/django/views/generic/create_update.py
@@ -7,6 +7,140 @@ from django.template import RequestContext
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
 from django.utils.translation import ugettext
+from django.newforms.models import ModelFormMetaclass, ModelForm
+from django.views.generic.base import BaseDetailView
+
+class EditView(BaseDetailView):
+    """
+    Create/Update generic view.
+
+    Templates: ``<app_label>/<model_name>_form.html``
+    Context:
+        form
+            the ``ModelForm`` instance for the object
+        object
+            the ``Model`` instance being changed
+    """
+    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
+        self.post_save_redirect = post_save_redirect
+        super(EditView, self).__init__(queryset, slug_field)
+
+    def __call__(self, request, object_pk=None, slug=None):
+        # If we didn't get object_pk or slug, assume this is an add view.
+        if object_pk is None and slug is None:
+            obj = None
+        else:
+            obj = self.get_object(request, object_pk, slug)
+        Form = self.get_form(request)
+        if request.POST:
+            form = Form(request.POST, request.FILES, instance=obj)
+            if form.is_valid():
+                new_obj = self.save_form(request, form)
+                return self.on_success(request, obj, new_obj)
+        else:
+            form = Form()
+        context_vars = {'object': obj, 'form': form}
+        template = self.get_template(request, obj)
+        return self.render_response(request, template, context_vars)
+
+    def get_form(self, request):
+        """
+        Returns a ``ModelForm`` class to be used in this view.
+        """
+        # TODO: we should be able to construct a ModelForm without creating
+        # and passing in a temporary inner class
+        class Meta:
+            model = self.model
+        class_name = self.model.__name__ + 'Form'
+        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
+
+    def get_template(self, request, obj=None):
+        """
+        Returns the template to be used when rendering this view. Those who
+        wish to use a custom template loader should do so here.
+        """
+        opts = self.model._meta
+        template_name = "%s/%s_form.html" % (opts.app_label, opts.object_name.lower())
+        return loader.get_template(template_name)
+
+    def get_message(self, request, obj, new_obj):
+        # If the primary ke of the original object is None, we just created an
+        # object, otherwise, we updated one.
+        if obj.pk is None:
+            return ugettext("The %s was created successfully.") % self.model._meta.verbose_name
+        return ugettext("The %s was updated successfully.") % self.model._meta.verbose_name
+
+    def save_form(self, request, form):
+        """
+        Saves and returns the object represented by the given form. This
+        method will only be called if the form is valid.
+        """
+        return form.save()
+
+    def on_success(self, request, obj, new_obj):
+        """
+        Returns an HttpResonse, generally an HttpResponse redirect. This will
+        be the final return value of the view and will only be called if the
+        object was saved successfuly.
+        """
+        # We can only do messaging for authenticated users right now
+        if request.user.is_authenticated():
+            message = self.get_message(request, new_obj)
+            request.user.message_set.create(message=message)
+        # Redirect to the new object: first by trying post_save_redirect,
+        # then by obj.get_absolute_url; fail if neither works.
+        if self.post_save_redirect:
+            return HttpResponseRedirect(self.post_save_redirect % new_obj.__dict__)
+        elif hasattr(new_obj, 'get_absolute_url'):
+            return HttpResponseRedirect(new_obj.get_absolute_url())
+        else:
+            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
+
+class DeleteView(BaseDetailView):
+    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
+        self.post_save_redirect = post_save_redirect
+        super(DeleteView, self).__init__(queryset, slug_field)
+
+    def __call__(self, request, object_pk=None, slug=None):
+        obj = self.get_object(request, object_pk, slug)
+        if request.method == 'POST':
+            self.delete(obj)
+            return self.on_success(request, obj)
+        context_vars = {'object': obj}
+        template = self.get_template(request, obj)
+        response = self.render_response(request, template, context_vars)
+        populate_xheaders(request, response, self.model, obj.pk)
+        return response
+
+    def get_template(self, request, obj=None):
+        opts = self.model._meta
+        template_name = "%s/%s_confirm_delete.html" % (opts.app_label, opts.object_name.lower())
+        return loader.get_template(template_name)
+
+    def delete(request, obj):
+        """
+        Deletes the given instance. Subclasses that wish to veto deletion
+        should do so here.
+        """
+        obj.delete()
+
+    def on_success(self, request, obj):
+        """
+        Redirects to self.post_save_redirect after setting a message if the
+        user is logged in.
+        
+        This method is only called if saving the object was successful.
+        """
+        if request.user.is_authenticated():
+            message = self.get_message(request, obj)
+            request.user.message_set.create(message=message)
+        return HttpResponseRedirect(self.post_save_redirect)
+
+    def get_message(self, request, new_object):
+        return ugettext("The %s was deleted.") % self.model._meta.verbose_name
+
+
+# Classic generic views ######################################################
 
 def create_object(request, model, template_name=None,
         template_loader=loader, extra_context=None, post_save_redirect=None,
diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
index cb9b014..2029a8a 100644
--- a/django/views/generic/list_detail.py
+++ b/django/views/generic/list_detail.py
@@ -3,6 +3,104 @@ from django.http import Http404, HttpResponse
 from django.core.xheaders import populate_xheaders
 from django.core.paginator import QuerySetPaginator, InvalidPage
 from django.core.exceptions import ObjectDoesNotExist
+from django.views.generic.base import BaseView, BaseDetailView
+
+class ObjectList(BaseView):
+    """
+    Generic list of objects.
+
+    Templates: ``<app_label>/<model_name>_list.html``
+    Context:
+        object_list
+            list of objects
+        paginator
+            a ``QuerySetPaginator``object if pagination is enabled, None otherwise
+        page_obj
+            a ``Page`` object if pagination is enabled, None otherwise
+    """
+    def __init__(self, queryset, paginate_by=None, allow_empty=True):
+        self.paginate_by = paginate_by
+        self.allow_empty = allow_empty
+        super(ObjectList, self).__init__(queryset)
+
+    def __call__(self, request, page=None):
+        queryset = self.get_query_set(request)
+        if self.paginate_by:
+            paginator = QuerySetPaginator(queryset, self.paginate_by,
+                                          allow_empty_first_page=self.allow_empty)
+            if not page:
+                page = request.GET.get('page', 1)
+            try:
+                page_number = int(page)
+            except ValueError:
+                if page == 'last':
+                    page_number = paginator.num_pages
+                else:
+                    # Page is not 'last', nor can it be converted to an int.
+                    raise Http404
+            try:
+                page_obj = paginator.page(page_number)
+            except InvalidPage:
+                raise Http404
+            object_list = page_obj.object_list
+        else:
+            object_list = queryset
+            paginator = None
+            page_obj = None
+            if not self.allow_empty and len(queryset) == 0:
+                raise Http404
+        context_vars = {
+            'object_list': object_list,
+            'paginator': paginator,
+            'page_obj': page_obj
+        }
+        template = self.get_template(request)
+        return self.render_response(request, template, context_vars)
+
+    def get_template(self, request, obj=None):
+        """
+        Returns the template to be used when rendering this view. Those who
+        wish to use a custom template loader should do so here.
+        """
+        opts = self.queryset.model._meta
+        template_name = "%s/%s_list.html" % (opts.app_label, opts.object_name.lower())
+        return loader.get_template(template_name)
+
+class ObjectDetail(BaseDetailView):
+    """
+    Generic detail of an object.
+
+    Templates: ``<app_label>/<model_name>_detail.html``
+    Context:
+        object
+            the object
+    """
+    def __init__(self, queryset, slug_field='slug'):
+        super(ObjectDetail, self).__init__(queryset, slug_field)
+
+    def __call__(self, request, object_pk=None, slug=None):
+        queryset = self.get_query_set(request)
+        opts = queryset.model._meta
+        try:
+            obj = self.get_object(request, object_pk, slug)
+        except ObjectDoesNotExist:
+            raise Http404(u"No %s found matching the query" % opts.verbose_name)
+        context_vars = {'object': obj}
+        template = self.get_template(request, obj)
+        response = self.render_response(request, template, context_vars)
+        populate_xheaders(request, response, queryset.model, getattr(obj, opts.pk.name))
+        return response
+
+    def get_template(self, request, obj=None):
+        """
+        Returns the template to be used when rendering this view. Those who
+        wish to use a custom template loader should do so here.
+        """
+        opts = self.queryset.model._meta
+        template_name = "%s/%s_detail.html" % (opts.app_label, opts.object_name.lower())
+        return loader.get_template(template_name)
+
+# Legacy generic views #######################################################
 
 def object_list(request, queryset, paginate_by=None, page=None,
         allow_empty=True, template_name=None, template_loader=loader,
diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py
index 4bed1f3..472ac2b 100644
--- a/tests/regressiontests/views/models.py
+++ b/tests/regressiontests/views/models.py
@@ -20,7 +20,6 @@ class Article(models.Model):
     slug = models.SlugField()
     author = models.ForeignKey(Author)
     date_created = models.DateTimeField()
-    
+
     def __unicode__(self):
         return self.title
-
diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
index 2c8c5b4..3def6a9 100644
--- a/tests/regressiontests/views/tests/__init__.py
+++ b/tests/regressiontests/views/tests/__init__.py
@@ -1,4 +1,6 @@
 from defaults import *
 from i18n import *
 from static import *
-from generic.date_based import *
\ No newline at end of file
+from generic.list_detail import *
+from generic.date_based import *
+from generic.create_update import *
diff --git a/tests/regressiontests/views/tests/generic/create_update.py b/tests/regressiontests/views/tests/generic/create_update.py
new file mode 100644
index 0000000..7c59eb4
--- /dev/null
+++ b/tests/regressiontests/views/tests/generic/create_update.py
@@ -0,0 +1,81 @@
+# coding: utf-8 
+from django.test import TestCase 
+from regressiontests.views.models import Article, Author
+
+class AddViewTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_initial(self):
+        response = self.client.get('/views/create_update/article/add/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.context['form']._meta.model, Article)
+        self.assertEqual(response.context['object'], None)
+
+    def test_submit(self):
+        response = self.client.post('/views/create_update/article/add/', {
+            'author': '1',
+            'title': "Don't read this",
+            'slug': 'dont-read-this',
+            'date_created': '2001-01-01 21:22:23'
+        })
+        self.assertEqual(response.status_code, 302)
+
+class ChangeViewByIdTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_initial(self):
+        response = self.client.get('/views/create_update/article/1/change/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.context['form']._meta.model, Article)
+        self.assertEqual(response.context['object'].title, u'Old Article')
+
+    def test_submit(self):
+        response = self.client.post('/views/create_update/article/1/change/', {
+            'author': '1',
+            'title': 'Ta Da!',
+            'slug': 'ta-da',
+            'date_created': '2001-01-01 21:22:23'
+        })
+        self.assertEqual(response.status_code, 302)
+
+class ChangeViewBySlugTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_initial(self):
+        response = self.client.get('/views/create_update/article/old_article/change/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.context['form']._meta.model, Article)
+        self.assertEqual(response.context['object'].title, u'Old Article')
+
+    def test_submit(self):
+        response = self.client.post('/views/create_update/article/old_article/change/', {
+            'author': '1',
+            'title': 'Ta Da!',
+            'slug': 'ta-da',
+            'date_created': '2001-01-01 21:22:23'
+        })
+        self.assertEqual(response.status_code, 302)
+
+class DeleteViewByIdTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_initial(self):
+        response = self.client.get('/views/create_update/article/1/delete/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.context['object'].title, u'Old Article')
+
+    def test_submit(self):
+        response = self.client.post('/views/create_update/article/1/delete/', {})
+        self.assertEqual(response.status_code, 302)
+
+class DeleteViewBySlugTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_initial(self):
+        response = self.client.get('/views/create_update/article/old_article/delete/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.context['object'].title, u'Old Article')
+
+    def test_submit(self):
+        response = self.client.post('/views/create_update/article/old_article/delete/', {})
+        self.assertEqual(response.status_code, 302)
diff --git a/tests/regressiontests/views/tests/generic/list_detail.py b/tests/regressiontests/views/tests/generic/list_detail.py
new file mode 100644
index 0000000..de9d50e
--- /dev/null
+++ b/tests/regressiontests/views/tests/generic/list_detail.py
@@ -0,0 +1,29 @@
+# coding: utf-8 
+from django.test import TestCase 
+from regressiontests.views.models import Article, Author
+
+class ObjectListViewTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_basic(self):
+        response = self.client.get('/views/articles/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.template.name, 'views/article_list.html')
+        self.assertEqual(len(response.context['object_list']), 3)
+        self.assertEqual(response.context['paginator'], None)
+        self.assertEqual(response.context['page_obj'], None)
+
+class ObjectDetailViewTest(TestCase):
+    fixtures = ['testdata.json']
+
+    def test_by_id(self):
+        response = self.client.get('/views/articles/1/')
+        self.assertEqual(response.status_code, 200) 
+        self.assertEqual(response.template.name, 'views/article_detail.html')
+        self.assertEqual(response.context['object'].title, u'Old Article')
+
+    def test_by_slug(self):
+        response = self.client.get('/views/articles/old_article/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.template.name, 'views/article_detail.html')
+        self.assertEqual(response.context['object'].title, u'Old Article')
diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
index 5ef0c51..5c3325e 100644
--- a/tests/regressiontests/views/urls.py
+++ b/tests/regressiontests/views/urls.py
@@ -1,6 +1,8 @@
 from os import path
 
 from django.conf.urls.defaults import *
+from django.views.generic.list_detail import ObjectList, ObjectDetail
+from django.views.generic.create_update import EditView, DeleteView
 
 from models import *
 import views
@@ -9,6 +11,15 @@ base_dir = path.dirname(path.abspath(__file__))
 media_dir = path.join(base_dir, 'media')
 locale_dir = path.join(base_dir, 'locale')
 
+# List/Detail views
+article_list = ObjectList(Article.objects.all())
+article_detail = ObjectDetail(Article.objects.all())
+
+# Create/Update/Delete Views
+article_add = article_change = EditView(Article.objects.all(), post_save_redirect='../')
+article_delete = DeleteView(Article.objects.all())
+
+
 js_info_dict = {
     'domain': 'djangojs',
     'packages': ('regressiontests.views',),
@@ -34,8 +45,20 @@ urlpatterns = patterns('',
     
     # Static views
     (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
-    
-	# Date-based generic views
+
+    # Create/Update generic views
+    (r'create_update/article/add/$', article_add),
+    (r'create_update/article/(?P<object_pk>\d+)/change/$', article_change),
+    (r'create_update/article/(?P<slug>\w+)/change/$', article_change),
+    (r'create_update/article/(?P<object_pk>\d+)/delete/$', article_delete),
+    (r'create_update/article/(?P<slug>\w+)/delete/$', article_delete),
+
+    (r'articles/$', article_list),
+    (r'articles/(?P<object_pk>\d+)/$', article_detail),
+    (r'articles/(?P<slug>\w+)/$', article_detail),
+
+
+    # Date-based generic views
     (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$', 
         'django.views.generic.date_based.object_detail', 
         dict(slug_field='slug', **date_based_info_dict)), 
diff --git a/tests/templates/views/article_confirm_delete.html b/tests/templates/views/article_confirm_delete.html
new file mode 100644
index 0000000..3f8ff55
--- /dev/null
+++ b/tests/templates/views/article_confirm_delete.html
@@ -0,0 +1 @@
+This template intentionally left blank
\ No newline at end of file
diff --git a/tests/templates/views/article_form.html b/tests/templates/views/article_form.html
new file mode 100644
index 0000000..3f8ff55
--- /dev/null
+++ b/tests/templates/views/article_form.html
@@ -0,0 +1 @@
+This template intentionally left blank
\ No newline at end of file
diff --git a/tests/templates/views/article_list.html b/tests/templates/views/article_list.html
new file mode 100644
index 0000000..3f8ff55
--- /dev/null
+++ b/tests/templates/views/article_list.html
@@ -0,0 +1 @@
+This template intentionally left blank
\ No newline at end of file
