~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Browser views for handling mailing lists."""

__metaclass__ = type
__all__ = [
    'HeldMessageView',
    'enabled_with_active_mailing_list',
    ]


from cgi import escape
from textwrap import TextWrapper
from urllib import quote

from zope.component import getUtility

from canonical.launchpad.webapp import (
    canonical_url,
    LaunchpadView,
    )
from lp.registry.interfaces.mailinglist import (
    IHeldMessageDetails,
    IMailingListSet,
    )
from lp.registry.interfaces.person import ITeam


class HeldMessageView(LaunchpadView):
    """A little helper view for for held messages."""

    def __init__(self, context, request):
        super(HeldMessageView, self).__init__(context, request)
        self.context = context
        self.request = request
        # The context object is an IMessageApproval, but we need some extra
        # details in order to present the u/i.  We need to adapt the
        # IMessageApproval into an IHeldMessageDetails in order to get most of
        # that extra detailed information.
        self.details = IHeldMessageDetails(self.context)
        # Some of the attributes are clear pass-throughs.
        self.message_id = self.details.message_id
        self.subject = self.details.subject
        self.date = self.details.date
        self.widget_name = 'field.' + quote(self.message_id)
        # The author field is very close to what the details has, except that
        # the view wants to include a link to the person's overview page.
        self.author = '<a href="%s">%s</a>' % (
            canonical_url(self.details.author),
            escape(self.details.sender))

    def initialize(self):
        """See `LaunchpadView`."""
        # Finally, the body text summary and details must be calculated from
        # the plain text body of the details object.
        #
        # Try to find a reasonable way to split the text of the message for
        # presentation as both a summary and a revealed detail.  This is
        # fraught with potential ugliness, so let's just do an 80% solution
        # that's safe and easy.
        text_lines = self._remove_leading_blank_lines()
        details = self._split_body(text_lines)
        # Now, ideally we'd like to wrap the details in <pre> tags so as to
        # preserve things like newlines in the original message body, but this
        # doesn't work very well with the JavaScript folding ellipsis control.
        # The next best, and easiest thing, is simply to replace all empty
        # blank lines in the details text with a <p> tag to give some
        # separation in the paragraphs.  No more than 20 lines in total
        # though, and here we don't worry about format="flowed".
        #
        # Again, 80% is good enough.
        paragraphs = []
        current_paragraph = []
        for lineno, line in enumerate(details.splitlines()):
            if lineno > 20:
                break
            if len(line.strip()) == 0:
                self._append_paragraph(paragraphs, current_paragraph)
                current_paragraph = []
            else:
                current_paragraph.append(line)
        self._append_paragraph(paragraphs, current_paragraph)
        self.body_details = u''.join(paragraphs)

    def _append_paragraph(self, paragraphs, current_paragraph):
        if len(current_paragraph) == 0:
            # There is nothing to append. The message has multiple
            # blank lines.
            return
        paragraphs.append(u'\n<p>\n')
        paragraphs.append(u'\n'.join(current_paragraph))
        paragraphs.append(u'\n</p>\n')

    def _remove_leading_blank_lines(self):
        """Strip off any leading blank lines.

        :return: The list of body text lines after stripping.
        """
        # Escape the text so that there's no chance of cross-site scripting,
        # then split into lines.
        text_lines = escape(self.details.body).splitlines()
        # Strip off any whitespace only lines from the start of the message.
        text_lines.reverse()
        while len(text_lines) > 0:
            first_line = text_lines.pop()
            if len(first_line.strip()) > 0:
                text_lines.append(first_line)
                break
        text_lines.reverse()
        return text_lines

    def _split_body(self, text_lines):
        """Split the body into summary and details.

        This will assign to self.body_summary the summary text, but it will
        return the details text for further santization.

        :return: the raw details text.
        """
        # If there are no non-blank lines, then we're done.
        if len(text_lines) == 0:
            self.body_summary = u''
            return u''
        # If the first line is of a completely arbitrarily chosen reasonable
        # length, then we'll just use that as the summary.
        elif len(text_lines[0]) < 60:
            self.body_summary = text_lines[0]
            return u'\n'.join(text_lines[1:])
        # It could be the case that the text is actually flowed using RFC
        # 3676 format="flowed" parameters.  In that case, just split the line
        # at the first whitespace after, again, our arbitrarily chosen limit.
        else:
            first_line = text_lines.pop(0)
            wrapper = TextWrapper(width=60)
            filled_lines = wrapper.fill(first_line).splitlines()
            self.body_summary = filled_lines[0]
            text_lines.insert(0, u''.join(filled_lines[1:]))
            return u'\n'.join(text_lines)


class enabled_with_active_mailing_list:
    """Disable the output link if the team's mailing list is not active."""

    def __init__(self, function):
        self._function = function

    def __get__(self, obj, type=None):
        """Called by the decorator machinery to return a decorated function.
        """

        def enable_if_active(*args, **kws):
            link = self._function(obj, *args, **kws)
            if not ITeam.providedBy(obj.context) or not obj.context.isTeam():
                link.enabled = False
            mailing_list = getUtility(IMailingListSet).get(obj.context.name)
            if mailing_list is None or not mailing_list.is_usable:
                link.enabled = False
            return link
        return enable_if_active