Review Board

beta

Move reviewboard.utils and emailtags into Djblets

Updated 10 months, 3 weeks ago

Christian Hammond Reviewers
trunk reviewboard
None Navi
Moved all of reviewboard.utils and reviewboard.reviews.templatetags.emailtags into djblets.util.

The template tags have been split into sections: djblets_utils (which has most of the old htmlutils), djblets_email (which has the old emailtags), djblets_deco (which contains the box tags), and djblets_js (which for now contains only the JavaScript dialog serialization code).

The Review Board side of this change will be in a separate review request.
Made sure Review Board's unit tests test Djblets and that everything works fine. Went to all the main Review Board pages without problems.
/trunk/djblets/djblets/util/fields.py
New File
1
#
2
# fields.py -- Model fields.
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26
27
from datetime import datetime
28
29
from django.db import models
30
31
32
class ModificationTimestampField(models.DateTimeField):
33
    """
34
    A subclass of DateTimeField that only auto-updates the timestamp when
35
    updating an existing object or when the value of the field is None. This
36
    specialized field is equivalent to DateTimeField's auto_now=True, except
37
    it allows for custom timestamp values (needed for
38
    serialization/deserialization).
39
    """
40
    def __init__(self, verbose_name=None, name=None, **kwargs):
41
        kwargs.update({
42
            'editable': False,
43
            'blank': True,
44
        })
45
        models.DateTimeField.__init__(self, verbose_name, name, **kwargs)
46
47
    def pre_save(self, model, add):
48
        if not add or getattr(model, self.attname) is None:
49
            value = datetime.now()
50
            setattr(model, self.attname, value)
51
            return value
52
53
        return super(ModificationTimestampField, self).pre_save(model, add)
54
55
    def get_internal_type(self):
56
        return "DateTimeField"
/trunk/djblets/djblets/util/tests.py
New File
1
#
2
# tests.py -- Unit tests for classes in djblets.util
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26
27
import datetime
28
import unittest
29
30
from django.conf import settings
31
from django.template import Token, TOKEN_TEXT, TemplateSyntaxError
32
from django.utils.html import strip_spaces_between_tags
33
34
from djblets.util.testing import TestCase, TagTest
35
from djblets.util.templatetags import djblets_deco
36
from djblets.util.templatetags import djblets_email
37
from djblets.util.templatetags import djblets_utils
38
39
40
def normalize_html(s):
41
    return strip_spaces_between_tags(s).strip()
42
43
44
class BoxTest(TagTest):
45
    def testPlain(self):
46
        """Testing box tag"""
47
        node = djblets_deco.box(self.parser, Token(TOKEN_TEXT, 'box'))
48
        context = {}
49
50
        self.assertEqual(normalize_html(node.render(context)),
51
                         '<div class="box-container"><div class="box">' +
52
                         '<div class="box-inner">\ncontent\n  ' +
53
                         '</div></div></div>')
54
55
    def testClass(self):
56
        """Testing box tag (with extra class)"""
57
        node = djblets_deco.box(self.parser, Token(TOKEN_TEXT, 'box "class"'))
58
        context = {}
59
60
        self.assertEqual(normalize_html(node.render(context)),
61
                         '<div class="box-container"><div class="box class">' +
62
                         '<div class="box-inner">\ncontent\n  ' +
63
                         '</div></div></div>')
64
65
    def testError(self):
66
        """Testing box tag (invalid usage)"""
67
        self.assertRaises(TemplateSyntaxError,
68
                          lambda: djblets_deco.box(self.parser,
69
                                                   Token(TOKEN_TEXT,
70
                                                         'box "class" "foo"')))
71
72
73
class ErrorBoxTest(TagTest):
74
    def testPlain(self):
75
        """Testing errorbox tag"""
76
        node = djblets_deco.errorbox(self.parser,
77
                                     Token(TOKEN_TEXT, 'errorbox'))
78
79
        context = {}
80
81
        self.assertEqual(normalize_html(node.render(context)),
82
                         '<div class="errorbox">\ncontent\n</div>')
83
84
    def testId(self):
85
        """Testing errorbox tag (with id)"""
86
        node = djblets_deco.errorbox(self.parser,
87
                                     Token(TOKEN_TEXT, 'errorbox "id"'))
88
89
        context = {}
90
91
        self.assertEqual(normalize_html(node.render(context)),
92
                         '<div class="errorbox" id="id">\ncontent\n</div>')
93
94
95
    def testError(self):
96
        """Testing errorbox tag (invalid usage)"""
97
        self.assertRaises(TemplateSyntaxError,
98
                          lambda: djblets_deco.errorbox(self.parser,
99
                                                        Token(TOKEN_TEXT,
100
                                                              'errorbox "id" ' +
101
                                                              '"foo"')))
102
103
104
class AgeIdTest(TagTest):
105
    def setUp(self):
106
        TagTest.setUp(self)
107
108
        self.now = datetime.datetime.now()
109
110
        self.context = {
111
            'now':    self.now,
112
            'minus1': self.now - datetime.timedelta(1),
113
            'minus2': self.now - datetime.timedelta(2),
114
            'minus3': self.now - datetime.timedelta(3),
115
            'minus4': self.now - datetime.timedelta(4),
116
        }
117
118
    def testNow(self):
119
        """Testing ageid tag (now)"""
120
        self.assertEqual(djblets_utils.ageid(self.now), 'age1')
121
122
    def testMinus1(self):
123
        """Testing ageid tag (yesterday)"""
124
        self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(1)),
125
                         'age2')
126
127
    def testMinus2(self):
128
        """Testing ageid tag (two days ago)"""
129
        self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(2)),
130
                         'age3')
131
132
    def testMinus3(self):
133
        """Testing ageid tag (three days ago)"""
134
        self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(3)),
135
                         'age4')
136
137
    def testMinus4(self):
138
        """Testing ageid tag (four days ago)"""
139
        self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(4)),
140
                         'age5')
141
142
    def testNotDateTime(self):
143
        """Testing ageid tag (non-datetime object)"""
144
        class Foo:
145
            def __init__(self, now):
146
                self.day   = now.day
147
                self.month = now.month
148
                self.year  = now.year
149
150
        self.assertEqual(djblets_utils.ageid(Foo(self.now)), 'age1')
151
152
153
class TestEscapeSpaces(unittest.TestCase):
154
    def test(self):
155
        """Testing escapespaces filter"""
156
        self.assertEqual(djblets_utils.escapespaces('Hi there'),
157
                         'Hi there')
158
        self.assertEqual(djblets_utils.escapespaces('Hi  there'),
159
                         'Hi&nbsp; there')
160
        self.assertEqual(djblets_utils.escapespaces('Hi  there\n'),
161
                         'Hi&nbsp; there<br />')
162
163
164
class TestHumanizeList(unittest.TestCase):
165
    def test0(self):
166
        """Testing humanize_list filter (length 0)"""
167
        self.assertEqual(djblets_utils.humanize_list([]), '')
168
169
    def test1(self):
170
        """Testing humanize_list filter (length 1)"""
171
        self.assertEqual(djblets_utils.humanize_list(['a']), 'a')
172
173
    def test2(self):
174
        """Testing humanize_list filter (length 2)"""
175
        self.assertEqual(djblets_utils.humanize_list(['a', 'b']), 'a and b')
176
177
    def test3(self):
178
        """Testing humanize_list filter (length 3)"""
179
        self.assertEqual(djblets_utils.humanize_list(['a', 'b', 'c']),
180
                         'a, b and c')
181
182
    def test4(self):
183
        """Testing humanize_list filter (length 4)"""
184
        self.assertEqual(djblets_utils.humanize_list(['a', 'b', 'c', 'd']),
185
                         'a, b, c, and d')
186
187
188
class TestIndent(unittest.TestCase):
189
    def test(self):
190
        """Testing indent filter"""
191
        self.assertEqual(djblets_utils.indent('foo'), '    foo')
192
        self.assertEqual(djblets_utils.indent('foo', 3), '   foo')
193
        self.assertEqual(djblets_utils.indent('foo\nbar'), '    foo\n    bar')
194
195
196
class QuotedEmailTagTest(TagTest):
197
    def testInvalid(self):
198
        """Testing quoted_email tag (invalid usage)"""
199
        self.assertRaises(TemplateSyntaxError,
200
                          lambda: djblets_email.quoted_email(self.parser,
201
                              Token(TOKEN_TEXT, 'quoted_email')))
202
203
204
class CondenseTagTest(TagTest):
205
    def getContentText(self):
206
        return "foo\nbar\n\n\n\n\n\n\nfoobar!"
207
208
    def testPlain(self):
209
        """Testing condense tag"""
210
        node = djblets_email.condense(self.parser,
211
                                      Token(TOKEN_TEXT, 'condense'))
212
        self.assertEqual(node.render({}), "foo\nbar\n\n\nfoobar!")
213
214
215
class QuoteTextFilterTest(unittest.TestCase):
216
    def testPlain(self):
217
        """Testing quote_text filter (default level)"""
218
        self.assertEqual(djblets_email.quote_text("foo\nbar"),
219
                         "> foo\n> bar")
220
221
    def testLevel2(self):
222
        """Testing quote_text filter (level 2)"""
223
        self.assertEqual(djblets_email.quote_text("foo\nbar", 2),
224
                         "> > foo\n> > bar")
/trunk/djblets/djblets/util/templatetags/djblets_deco.py
New File
1
#
2
# djblets_deco.py -- Decorational template tags
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26
27
from django import template
28
from django.template.loader import render_to_string
29
30
from djblets.util.decorators import blocktag
31
32
33
register = template.Library()
34
35
36
@register.tag
37
@blocktag
38
def box(context, nodelist, classname=None):
39
    """
40
    Displays a box container around content, with an optional class name.
41
    """
42
    return render_to_string('box.html', {
43
        'classname': classname or "",
44
        'content': nodelist.render(context)
45
    })
46
47
48
@register.tag
49
@blocktag
50
def errorbox(context, nodelist, box_id=None):
51
    """
52
    Displays an error box around content, with an optional ID.
53
    """
54
    return render_to_string('errorbox.html', {
55
        'box_id': box_id or "",
56
        'content': nodelist.render(context)
57
    })
/trunk/djblets/djblets/util/templatetags/djblets_email.py
New File
1
#
2
# djblets_email.py -- E-mail formatting template tags
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26
27
import re
28
29
from django import template
30
from django.template.loader import render_to_string
31
32
from djblets.util.decorators import basictag, blocktag
33
from djblets.util.templatetags.djblets_utils import humanize_list
34
35
36
register = template.Library()
37
38
39
@register.tag
40
@basictag(takes_context=True)
41
def quoted_email(context, template_name):
42
    """
43
    Renders a specified template as a quoted reply, using the current context.
44
    """
45
    return quote_text(render_to_string(template_name, context))
46
47
48
@register.tag
49
@blocktag
50
def condense(context, nodelist):
51
    """
52
    Condenses a block of text so that there are never more than three
53
    consecutive newlines.
54
    """
55
    text = nodelist.render(context).strip()
56
    text = re.sub("\n{4,}", "\n\n\n", text)
57
    return text
58
59
60
@register.filter
61
def quote_text(text, level=1):
62
    """
63
    Quotes a block of text the specified number of times.
64
    """
65
    lines = text.split("\n")
66
    quoted = ""
67
68
    for line in lines:
69
        quoted += "%s%s\n" % ("> " * level, line)
70
71
    return quoted.rstrip()
72
/trunk/djblets/djblets/util/templatetags/djblets_images.py
New File
1
#
2
# djblets_images.py -- Image-related template tags
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26
27
import Image
28
import os
29
30
from django import template
31
from django.conf import settings
32
33
34
register = template.Library()
35
36
37
@register.simple_tag
38
def crop_image(file, x, y, width, height):
39
    """
40
    Crops an image at the specified coordinates and dimensions, returning the
41
    resulting URL of the cropped image.
42
    """
43
    if file.find(".") != -1:
44
        basename, format = file.rsplit('.', 1)
45
        new_name = '%s_%s_%s_%s_%s.%s' % (basename, x, y, width, height, format)
46
    else:
47
        basename = file
48
        new_name = '%s_%s_%s_%s_%s' % (basename, x, y, width, height)
49
50
    new_path = os.path.join(settings.MEDIA_ROOT, new_name)
51
    new_url = os.path.join(settings.MEDIA_URL, new_name)
52
53
    if not os.path.exists(new_path):
54
        try:
55
            image = Image.open(os.path.join(settings.MEDIA_ROOT, file))
56
            image = image.crop((x, y, x + width, y + height))
57
            image.save(new_path, image.format)
58
        except (IOError, KeyError):
59
            return ""
60
61
    return new_url
62
63
64
# From http://www.djangosnippets.org/snippets/192
65
@register.filter
66
def thumbnail(file, size='400x100'):
67
    """
68
    Creates a thumbnail of an image with the specified size, returning
69
    the URL of the thumbnail.
70
    """
71
    x, y = [int(x) for x in size.split('x')]
72
73
    if file.find(".") != -1:
74
        basename, format = file.rsplit('.', 1)
75
        miniature = '%s_%s.%s' % (basename, size, format)
76
    else:
77
        basename = file
78
        miniature = '%s_%s' % (basename, size)
79
80
    miniature_filename = os.path.join(settings.MEDIA_ROOT, miniature)
81
    miniature_url = os.path.join(settings.MEDIA_URL, miniature)
82
83
    if not os.path.exists(miniature_filename):
84
        try:
85
            image = Image.open(os.path.join(settings.MEDIA_ROOT, file))
86
            image.thumbnail([x, y], Image.ANTIALIAS)
87
            image.save(miniature_filename, image.format)
88
        except IOError:
89
            return ""
90
        except KeyError:
91
            return ""
92
93
    return miniature_url
/trunk/djblets/djblets/util/templatetags/djblets_js.py
New File
1
#
2
# djblets_js.py -- JavaScript-related template tags
3
#
4
# Copyright (c) 2007-2008  Christian Hammond
5
# Copyright (c) 2007-2008  David Trowbridge
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining
8
# a copy of this software and associated documentation files (the
9
# "Software"), to deal in the Software without restriction, including
10
# without limitation the rights to use, copy, modify, merge, publish,
11
# distribute, sublicense, and/or sell copies of the Software, and to
12
# permit persons to whom the Software is furnished to do so, subject to
13
# the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included
16
# in all copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19