Ticket #3639: 3639.diff

File 3639.diff, 24.3 KB (added by gwilson, 7 years ago)

some updates to Brian's latest patch

  • tests/regressiontests/views/tests/generic/create_update.py

    === added file 'tests/regressiontests/views/tests/generic/create_update.py'
     
     1import datetime
     2
     3from django.test import TestCase
     4from regressiontests.views.models import Article, Author
     5
     6class CreateObjectTest(TestCase):
     7    fixtures = ['testdata.json']
     8
     9    def test_not_logged_in(self):
     10        """Verifies the user is logged in through the login_required kwarg"""
     11        view_url = '/views/create_update/member/create/article/'
     12        response = self.client.get(view_url)
     13        self.assertRedirects(response, 'http://testserver/accounts/login/?next=%s' % view_url)
     14
     15    def test_create_article_display_page(self):
     16        """Ensures the generic view returned the page and contains a form."""
     17        view_url = '/views/create_update/create/article/'
     18        response = self.client.get(view_url)
     19        self.assertEqual(response.status_code, 200)
     20        if not response.context.get('form'):
     21            self.fail('No form found in the response.')
     22
     23    def test_create_article_with_errors(self):
     24        """POSTs a form that contain validation errors."""
     25        view_url = '/views/create_update/create/article/'
     26        response = self.client.post(view_url, {
     27            'title': 'My First Article',
     28        })
     29        self.assertFormError(response, 'form', 'slug', [u'This field is required.'])
     30
     31    def test_create_article(self):
     32        """Creates a new article through the generic view and ensures it gets
     33        redirected to the correct URL defined by post_save_redirect"""
     34        view_url = '/views/create_update/create/article/'
     35        response = self.client.post(view_url, {
     36            'title': 'My First Article',
     37            'slug': 'my-first-article',
     38            'author': '1',
     39            'date_created': datetime.datetime(2007, 6, 25),
     40        })
     41        self.assertRedirects(response,
     42            'http://testserver/views/create_update/view/article/my-first-article/',
     43            target_status_code=404)
     44
     45    def test_create_custom_save_article(self):
     46        """
     47        Creates a new article with a save method override to adjust the slug
     48        before committing to the database.
     49        """
     50        view_url = '/views/create_update/create_custom/article/'
     51        response = self.client.post(view_url, {
     52            'title': 'Test Article',
     53            'slug': 'this-should-get-replaced',
     54            'author': 1,
     55            'date_created': datetime.datetime(2007, 6, 25),
     56        })
     57        self.assertRedirects(response,
     58            'http://testserver/views/create_update/view/article/some-other-slug/',
     59            target_status_code=404)
     60
     61class UpdateDeleteObjectTest(TestCase):
     62    fixtures = ['testdata.json']
     63
     64    def test_update_object_form_display(self):
     65        """Verifies that the form was created properly and with initial values."""
     66        response = self.client.get('/views/create_update/update/article/old_article/')
     67        self.assertEquals(unicode(response.context['form']['title']),
     68            u'<input id="id_title" type="text" name="title" value="Old Article" maxlength="100" />')
     69
     70    def test_update_object(self):
     71        """Verifies the form POST data and performs a redirect to
     72        post_save_redirect"""
     73        response = self.client.post('/views/create_update/update/article/old_article/', {
     74            'title': 'Another Article',
     75            'slug': 'another-article-slug',
     76            'author': 1,
     77            'date_created': datetime.datetime(2007, 6, 25),
     78        })
     79        self.assertRedirects(response,
     80            'http://testserver/views/create_update/view/article/another-article-slug/',
     81            target_status_code=404)
     82        article = Article.objects.get(pk=1)
     83        self.assertEquals(article.title, "Another Article")
     84
     85    def test_delete_object_confirm(self):
     86        """Verifies the confirm deletion page is displayed using a GET."""
     87        response = self.client.get('/views/create_update/delete/article/old_article/')
     88        self.assertTemplateUsed(response, 'views/article_confirm_delete.html')
     89
     90    def test_delete_object_redirect(self):
     91        """Verifies that post_delete_redirect works properly."""
     92        response = self.client.post('/views/create_update/delete/article/old_article/')
     93        self.assertRedirects(response,
     94            'http://testserver/views/create_update/',
     95            target_status_code=404)
     96
     97    def test_delete_object(self):
     98        """Verifies the object actually gets deleted on a POST."""
     99        response = self.client.post('/views/create_update/delete/article/old_article/')
     100        try:
     101            Article.objects.get(slug='old_article')
     102        except Article.DoesNotExist:
     103            pass
     104        else:
     105            self.fail('Object was not deleted.')
  • tests/templates/views/article_confirm_delete.html

    === added file 'tests/templates/views/article_confirm_delete.html'
     
     1This template intentionally left blank
     2 No newline at end of file
  • tests/templates/views/article_form.html

    === added file 'tests/templates/views/article_form.html'
     
     1This template intentionally left blank
     2 No newline at end of file
  • django/views/generic/__init__.py

    === modified file 'django/views/generic/__init__.py'
     
     1class GenericViewError(Exception):
     2    """A problem in a generic view."""
     3    pass
  • django/views/generic/create_update.py

    === modified file 'django/views/generic/create_update.py'
     
     1from django.newforms.models import ModelFormMetaclass, ModelForm
     2from django.template import RequestContext, loader
     3from django.http import Http404, HttpResponse, HttpResponseRedirect
    14from django.core.xheaders import populate_xheaders
    2 from django.template import loader
    3 from django import oldforms
    4 from django.db.models import FileField
    5 from django.contrib.auth.views import redirect_to_login
    6 from django.template import RequestContext
    7 from django.http import Http404, HttpResponse, HttpResponseRedirect
    85from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
    96from django.utils.translation import ugettext
    10 
    11 def create_object(request, model, template_name=None,
     7from django.contrib.auth.views import redirect_to_login
     8from django.views.generic import GenericViewError
     9
     10def deprecate_follow(follow):
     11    """
     12    Raises a DeprecationWarning if follow is anything but None.
     13   
     14    The old Manipulator-based forms used an argument named follow, but it is
     15    no longer needed for newforms-based forms.
     16    """
     17    if follow is not None:
     18        raise DeprecationWarning(
     19            "Generic views have been changed to use newforms and the 'follow'"
     20            " argument is no longer supported.  Please update your code to use"
     21            " the new form_class argument in order to use a custom form.")
     22
     23def get_model_and_form_class(model, form_class):
     24    """
     25    Returns a model and form class based on the model and form_class
     26    parameters that were passed to the generic view.
     27   
     28    If ``form_class`` is given then its associated model will be returned along
     29    with ``form_class`` itself.  Otherwise, if ``model`` is given, ``model``
     30    itself will be returned along with a ``ModelForm`` class created from
     31    ``model``.
     32    """
     33    if form_class:
     34        return form_class._meta.model, form_class
     35    if model:
     36        # The inner Meta class fails if model = model is used for some reason.
     37        tmp_model = model
     38        # TODO: we should be able to construct a ModelForm without creating
     39        # and passing in a temporary inner class.
     40        class Meta:
     41            model = tmp_model
     42        class_name = model.__name__ + 'Form'
     43        form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
     44        return model, form_class
     45    raise GenericViewError("Generic view must be called with either a model or"
     46                           " form_class argument.")
     47
     48def create_object(request, model=None, template_name=None,
    1249        template_loader=loader, extra_context=None, post_save_redirect=None,
    13         login_required=False, follow=None, context_processors=None):
     50        login_required=False, follow=None, context_processors=None,
     51        form_class=None):
    1452    """
    1553    Generic object-creation function.
    1654
    1755    Templates: ``<app_label>/<model_name>_form.html``
    1856    Context:
    1957        form
    20             the form wrapper for the object
     58            the form for the object
    2159    """
     60    deprecate_follow(follow)
    2261    if extra_context is None: extra_context = {}
    2362    if login_required and not request.user.is_authenticated():
    2463        return redirect_to_login(request.path)
    25 
    26     manipulator = model.AddManipulator(follow=follow)
    27     if request.POST:
    28         # If data was POSTed, we're trying to create a new object
    29         new_data = request.POST.copy()
    30 
    31         if model._meta.has_field_type(FileField):
    32             new_data.update(request.FILES)
    33 
    34         # Check for errors
    35         errors = manipulator.get_validation_errors(new_data)
    36         manipulator.do_html2python(new_data)
    37 
    38         if not errors:
    39             # No errors -- this means we can save the data!
    40             new_object = manipulator.save(new_data)
    41 
     64   
     65    model, form_class = get_model_and_form_class(model, form_class)
     66    if request.method == 'POST':
     67        form = form_class(request.POST, request.FILES)
     68        if form.is_valid():
     69            new_object = form.save()
     70           
    4271            if request.user.is_authenticated():
    4372                request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
    4473
     
    5180            else:
    5281                raise ImproperlyConfigured("No URL to redirect to from generic create view.")
    5382    else:
    54         # No POST, so we want a brand new form without any data or errors
    55         errors = {}
    56         new_data = manipulator.flatten_data()
     83        form = form_class()
    5784
    58     # Create the FormWrapper, template, context, response
    59     form = oldforms.FormWrapper(manipulator, new_data, errors)
     85    # Create the template, context, response
    6086    if not template_name:
    6187        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    6288    t = template_loader.get_template(template_name)
     
    7096            c[key] = value
    7197    return HttpResponse(t.render(c))
    7298
    73 def update_object(request, model, object_id=None, slug=None,
     99def update_object(request, model=None, object_id=None, slug=None,
    74100        slug_field='slug', template_name=None, template_loader=loader,
    75101        extra_context=None, post_save_redirect=None,
    76102        login_required=False, follow=None, context_processors=None,
    77         template_object_name='object'):
     103        template_object_name='object', form_class=None):
    78104    """
    79105    Generic object-update function.
    80106
    81107    Templates: ``<app_label>/<model_name>_form.html``
    82108    Context:
    83109        form
    84             the form wrapper for the object
     110            the form for the object
    85111        object
    86112            the original object being edited
    87113    """
     114    deprecate_follow(follow)
    88115    if extra_context is None: extra_context = {}
    89116    if login_required and not request.user.is_authenticated():
    90117        return redirect_to_login(request.path)
    91 
     118   
     119    model, form_class = get_model_and_form_class(model, form_class)
     120   
    92121    # Look up the object to be edited
    93122    lookup_kwargs = {}
    94123    if object_id:
     
    96125    elif slug and slug_field:
    97126        lookup_kwargs['%s__exact' % slug_field] = slug
    98127    else:
    99         raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field")
     128        raise GenericViewError("Generic edit view must be called with either an object_id or a slug/slug_field")
    100129    try:
    101130        object = model.objects.get(**lookup_kwargs)
    102131    except ObjectDoesNotExist:
    103132        raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs)
    104133
    105     manipulator = model.ChangeManipulator(getattr(object, object._meta.pk.attname), follow=follow)
    106 
    107     if request.POST:
    108         new_data = request.POST.copy()
    109         if model._meta.has_field_type(FileField):
    110             new_data.update(request.FILES)
    111         errors = manipulator.get_validation_errors(new_data)
    112         manipulator.do_html2python(new_data)
    113         if not errors:
    114             object = manipulator.save(new_data)
     134    if request.method == 'POST':
     135        form = form_class(request.POST, request.FILES, instance=object)
     136        if form.is_valid():
     137            object = form.save()
    115138
    116139            if request.user.is_authenticated():
    117140                request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
     
    124147            else:
    125148                raise ImproperlyConfigured("No URL to redirect to from generic create view.")
    126149    else:
    127         errors = {}
    128         # This makes sure the form acurate represents the fields of the place.
    129         new_data = manipulator.flatten_data()
     150        form = form_class(instance=object)
    130151
    131     form = oldforms.FormWrapper(manipulator, new_data, errors)
    132152    if not template_name:
    133153        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    134154    t = template_loader.get_template(template_name)
     
    145165    populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname))
    146166    return response
    147167
    148 def delete_object(request, model, post_delete_redirect,
    149         object_id=None, slug=None, slug_field='slug', template_name=None,
    150         template_loader=loader, extra_context=None,
    151         login_required=False, context_processors=None, template_object_name='object'):
     168def delete_object(request, model, post_delete_redirect, object_id=None,
     169        slug=None, slug_field='slug', template_name=None,
     170        template_loader=loader, extra_context=None, login_required=False,
     171        context_processors=None, template_object_name='object'):
    152172    """
    153173    Generic object-delete function.
    154174
     
    172192    elif slug and slug_field:
    173193        lookup_kwargs['%s__exact' % slug_field] = slug
    174194    else:
    175         raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field")
     195        raise GenericViewError("Generic delete view must be called with either an object_id or a slug/slug_field")
    176196    try:
    177197        object = model._default_manager.get(**lookup_kwargs)
    178198    except ObjectDoesNotExist:
  • docs/generic_views.txt

    === modified file 'docs/generic_views.txt'
     
    701701      query string parameter (via ``GET``) or a ``page`` variable specified in
    702702      the URLconf. See `Notes on pagination`_ below.
    703703
    704     * ``page``: The current page number, as an integer. This is 1-based. 
     704    * ``page``: The current page number, as an integer. This is 1-based.
    705705      See `Notes on pagination`_ below.
    706706
    707707    * ``template_name``: The full name of a template to use in rendering the
     
    809809
    810810        /objects/?page=3
    811811
    812     * To loop over all the available page numbers, use the ``page_range`` 
    813       variable. You can iterate over the list provided by ``page_range`` 
     812    * To loop over all the available page numbers, use the ``page_range``
     813      variable. You can iterate over the list provided by ``page_range``
    814814      to create a link to every page of results.
    815815
    816816These values and lists are 1-based, not 0-based, so the first page would be
    817 represented as page ``1``. 
     817represented as page ``1``.
    818818
    819819An example of the use of pagination can be found in the `object pagination`_
    820 example model. 
    821          
     820example model.
     821
    822822.. _`object pagination`: ../models/pagination/
    823823
    824 **New in Django development version:** 
     824**New in Django development version:**
    825825
    826 As a special case, you are also permitted to use 
     826As a special case, you are also permitted to use
    827827``last`` as a value for ``page``::
    828828
    829829    /objects/?page=last
    830830
    831 This allows you to access the final page of results without first having to 
     831This allows you to access the final page of results without first having to
    832832determine how many pages there are.
    833833
    834834Note that ``page`` *must* be either a valid page number or the value ``last``;
     
    907907The ``django.views.generic.create_update`` module contains a set of functions
    908908for creating, editing and deleting objects.
    909909
     910**New in Django development version:**
     911
     912``django.views.generic.create_update.create_object`` and
     913``django.views.generic.create_update.update_object`` now use `newforms`_ to
     914build and display the form.
     915
     916.. _newforms: ../newforms/
     917
    910918``django.views.generic.create_update.create_object``
    911919----------------------------------------------------
    912920
    913921**Description:**
    914922
    915923A page that displays a form for creating an object, redisplaying the form with
    916 validation errors (if there are any) and saving the object. This uses the
    917 automatic manipulators that come with Django models.
     924validation errors (if there are any) and saving the object.
    918925
    919926**Required arguments:**
    920927
    921     * ``model``: The Django model class of the object that the form will
    922       create.
     928    * Either ``form_class`` or ``model`` is required.
     929
     930      If you provide ``form_class``, it should be a
     931      ``django.newforms.ModelForm`` subclass.  Use this argument when you need
     932      to customize the model's form.  See the `ModelForm docs`_ for more
     933      information.
     934
     935      Otherwise, ``model`` should be a Django model class and the form used
     936      will be a standard ``ModelForm`` for ``model``.
    923937
    924938**Optional arguments:**
    925939
     
    960974
    961975In addition to ``extra_context``, the template's context will be:
    962976
    963     * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form
    964       for editing the object. This lets you refer to form fields easily in the
     977    * ``form``: A ``django.newforms.ModelForm`` instance representing the form
     978      for creating the object. This lets you refer to form fields easily in the
    965979      template system.
    966980
    967       For example, if ``model`` has two fields, ``name`` and ``address``::
     981      For example, if the model has two fields, ``name`` and ``address``::
    968982
    969983          <form action="" method="post">
    970           <p><label for="id_name">Name:</label> {{ form.name }}</p>
    971           <p><label for="id_address">Address:</label> {{ form.address }}</p>
     984          <p>{{ form.name.label_tag }} {{ form.name }}</p>
     985          <p>{{ form.address.label_tag }} {{ form.address }}</p>
    972986          </form>
    973987
    974       See the `manipulator and formfield documentation`_ for more information
    975       about using ``FormWrapper`` objects in templates.
     988      See the `newforms documentation`_ for more information about using
     989      ``Form`` objects in templates.
    976990
    977991.. _authentication system: ../authentication/
    978 .. _manipulator and formfield documentation: ../forms/
     992.. _ModelForm docs: ../newforms/modelforms
     993.. _newforms documentation: ../newforms/
    979994
    980995``django.views.generic.create_update.update_object``
    981996----------------------------------------------------
     
    9881003
    9891004**Required arguments:**
    9901005
    991     * ``model``: The Django model class of the object that the form will
    992       create.
     1006    * Either ``form_class`` or ``model`` is required.
     1007
     1008      If you provide ``form_class``, it should be a
     1009      ``django.newforms.ModelForm`` subclass.  Use this argument when you need
     1010      to customize the model's form.  See the `ModelForm docs`_ for more
     1011      information.
     1012
     1013      Otherwise, ``model`` should be a Django model class and the form used
     1014      will be a standard ``ModelForm`` for ``model``.
    9931015
    9941016    * Either ``object_id`` or (``slug`` *and* ``slug_field``) is required.
    9951017
     
    10421064
    10431065In addition to ``extra_context``, the template's context will be:
    10441066
    1045     * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form
     1067    * ``form``: A ``django.newforms.ModelForm`` instance representing the form
    10461068      for editing the object. This lets you refer to form fields easily in the
    10471069      template system.
    10481070
    1049       For example, if ``model`` has two fields, ``name`` and ``address``::
     1071      For example, if the model has two fields, ``name`` and ``address``::
    10501072
    10511073          <form action="" method="post">
    1052           <p><label for="id_name">Name:</label> {{ form.name }}</p>
    1053           <p><label for="id_address">Address:</label> {{ form.address }}</p>
     1074          <p>{{ form.name.label_tag }} {{ form.name }}</p>
     1075          <p>{{ form.address.label_tag }} {{ form.address }}</p>
    10541076          </form>
    10551077
    1056       See the `manipulator and formfield documentation`_ for more information
    1057       about using ``FormWrapper`` objects in templates.
     1078      See the `newforms documentation`_ for more information about using
     1079      ``Form`` objects in templates.
    10581080
    10591081    * ``object``: The original object being edited. This variable's name
    10601082      depends on the ``template_object_name`` parameter, which is ``'object'``
  • tests/regressiontests/views/tests/__init__.py

    === modified file 'tests/regressiontests/views/tests/__init__.py'
     
    11from defaults import *
    22from i18n import *
    33from static import *
    4 from generic.date_based import *
    5  No newline at end of file
     4from generic.date_based import *
     5from generic.create_update import *
     6 No newline at end of file
  • tests/regressiontests/views/urls.py

    === modified file 'tests/regressiontests/views/urls.py'
     
    2020    'month_format': '%m',
    2121}
    2222
     23crud_form_info_dict = {
     24    'model': Article,
     25}
     26
    2327urlpatterns = patterns('',
    2428    (r'^$', views.index_page),
    2529   
     
    4448        dict(allow_future=True, slug_field='slug', **date_based_info_dict)),
    4549    (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$',
    4650        'django.views.generic.date_based.archive_month',
    47         date_based_info_dict),     
     51        date_based_info_dict),
     52   
     53    # crud generic views
     54    (r'^create_update/member/create/article/$', 'django.views.generic.create_update.create_object',
     55        dict(login_required=True, **crud_form_info_dict)),
     56    (r'^create_update/create/article/$', 'django.views.generic.create_update.create_object',
     57        dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', **crud_form_info_dict)),
     58    (r'^create_update/create_custom/article/$', views.custom_create),
     59    (r'create_update/update/article/(?P<slug>[-\w]+)/$', 'django.views.generic.create_update.update_object',
     60        dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', slug_field='slug', **crud_form_info_dict)),
     61    (r'create_update/delete/article/(?P<slug>[-\w]+)/$', 'django.views.generic.create_update.delete_object',
     62        dict(post_delete_redirect='/views/create_update/', slug_field='slug', **crud_form_info_dict)),
    4863)
  • tests/regressiontests/views/views.py

    === modified file 'tests/regressiontests/views/views.py'
     
    11from django.http import HttpResponse
     2import django.newforms as forms
     3from django.views.generic.create_update import create_object
     4
     5from models import Article
    26
    37def index_page(request):
    48    """Dummy index page"""
    59    return HttpResponse('<html><body>Dummy page</body></html>')
     10
     11def custom_create(request):
     12    """
     13    Calls create_object generic view with a custom form class.
     14    """
     15    class SlugChangingArticleForm(forms.ModelForm):
     16        """Custom form class to overwrite the slug."""
     17
     18        class Meta:
     19            model = Article
     20
     21        def save(self, *args, **kwargs):
     22            self.cleaned_data['slug'] = 'some-other-slug'
     23            return super(SlugChangingArticleForm, self).save(*args, **kwargs)
     24
     25    return create_object(request,
     26        post_save_redirect='/views/create_update/view/article/%(slug)s/',
     27        form_class=SlugChangingArticleForm)
Back to Top