web/lib/django/contrib/formtools/tests.py
author ymh <ymh.work@gmail.com>
Wed, 02 Jun 2010 18:57:35 +0200
changeset 38 77b6da96e6f1
permissions -rw-r--r--
update django
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
38
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     1
import unittest
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     2
from django import forms
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     3
from django.contrib.formtools import preview, wizard, utils
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     4
from django import http
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     5
from django.test import TestCase
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     6
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     7
success_string = "Done was called!"
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     8
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     9
class TestFormPreview(preview.FormPreview):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    10
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    11
    def done(self, request, cleaned_data):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    12
        return http.HttpResponse(success_string)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    13
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    14
class TestForm(forms.Form):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    15
    field1 = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    16
    field1_ = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    17
    bool1 = forms.BooleanField(required=False)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    18
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    19
class PreviewTests(TestCase):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    20
    urls = 'django.contrib.formtools.test_urls'
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    21
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    22
    def setUp(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    23
        # Create a FormPreview instance to share between tests
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    24
        self.preview = preview.FormPreview(TestForm)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    25
        input_template = '<input type="hidden" name="%s" value="%s" />'
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    26
        self.input = input_template % (self.preview.unused_name('stage'), "%d")
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    27
        self.test_data = {'field1':u'foo', 'field1_':u'asdf'}
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    28
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    29
    def test_unused_name(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    30
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    31
        Verifies name mangling to get uniue field name.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    32
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    33
        self.assertEqual(self.preview.unused_name('field1'), 'field1__')
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    34
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    35
    def test_form_get(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    36
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    37
        Test contrib.formtools.preview form retrieval.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    38
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    39
        Use the client library to see if we can sucessfully retrieve
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    40
        the form (mostly testing the setup ROOT_URLCONF
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    41
        process). Verify that an additional  hidden input field
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    42
        is created to manage the stage.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    43
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    44
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    45
        response = self.client.get('/test1/')
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    46
        stage = self.input % 1
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    47
        self.assertContains(response, stage, 1)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    48
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    49
    def test_form_preview(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    50
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    51
        Test contrib.formtools.preview form preview rendering.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    52
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    53
        Use the client library to POST to the form to see if a preview
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    54
        is returned.  If we do get a form back check that the hidden
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    55
        value is correctly managing the state of the form.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    56
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    57
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    58
        # Pass strings for form submittal and add stage variable to
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    59
        # show we previously saw first stage of the form.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    60
        self.test_data.update({'stage': 1})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    61
        response = self.client.post('/test1/', self.test_data)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    62
        # Check to confirm stage is set to 2 in output form.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    63
        stage = self.input % 2
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    64
        self.assertContains(response, stage, 1)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    65
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    66
    def test_form_submit(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    67
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    68
        Test contrib.formtools.preview form submittal.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    69
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    70
        Use the client library to POST to the form with stage set to 3
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    71
        to see if our forms done() method is called. Check first
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    72
        without the security hash, verify failure, retry with security
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    73
        hash and verify sucess.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    74
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    75
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    76
        # Pass strings for form submittal and add stage variable to
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    77
        # show we previously saw first stage of the form.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    78
        self.test_data.update({'stage':2})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    79
        response = self.client.post('/test1/', self.test_data)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    80
        self.failIfEqual(response.content, success_string)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    81
        hash = self.preview.security_hash(None, TestForm(self.test_data))
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    82
        self.test_data.update({'hash': hash})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    83
        response = self.client.post('/test1/', self.test_data)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    84
        self.assertEqual(response.content, success_string)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    85
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    86
    def test_bool_submit(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    87
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    88
        Test contrib.formtools.preview form submittal when form contains:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    89
        BooleanField(required=False)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    90
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    91
        Ticket: #6209 - When an unchecked BooleanField is previewed, the preview
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    92
        form's hash would be computed with no value for ``bool1``. However, when
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    93
        the preview form is rendered, the unchecked hidden BooleanField would be
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    94
        rendered with the string value 'False'. So when the preview form is
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    95
        resubmitted, the hash would be computed with the value 'False' for
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    96
        ``bool1``. We need to make sure the hashes are the same in both cases.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    97
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    98
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    99
        self.test_data.update({'stage':2})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   100
        hash = self.preview.security_hash(None, TestForm(self.test_data))
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   101
        self.test_data.update({'hash':hash, 'bool1':u'False'})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   102
        response = self.client.post('/test1/', self.test_data)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   103
        self.assertEqual(response.content, success_string)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   104
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   105
class SecurityHashTests(unittest.TestCase):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   106
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   107
    def test_textfield_hash(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   108
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   109
        Regression test for #10034: the hash generation function should ignore
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   110
        leading/trailing whitespace so as to be friendly to broken browsers that
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   111
        submit it (usually in textareas).
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   112
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   113
        f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   114
        f2 = HashTestForm({'name': '  joe', 'bio': 'Nothing notable.  '})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   115
        hash1 = utils.security_hash(None, f1)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   116
        hash2 = utils.security_hash(None, f2)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   117
        self.assertEqual(hash1, hash2)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   118
        
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   119
    def test_empty_permitted(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   120
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   121
        Regression test for #10643: the security hash should allow forms with
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   122
        empty_permitted = True, or forms where data has not changed.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   123
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   124
        f1 = HashTestBlankForm({})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   125
        f2 = HashTestForm({}, empty_permitted=True)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   126
        hash1 = utils.security_hash(None, f1)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   127
        hash2 = utils.security_hash(None, f2)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   128
        self.assertEqual(hash1, hash2)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   129
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   130
class HashTestForm(forms.Form):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   131
    name = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   132
    bio = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   133
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   134
class HashTestBlankForm(forms.Form):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   135
    name = forms.CharField(required=False)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   136
    bio = forms.CharField(required=False)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   137
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   138
#
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   139
# FormWizard tests
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   140
#
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   141
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   142
class WizardPageOneForm(forms.Form):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   143
    field = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   144
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   145
class WizardPageTwoForm(forms.Form):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   146
    field = forms.CharField()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   147
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   148
class WizardClass(wizard.FormWizard):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   149
    def render_template(self, *args, **kw):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   150
        return http.HttpResponse("")
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   151
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   152
    def done(self, request, cleaned_data):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   153
        return http.HttpResponse(success_string)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   154
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   155
class DummyRequest(http.HttpRequest):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   156
    def __init__(self, POST=None):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   157
        super(DummyRequest, self).__init__()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   158
        self.method = POST and "POST" or "GET"
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   159
        if POST is not None:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   160
            self.POST.update(POST)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   161
        self._dont_enforce_csrf_checks = True
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   162
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   163
class WizardTests(TestCase):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   164
    def test_step_starts_at_zero(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   165
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   166
        step should be zero for the first form
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   167
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   168
        wizard = WizardClass([WizardPageOneForm, WizardPageTwoForm])
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   169
        request = DummyRequest()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   170
        wizard(request)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   171
        self.assertEquals(0, wizard.step)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   172
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   173
    def test_step_increments(self):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   174
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   175
        step should be incremented when we go to the next page
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   176
        """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   177
        wizard = WizardClass([WizardPageOneForm, WizardPageTwoForm])
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   178
        request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"})
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   179
        response = wizard(request)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   180
        self.assertEquals(1, wizard.step)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   181