__init__.py 9.68 KB
Newer Older
Jeremy Kerr's avatar
Jeremy Kerr committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# Patchwork - automated patch tracking system
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
#
# This file is part of the Patchwork package.
#
# Patchwork is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Patchwork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

20
from __future__ import absolute_import
Jeremy Kerr's avatar
Jeremy Kerr committed
21

Stephen Finucane's avatar
Stephen Finucane committed
22
import datetime
23
import time
24
from email.encoders import encode_7or8bit, encode_base64
Stephen Finucane's avatar
Stephen Finucane committed
25 26 27 28 29 30
from email.header import Header
from email.mime.nonmultipart import MIMENonMultipart
from email.parser import HeaderParser
import email.utils
import re

31
from .base import *  # noqa
32
from patchwork.utils import Order, get_patch_ids, bundle_actions, set_bundle
Jeremy Kerr's avatar
Jeremy Kerr committed
33 34
from patchwork.paginator import Paginator
from patchwork.forms import MultiplePatchForm
35
from patchwork.models import Comment, Patch
36
from patchwork.filters import Filters
37
from patchwork.permissions import Can
Jeremy Kerr's avatar
Jeremy Kerr committed
38

39

Jeremy Kerr's avatar
Jeremy Kerr committed
40
def generic_list(request, project, view,
41 42
                 view_args={}, filter_settings=[], patches=None,
                 editable_order=False):
Jeremy Kerr's avatar
Jeremy Kerr committed
43

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
    filters = Filters(request)

    params = filters.params()
    for param in ['order', 'page']:
        data = {}
        if request.method == 'GET':
            data = request.GET
        elif request.method == 'POST':
            data = request.POST

        value = data.get(param, None)
        if value:
            params.append((param, value))

    context = {
      'messages': [],
      'project': project,
      'filters': filters,
      'projects': Project.objects.all(),
      'list_view': {'view': view, 'view_params': view_args, 'params': params},
    }
Jeremy Kerr's avatar
Jeremy Kerr committed
65

66 67 68 69 70 71
    data = {}
    if request.method == 'GET':
        data = request.GET
    elif request.method == 'POST':
        data = request.POST
    order = Order(data.get('order'), editable=editable_order)
Jeremy Kerr's avatar
Jeremy Kerr committed
72

73 74 75 76
    # Explicitly set data to None because request.POST will be an empty dict
    # when the form is not submitted, but passing a non-None data argument to
    # a forms.Form will make it bound and we don't want that to happen unless
    # there's been a form submission.
77 78
    if request.method != 'POST':
        data = None
79 80
    user = request.user
    properties_form = None
81
    if user.is_authenticated:
82 83 84 85 86 87 88

        # we only pass the post data to the MultiplePatchForm if that was
        # the actual form submitted
        data_tmp = None
        if data and data.get('form', '') == 'patchlistform':
            data_tmp = data

89
        properties_form = MultiplePatchForm(project, data=data_tmp)
90 91 92

    if request.method == 'POST' and data.get('form') == 'patchlistform':
        action = data.get('action', '').lower()
Jeremy Kerr's avatar
Jeremy Kerr committed
93 94 95

        # special case: the user may have hit enter in the 'create bundle'
        # text field, so if non-empty, assume the create action:
96
        if data.get('bundle_name', False):
Jeremy Kerr's avatar
Jeremy Kerr committed
97 98
            action = 'create'

99
        ps = Patch.objects.filter(id__in=get_patch_ids(data))
100 101 102 103 104 105 106 107 108

        if action in bundle_actions:
            errors = set_bundle(user, project, action, data, ps, context)

        elif properties_form and action == properties_form.action:
            errors = process_multiplepatch_form(properties_form, user,
                                                action, ps, context)
        else:
            errors = []
Jeremy Kerr's avatar
Jeremy Kerr committed
109 110 111 112 113 114

        if errors:
            context['errors'] = errors

    for (filterclass, setting) in filter_settings:
        if isinstance(setting, dict):
115
            context['filters'].set_status(filterclass, **setting)
Jeremy Kerr's avatar
Jeremy Kerr committed
116
        elif isinstance(setting, list):
117
            context['filters'].set_status(filterclass, *setting)
Jeremy Kerr's avatar
Jeremy Kerr committed
118
        else:
119
            context['filters'].set_status(filterclass, setting)
Jeremy Kerr's avatar
Jeremy Kerr committed
120

121
    if patches is None:
Jeremy Kerr's avatar
Jeremy Kerr committed
122 123
        patches = Patch.objects.filter(project=project)

Jeremy Kerr's avatar
Jeremy Kerr committed
124 125 126
    # annotate with tag counts
    patches = patches.with_tag_counts(project)

127
    patches = context['filters'].apply(patches)
128
    if not editable_order:
129
        patches = order.apply(patches)
Jeremy Kerr's avatar
Jeremy Kerr committed
130

131 132 133 134 135 136
    # we don't need the content or headers for a list; they're text fields
    # that can potentially contain a lot of data
    patches = patches.defer('content', 'headers')

    # but we will need to follow the state and submitter relations for
    # rendering the list template
137
    patches = patches.select_related('state', 'submitter', 'delegate')
138

Jeremy Kerr's avatar
Jeremy Kerr committed
139 140 141
    paginator = Paginator(request, patches)

    context.update({
142 143 144 145
        'page': paginator.current_page,
        'patchform': properties_form,
        'project': project,
        'order': order,
146
    })
Jeremy Kerr's avatar
Jeremy Kerr committed
147 148 149

    return context

150 151 152 153 154 155 156

def process_multiplepatch_form(form, user, action, patches, context):
    errors = []
    if not form.is_valid() or action != form.action:
        return ['The submitted form data was invalid']

    if len(patches) == 0:
157
        context['messages'] += ["No patches selected; nothing updated"]
158 159 160 161
        return errors

    changed_patches = 0
    for patch in patches:
162
        if not Can(user).edit(patch):
163
            errors.append("You don't have permissions to edit patch '%s'"
164
                          % patch.name)
165 166 167 168 169 170
            continue

        changed_patches += 1
        form.save(patch)

    if changed_patches == 1:
171
        context['messages'] += ["1 patch updated"]
172
    elif changed_patches > 1:
173
        context['messages'] += ["%d patches updated" % changed_patches]
174
    else:
175
        context['messages'] += ["No patches updated"]
176 177

    return errors
178

179

180 181
class PatchMbox(MIMENonMultipart):
    patch_charset = 'utf-8'
182

183 184
    def __init__(self, _text):
        MIMENonMultipart.__init__(self, 'text', 'plain',
185
                                  **{'charset': self.patch_charset})
186
        self.set_payload(_text.encode(self.patch_charset))
187

188 189
        if max((len(line) for line in _text.splitlines()), default=0) > 200:
            encode_base64(self)
190 191
        else:
            encode_7or8bit(self)
192

193

194 195 196 197
def get_from(patch, charset):
    if patch.headers:
        headers = HeaderParser().parsestr(patch.headers)
        if 'From' in headers:
198 199 200 201 202
            # XXX: FDO hax
            if '@lists.freedesktop.org' in headers['From'] \
               and 'Reply-To' in headers:
                return headers['Reply-To']

203 204 205 206 207 208 209 210
            return headers['From']

    # just in case we don't have headers in some old patches
    return email.utils.formataddr(
            (str(Header(patch.submitter.name, charset)),
                patch.submitter.email))


211 212 213 214 215 216
def revision_cover_letter_to_mbox(revision):
    if revision.raw_cover_letter:
        body = revision.raw_cover_letter
    else:
        body = revision.cover_letter

Arkadiusz Hiler's avatar
Arkadiusz Hiler committed
217 218 219
    if body is None:
        body = ''

220 221 222 223 224 225 226 227 228 229 230 231 232 233
    mail = PatchMbox(body + '\n')

    if revision.raw_cover_letter_headers:
        headers = HeaderParser().parsestr(revision.raw_cover_letter_headers)
        for header in ['Subject', 'From', 'Message-Id', 'To', 'Cc', 'Date']:
            if header in headers:
                mail[header] = headers[header]
    else:
        mail['Subject'] = "[PATCH 00/%02d] %s" % \
                          (revision.n_patches, revision.series.name)
        mail['From'] = revision.series.submitter.email_name()
        mail['Message-Id'] = "<filler-patchwork-generated-id-%d@patchwork>" % \
                             time.time()

234 235 236 237 238 239
    if revision.ordered_patches():
        from_time = revision.ordered_patches()[0].date.ctime()
    else:
        from_time = revision.completed.ctime()

    mail.set_unixfrom('From patchwork ' + from_time)
240 241 242 243

    return mail


244
def patch_to_mbox(patch, mbox_options={}):
245 246
    postscript_re = re.compile('\n-{2,3} ?\n')

247 248 249 250 251 252
    options = {
        'patch-link': None,
        'request': None,                 # needed to build the link URL
    }
    options.update(mbox_options)

253 254
    comment = None
    try:
255
        comment = Comment.objects.get(patch=patch, msgid=patch.msgid)
256 257 258 259 260 261 262 263 264 265 266
    except Exception:
        pass

    body = ''
    if comment:
        body = comment.content.strip() + "\n"

    parts = postscript_re.split(body, 1)
    if len(parts) == 2:
        (body, postscript) = parts
        body = body.strip() + "\n"
267
        postscript = postscript.rstrip()
268 269 270
    else:
        postscript = ''

271 272
    for comment in Comment.objects.filter(patch=patch) \
            .exclude(msgid=patch.msgid):
273 274
        body += comment.patch_responses()

275 276 277 278 279 280
    request = options.get('request')
    link_name = options['patch-link']
    if link_name and request:
        body += link_name + ': ' + \
                request.build_absolute_uri(patch.get_absolute_url()) + '\n'

281
    if postscript:
282
        body += '---\n' + postscript + '\n'
283 284 285 286

    if patch.content:
        body += '\n' + patch.content

287
    delta = patch.date - datetime.datetime.utcfromtimestamp(0)
288
    utc_timestamp = delta.seconds + delta.days * 24 * 3600
289 290 291

    mail = PatchMbox(body)
    mail['Subject'] = patch.name
292
    mail['From'] = get_from(patch, mail.patch_charset)
293
    mail['X-Patchwork-Id'] = str(patch.id)
294 295
    if patch.delegate:
        mail['X-Patchwork-Delegate'] = str(patch.delegate.email)
296 297 298
    mail['Message-Id'] = patch.msgid
    mail.set_unixfrom('From patchwork ' + patch.date.ctime())

299
    copied_headers = ['To', 'Cc', 'Date']
300 301 302 303 304
    orig_headers = HeaderParser().parsestr(str(patch.headers))
    for header in copied_headers:
        if header in orig_headers:
            mail[header] = orig_headers[header]

305 306 307
    if 'Date' not in mail:
        mail['Date'] = email.utils.formatdate(utc_timestamp)

308
    return mail